Hoi folks,

I've started to toy with VPP and eVPN/VxLAN, and took a look at the evpn branch from a few years ago. For my network, I'll need the OSPFv3 'unnumbered' features we built, so I thought I'd ask - would it be possible to rebase the evpn branch ? I've taken a stab at it (see attached patch) by replaying the 9 commits on top if HEAD (f1a7229d-evpn.diff).

It may not be correct, but it does compile and seemingly work :)

root@vpp0-0:/etc/bird# birdc show proto
BIRD 2.18+branch.evpn.1efd115564ea ready.
Name       Proto      Table      State  Since         Info
device1    Device     ---        up     12:20:01.347
direct1    Direct     ---        up     12:20:01.347
kernel4    Kernel     master4    up     12:20:01.347
kernel6    Kernel     master6    up     12:20:01.347
static4    Static     master4    up     12:20:01.347
static6    Static     master6    up     12:20:01.347
bfd1       BFD        ---        up     12:20:01.347
ospf4      OSPF       master4    up     12:20:01.347  Running
ospf6      OSPF       master6    up     12:20:01.347  Running
bridge1    Bridge     etab       up     12:45:13.450
evpn1      EVPN       ---        up     12:45:13.450

I have a few further changes, notably a new 'vpp protocol' to handle communication with the VPP dataplane for eVPN/VxLAN (and possibly later, GENEVE and MPLS). Before I get too far down the rabbit hole though, I'm also wondering: is releasing eVPN in the cards?

groet,
Pim

--
Pim van Pelt <[email protected]>
PBVP1-RIPE https://ipng.ch/
diff --git a/conf/cf-lex.l b/conf/cf-lex.l
index 0bf76e9b..edd233fa 100644
--- a/conf/cf-lex.l
+++ b/conf/cf-lex.l
@@ -246,7 +246,19 @@ WHITE [ \t]
   return IP4;
 }
 
-({XIGIT}{2}){16,}|{XIGIT}{2}(:{XIGIT}{2}){15,}|hex:({XIGIT}{2}*|{XIGIT}{2}(:{XIGIT}{2})*)
 {
+{XIGIT}{2}(:{XIGIT}{2}){5} {
+  mac_addr mac;
+  char *s = yytext;
+
+  int len = bstrhextobin(s, mac.addr);
+  if (len != 6)
+    cf_error("Invalid MAC address");
+
+  cf_lval.mac = mac;
+  return MAC_;
+}
+
+({XIGIT}{2}){10,}|{XIGIT}{2}(:{XIGIT}{2}){9,}|hex:({XIGIT}{2}*|{XIGIT}{2}(:{XIGIT}{2})*)
 {
   char *s = yytext;
   struct adata *bs;
 
diff --git a/conf/confbase.Y b/conf/confbase.Y
index 27c422ea..9e566ce7 100644
--- a/conf/confbase.Y
+++ b/conf/confbase.Y
@@ -16,6 +16,7 @@ CF_HDR
 #include "lib/socket.h"
 #include "lib/timer.h"
 #include "lib/string.h"
+#include "lib/evpn.h"
 #include "nest/protocol.h"
 #include "nest/iface.h"
 #include "nest/route.h"
@@ -86,7 +87,9 @@ CF_DECLS
   ip_addr a;
   ip4_addr ip4;
   ip6_addr ip6;
+  mac_addr mac;
   net_addr net;
+  evpn_esi esi;
   net_addr *net_ptr;
   struct symbol *s;
   struct keyword *kw;
@@ -135,6 +138,7 @@ CF_DECLS
 %token <i> NUM ENUM_TOKEN
 %token <ip4> IP4
 %token <ip6> IP6
+%token <mac> MAC_
 %token <rd> VPN_RD
 %token <s> CF_SYM_KNOWN CF_SYM_UNDEFINED CF_SYM_METHOD_BARE CF_SYM_METHOD_ARGS
 %token <t> TEXT
@@ -146,8 +150,10 @@ CF_DECLS
 %type <time> expr_us time
 %type <a> ipa
 %type <net> net_ip4_ net_ip4 net_ip6_ net_ip6 net_ip_ net_ip net_or_ipa
-%type <net_ptr> net_ net_any net_vpn4_ net_vpn6_ net_vpn_ net_roa4_ net_roa6_ 
net_roa_ net_ip6_sadr_ net_mpls_ net_aspa_
+%type <net_ptr> net_ net_any net_vpn4_ net_vpn6_ net_vpn_ net_roa4_ net_roa6_ 
net_roa_ net_ip6_sadr_ net_eth_ net_mpls_ net_aspa_
+%type <net_ptr> net_evpn_ net_evpn_ead_ net_evpn_mac_ net_evpn_mac_ip_ 
net_evpn_imet_ net_evpn_es_
 %type <mls> label_stack_start label_stack
+%type <esi> evpn_esi
 
 %type <t> text opttext
 %type <bs> bytestring
@@ -168,7 +174,8 @@ CF_DECLS
 /* See r_args */
 %expect 2
 
-CF_KEYWORDS(DEFINE, ON, OFF, YES, NO, S, MS, US, PORT, VPN, MPLS, FROM, MAX, 
AS)
+CF_KEYWORDS(DEFINE, ON, OFF, YES, NO, S, MS, US, PORT, VPN, ETH, VLAN, MPLS, 
FROM, MAX, AS)
+CF_KEYWORDS(EVPN, EAD, MAC, IMET, ES)
 
 CF_GRAMMAR
 
@@ -360,6 +367,18 @@ net_roa6_: net_ip6_ MAX expr AS expr
     cf_error("Invalid max prefix length %u", $3);
 };
 
+net_eth_: ETH MAC_
+{
+  $$ = cfg_alloc(sizeof(net_addr_eth));
+  net_fill_eth($$, $2, 0);
+}
+
+net_eth_: ETH MAC_ VLAN NUM
+{
+  $$ = cfg_alloc(sizeof(net_addr_eth));
+  net_fill_eth($$, $2, $4);
+}
+
 net_mpls_: MPLS expr
 {
   $$ = cfg_alloc(sizeof(net_addr_mpls));
@@ -372,6 +391,46 @@ net_aspa_: ASPA expr
   net_fill_aspa($$, $2);
 }
 
+evpn_esi: bytestring
+{
+  if ($1->length != 10)
+    cf_error("Invalid ESI length %u", (uint) $1->length);
+
+  memcpy(&$$, $1->data, 10);
+}
+
+net_evpn_ead_: EVPN EAD VPN_RD NUM evpn_esi
+{
+  $$ = cfg_alloc(sizeof(net_addr_evpn_ead));
+  net_fill_evpn_ead($$, rd_to_u64($3), $4, $5);
+}
+
+net_evpn_mac_: EVPN MAC VPN_RD NUM MAC_ '*'
+{
+  $$ = cfg_alloc(sizeof(net_addr_evpn_mac));
+  net_fill_evpn_mac($$, rd_to_u64($3), $4, $5);
+}
+
+net_evpn_mac_ip_: EVPN MAC VPN_RD NUM MAC_ ipa
+{
+  $$ = cfg_alloc(sizeof(net_addr_evpn_mac_ip));
+  net_fill_evpn_mac_ip($$, rd_to_u64($3), $4, $5, $6);
+}
+
+net_evpn_imet_: EVPN IMET VPN_RD NUM ipa
+{
+  $$ = cfg_alloc(sizeof(net_addr_evpn_imet));
+  net_fill_evpn_imet($$, rd_to_u64($3), $4, $5);
+}
+
+net_evpn_es_: EVPN ES VPN_RD evpn_esi ipa
+{
+  $$ = cfg_alloc(sizeof(net_addr_evpn_es));
+  net_fill_evpn_es($$, rd_to_u64($3), $4, $5);
+}
+
+net_evpn_: net_evpn_ead_ | net_evpn_mac_ | net_evpn_mac_ip_ | net_evpn_imet_ | 
net_evpn_es_ ;
+
 net_ip_: net_ip4_ | net_ip6_ ;
 net_vpn_: net_vpn4_ | net_vpn6_ ;
 net_roa_: net_roa4_ | net_roa6_ ;
@@ -382,8 +441,10 @@ net_:
  | net_roa_
  | net_flow_
  | net_ip6_sadr_
+ | net_eth_
  | net_mpls_
  | net_aspa_
+ | net_evpn_
  ;
 
 
diff --git a/configure.ac b/configure.ac
index c27573cf..38908bae 100644
--- a/configure.ac
+++ b/configure.ac
@@ -319,7 +319,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 evpn l3vpn mrt ospf perf 
pipe radv rip rpki static"
 
 all_protocols=`echo $all_protocols | sed 's/ /,/g'`
 
@@ -332,6 +332,8 @@ 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_EVPN],     [EVPN protocol])
 AH_TEMPLATE([CONFIG_L3VPN],    [L3VPN protocol])
 AH_TEMPLATE([CONFIG_MRT],      [MRT protocol])
 AH_TEMPLATE([CONFIG_OSPF],     [OSPF protocol])
diff --git a/filter/config.Y b/filter/config.Y
index 1fa17afd..064d36bd 100644
--- a/filter/config.Y
+++ b/filter/config.Y
@@ -126,6 +126,7 @@ f_valid_set_type(int type)
   case T_EC:
   case T_LC:
   case T_RD:
+  case T_MAC:
     return 1;
 
   default:
@@ -357,7 +358,7 @@ CF_DECLS
 CF_KEYWORDS_EXCLUSIVE(IN)
 CF_KEYWORDS(FUNCTION, PRINT, PRINTN, UNSET, RETURN,
        ACCEPT, REJECT, ERROR,
-       INT, BOOL, IP, PREFIX, RD, PAIR, QUAD, EC, LC, ENUM,
+       INT, BOOL, IP, PREFIX, RD, MAC, PAIR, QUAD, EC, LC, ENUM,
        SET, STRING, BYTESTRING, BGPMASK, BGPPATH, CLIST, ECLIST, LCLIST,
        IF, THEN, ELSE, CASE,
        FOR, DO,
@@ -456,6 +457,7 @@ type:
  | BOOL { $$ = T_BOOL; }
  | IP { $$ = T_IP; }
  | RD { $$ = T_RD; }
+ | MAC { $$ = T_MAC; }
  | PREFIX { $$ = T_NET; }
  | PAIR { $$ = T_PAIR; }
  | QUAD { $$ = T_QUAD; }
@@ -477,8 +479,9 @@ type:
          case T_ENUM:
          case T_EC:
          case T_LC:
-         case T_RD:
          case T_IP:
+         case T_RD:
+         case T_MAC:
               $$ = T_SET;
               break;
 
@@ -648,6 +651,7 @@ set_atom0:
    NUM    { $$.type = T_INT; $$.val.i = $1; }
  | fipa   { $$ = $1; }
  | VPN_RD { $$.type = T_RD; $$.val.rd = $1; }
+ | MAC_   { $$.type = T_MAC; $$.val.mac = $1; }
  | ENUM_TOKEN { $$.type = pair_a($1); $$.val.i = pair_b($1); }
  | '(' term ')' {
      $$ = cf_eval($2, T_VOID);
@@ -808,6 +812,7 @@ constant:
  | BYTETEXT { $$ = f_new_inst(FI_CONSTANT, (struct f_val) { .type = 
T_BYTESTRING, .val.bs = $1, }); }
  | fipa     { $$ = f_new_inst(FI_CONSTANT, $1); }
  | VPN_RD   { $$ = f_new_inst(FI_CONSTANT, (struct f_val) { .type = T_RD, 
.val.rd = $1, }); }
+ | MAC_     { $$ = f_new_inst(FI_CONSTANT, (struct f_val) { .type = T_MAC, 
.val.mac = $1, }); }
  | net_     { $$ = f_new_inst(FI_CONSTANT, (struct f_val) { .type = T_NET, 
.val.net = $1, }); }
  | '[' ']' { $$ = f_new_inst(FI_CONSTANT, (struct f_val) { .type = T_SET, 
.val.t = NULL, }); }
  | '[' set_items ']' {
diff --git a/filter/data.c b/filter/data.c
index 211faeea..4f9760e5 100644
--- a/filter/data.c
+++ b/filter/data.c
@@ -44,6 +44,7 @@ static const char * const f_type_str[] = {
   [T_ENUM_RA_PREFERENCE] = "enum ra_preference",
   [T_ENUM_AF]  = "enum af",
   [T_ENUM_MPLS_POLICY] = "enum mpls_policy",
+  [T_ENUM_EVPN_TYPE] = "enum net_evpn",
 
   [T_IP]       = "ip",
   [T_NET]      = "prefix",
@@ -57,6 +58,7 @@ static const char * const f_type_str[] = {
   [T_LC]       = "lc",
   [T_LCLIST]   = "lclist",
   [T_RD]       = "rd",
+  [T_MAC]      = "mac",
 
   [T_ROUTE] = "route",
   [T_ROUTES_BLOCK] = "block of routes",
@@ -207,6 +209,8 @@ val_compare(const struct f_val *v1, const struct f_val *v2)
     return lcomm_cmp(v1->val.lc, v2->val.lc);
   case T_IP:
     return ipa_compare(v1->val.ip, v2->val.ip);
+  case T_MAC:
+    return mac_compare(v1->val.mac, v2->val.mac);
   case T_NET:
     return net_compare(v1->val.net, v2->val.net);
   case T_STRING:;
@@ -635,6 +639,7 @@ val_format(const struct f_val *v, buffer *buf)
   case T_EC:   ec_format(buf2, v->val.ec); buffer_print(buf, "%s", buf2); 
return;
   case T_LC:   lc_format(buf2, v->val.lc); buffer_print(buf, "%s", buf2); 
return;
   case T_RD:   rd_format(v->val.rd, buf2, 1024); buffer_print(buf, "%s", 
buf2); return;
+  case T_MAC:  buffer_print(buf, "%6b", v->val.mac.addr); return;
   case T_PREFIX_SET: trie_format(v->val.ti, buf); return;
   case T_SET:  tree_format(v->val.t, buf); return;
   case T_ENUM: buffer_print(buf, "(enum %x)%u", v->type, v->val.i); return;
diff --git a/filter/data.h b/filter/data.h
index a1986496..a2e29252 100644
--- a/filter/data.h
+++ b/filter/data.h
@@ -43,6 +43,7 @@ enum f_type {
   T_ENUM_RA_PREFERENCE = 0x37,
   T_ENUM_AF = 0x38,
   T_ENUM_MPLS_POLICY = 0x39,
+  T_ENUM_EVPN_TYPE = 0x3a,
 
 /* new enums go here */
   T_ENUM_EMPTY = 0x3f, /* Special hack for atomic_aggr */
@@ -63,6 +64,7 @@ enum f_type {
   T_RD = 0x2a,         /* Route distinguisher for VPN addresses */
   T_PATH_MASK_ITEM = 0x2b,     /* Path mask item for path mask constructors */
   T_BYTESTRING = 0x2c,
+  T_MAC = 0x2d,
 
   T_ROUTE = 0x78,
   T_ROUTES_BLOCK = 0x79,
@@ -87,6 +89,7 @@ struct f_val {
     lcomm lc;
     vpn_rd rd;
     ip_addr ip;
+    mac_addr mac;
     const net_addr *net;
     const char *s;
     const struct adata *bs;
diff --git a/filter/f-inst.c b/filter/f-inst.c
index 81360430..4350d8dc 100644
--- a/filter/f-inst.c
+++ b/filter/f-inst.c
@@ -1095,16 +1095,118 @@
   ]]);
 
   /* Convert prefix to IP */
-  METHOD_R(T_NET, ip, T_IP, ip, net_prefix(v1.val.net));
+  METHOD(T_NET, ip, 0, [[
+    const net_addr_union *net = (void *) v1.val.net;
 
+    if ((net->n.type == NET_EVPN) && (net->evpn.subtype == NET_EVPN_MAC))
+      RESULT(T_IP, ip, (net->n.length == sizeof(net_addr_evpn_mac_ip)) ? 
net->evpn.mac_ip.ip : IPA_NONE);
+    else
+      RESULT(T_IP, ip, net_prefix(v1.val.net));
+  ]]);
+
+  /* Get route distinguisher */
   INST(FI_ROUTE_DISTINGUISHER, 1, 1) {
     ARG(1, T_NET);
     METHOD_CONSTRUCTOR("rd");
-    if (!net_is_vpn(v1.val.net))
-      runtime( "VPN address expected" );
+    if (!net_type_match(v1.val.net, NB_VPN | NB_EVPN))
+      runtime( "VPN or EVPN address expected" );
     RESULT(T_RD, rd, net_rd(v1.val.net));
   }
 
+  /* Get MAC address */
+  METHOD(T_NET, mac, 0, [[
+    const net_addr_union *net = (void *) v1.val.net;
+
+    if (net->n.type == NET_ETH)
+      RESULT(T_MAC, mac, net->eth.mac);
+    else if ((net->n.type == NET_EVPN) && (net->evpn.subtype == NET_EVPN_MAC))
+      RESULT(T_MAC, mac, net->evpn.mac.mac);
+    else
+      runtime( "Ethernet or EVPN MAC expected" );
+  ]]);
+
+  /* Get VLAN ID */
+  METHOD(T_NET, vlan_id, 0, [[
+    if (v1.val.net->type != NET_ETH)
+      runtime( "Ethernet address expected" );
+
+    const net_addr_eth *eth = (void *) v1.val.net;
+    RESULT(T_INT, i, eth->vid);
+  ]]);
+
+  /* Get EVPN type */
+  METHOD(T_NET, evpn_type, 0, [[
+    if (v1.val.net->type != NET_EVPN)
+      runtime( "EVPN address expected" );
+
+    const net_addr_evpn *evpn = (void *) v1.val.net;
+    RESULT(T_ENUM_EVPN_TYPE, i, evpn->subtype);
+  ]]);
+
+  /* Get EVPN tag */
+  METHOD(T_NET, evpn_tag, 0, [[
+    if (v1.val.net->type != NET_EVPN)
+      runtime( "EVPN address expected" );
+
+    const net_addr_evpn *evpn = (void *) v1.val.net;
+    if (evpn->subtype > NET_EVPN_IMET)
+      runtime( "EVPN EAD/MAC/IMET address expected" );
+
+    RESULT(T_INT, i, evpn->tag);
+  ]]);
+
+  /* Get EVPN ESI */
+  METHOD(T_NET, evpn_esi, 0, [[
+    if (v1.val.net->type != NET_EVPN)
+      runtime( "EVPN address expected" );
+
+    const net_addr_evpn *evpn = (void *) v1.val.net;
+    const evpn_esi *esi;
+
+    switch (evpn->subtype)
+    {
+    case NET_EVPN_EAD:
+      esi = &evpn->ead.esi;
+      break;
+
+    case NET_EVPN_ES:
+      esi = &evpn->es.esi;
+      break;
+
+    default:
+      runtime( "EVPN EAD/ES address expected" );
+    }
+
+    struct adata *bs;
+    bs = falloc(sizeof(struct adata) + sizeof(evpn_esi));
+    bs->length = sizeof(evpn_esi);
+    memcpy(bs->data, esi, sizeof(evpn_esi));
+
+    RESULT(T_BYTESTRING, bs, bs);
+  ]]);
+
+  /* Get EVPN outer IP */
+  METHOD(T_NET, router_ip, 0, [[
+    if (v1.val.net->type != NET_EVPN)
+      runtime( "EVPN address expected" );
+
+    const net_addr_evpn *evpn = (void *) v1.val.net;
+
+    switch (evpn->subtype)
+    {
+    case NET_EVPN_IMET:
+      RESULT(T_IP, ip, evpn->imet.rtr);
+      break;
+
+    case NET_EVPN_ES:
+      RESULT(T_IP, ip, evpn->es.rtr);
+      break;
+
+    default:
+      runtime( "EVPN IMET/ES address expected" );
+    }
+  ]]);
+
   /* Get first ASN from AS PATH */
   METHOD_R(T_PATH, first, T_INT, i, ({ u32 as = 0; 
as_path_get_first(v1.val.ad, &as); as; }));
 
diff --git a/filter/test.conf b/filter/test.conf
index 096798c5..1023f2b0 100644
--- a/filter/test.conf
+++ b/filter/test.conf
@@ -2031,6 +2031,107 @@ bt_test_suite(t_net_sadr, "Testing IPv6 SADR nets");
 
 
 
+/*
+ *     Testing Ethernet nets
+ *     ---------------------
+ */
+
+function t_net_eth()
+{
+       prefix p;
+
+       p = eth 12:3f:c9:48:9c:9b;
+       bt_assert(format(p) = "12:3f:c9:48:9c:9b");
+       bt_assert(p.type = NET_ETH);
+       bt_assert(p.mac = 12:3f:c9:48:9c:9b);
+       bt_assert(p.vlan_id = 0);
+
+       p = eth 16:36:f8:80:5b:48 vlan 100;
+       bt_assert(format(p) = "16:36:f8:80:5b:48 vlan 100");
+       bt_assert(p.type = NET_ETH);
+       bt_assert(p.mac = 16:36:f8:80:5b:48);
+       bt_assert(p.vlan_id = 100);
+}
+
+bt_test_suite(t_net_eth, "Testing Ethernet nets");
+
+
+
+
+/*
+ *     Testing EVPN nets
+ *     -----------------
+ */
+
+function t_net_evpn()
+{
+       prefix p;
+
+       p = evpn ead 10:1010 210 00:10:20:30:40:50:60:70:80:90;
+       bt_assert(format(p) = "evpn ead 10:1010 210 
00:10:20:30:40:50:60:70:80:90");
+       bt_assert(p.type = NET_EVPN);
+       bt_assert(p.evpn_type = NET_EVPN_EAD);
+       bt_assert(p.rd = 10:1010);
+       bt_assert(p.evpn_tag = 210);
+       bt_assert(p.evpn_esi = 00:10:20:30:40:50:60:70:80:90);
+
+       p = evpn mac 10:1020 220 12:a6:54:da:bc:bf *;
+       bt_assert(format(p) = "evpn mac 10:1020 220 12:a6:54:da:bc:bf *");
+       bt_assert(p.type = NET_EVPN);
+       bt_assert(p.evpn_type = NET_EVPN_MAC);
+       bt_assert(p.rd = 10:1020);
+       bt_assert(p.evpn_tag = 220);
+       bt_assert(p.mac = 12:a6:54:da:bc:bf);
+       bt_assert(p.ip = ::);
+
+       p = evpn mac 10:1020 224 16:c2:8d:50:86:c5 192.0.2.10;
+       bt_assert(format(p) = "evpn mac 10:1020 224 16:c2:8d:50:86:c5 
192.0.2.10");
+       bt_assert(p.type = NET_EVPN);
+       bt_assert(p.evpn_type = NET_EVPN_MAC);
+       bt_assert(p.rd = 10:1020);
+       bt_assert(p.evpn_tag = 224);
+       bt_assert(p.mac = 16:c2:8d:50:86:c5);
+       bt_assert(p.ip = 192.0.2.10);
+
+       p = evpn mac 10:1020 226 2a:3b:24:3d:f0:a0 2001:db8:10:20::1;
+       bt_assert(format(p) = "evpn mac 10:1020 226 2a:3b:24:3d:f0:a0 
2001:db8:10:20::1");
+       bt_assert(p.type = NET_EVPN);
+       bt_assert(p.evpn_type = NET_EVPN_MAC);
+       bt_assert(p.rd = 10:1020);
+       bt_assert(p.evpn_tag = 226);
+       bt_assert(p.mac = 2a:3b:24:3d:f0:a0);
+       bt_assert(p.ip = 2001:db8:10:20::1);
+
+       p = evpn imet 10:1030 234 192.0.2.20;
+       bt_assert(format(p) = "evpn imet 10:1030 234 192.0.2.20");
+       bt_assert(p.type = NET_EVPN);
+       bt_assert(p.evpn_type = NET_EVPN_IMET);
+       bt_assert(p.rd = 10:1030);
+       bt_assert(p.evpn_tag = 234);
+       bt_assert(p.router_ip = 192.0.2.20);
+
+       p = evpn imet 10:1030 236 2001:db8:10:20::2;
+       bt_assert(format(p) = "evpn imet 10:1030 236 2001:db8:10:20::2");
+       bt_assert(p.type = NET_EVPN);
+       bt_assert(p.evpn_type = NET_EVPN_IMET);
+       bt_assert(p.rd = 10:1030);
+       bt_assert(p.evpn_tag = 236);
+       bt_assert(p.router_ip = 2001:db8:10:20::2);
+
+       p = evpn es 10:1040 00:10:20:30:40:50:60:70:80:90 192.0.2.40;
+       bt_assert(format(p) = "evpn es 10:1040 00:10:20:30:40:50:60:70:80:90 
192.0.2.40");
+       bt_assert(p.type = NET_EVPN);
+       bt_assert(p.evpn_type = NET_EVPN_ES);
+       bt_assert(p.rd = 10:1040);
+       bt_assert(p.evpn_esi = 00:10:20:30:40:50:60:70:80:90);
+       bt_assert(p.router_ip = 192.0.2.40);
+}
+
+bt_test_suite(t_net_evpn, "Testing EVPN nets");
+
+
+
+
 /*
  *     Testing defined() function
  *     --------------------------
diff --git a/lib/Makefile b/lib/Makefile
index 812f721c..21aad378 100644
--- a/lib/Makefile
+++ b/lib/Makefile
@@ -1,4 +1,4 @@
-src := bitmap.c bitops.c blake2s.c blake2b.c checksum.c event.c flowspec.c 
idm.c ip.c lists.c mac.c md5.c mempool.c net.c patmatch.c printf.c resource.c 
sha1.c sha256.c sha512.c slab.c slists.c strtoul.c tbf.c timer.c xmalloc.c
+src := bitmap.c bitops.c blake2s.c blake2b.c checksum.c event.c evpn.c 
flowspec.c idm.c ip.c lists.c mac.c md5.c mempool.c net.c patmatch.c printf.c 
resource.c sha1.c sha256.c sha512.c slab.c slists.c strtoul.c tbf.c timer.c 
xmalloc.c
 obj := $(src-o-files)
 $(all-daemon)
 
diff --git a/lib/evpn.c b/lib/evpn.c
new file mode 100644
index 00000000..997c1f7e
--- /dev/null
+++ b/lib/evpn.c
@@ -0,0 +1,38 @@
+/*
+ *     BIRD Internet Routing Daemon -- EVPN Net Type
+ *
+ *     (c) 2023 Ondrej Zajicek <[email protected]>
+ *     (c) 2023 CZ.NIC z.s.p.o.
+ *
+ *     Can be freely distributed and used under the terms of the GNU GPL.
+ */
+
+#include "nest/bird.h"
+#include "lib/net.h"
+
+uint
+evpn_format(char *buf, uint blen, const net_addr_evpn *n)
+{
+  char rds[32];
+  rd_format(rd_from_u64(n->rd), rds, 32);
+
+  switch (n->subtype)
+  {
+  case NET_EVPN_EAD:
+    return bsnprintf(buf, blen, "evpn ead %s %u %10b", rds, n->tag, 
&n->ead.esi);
+
+  case NET_EVPN_MAC:
+    if (n->length < sizeof(net_addr_evpn_mac_ip))
+      return bsnprintf(buf, blen, "evpn mac %s %u %6b *", rds, n->tag, 
&n->mac.mac);
+    else
+      return bsnprintf(buf, blen, "evpn mac %s %u %6b %I", rds, n->tag, 
&n->mac_ip.mac, n->mac_ip.ip);
+
+  case NET_EVPN_IMET:
+    return bsnprintf(buf, blen, "evpn imet %s %u %I", rds, n->tag, 
n->imet.rtr);
+
+  case NET_EVPN_ES:
+    return bsnprintf(buf, blen, "evpn es %s %10b %I", rds, &n->es.esi, 
n->es.rtr);
+  }
+
+  bug("unknown EVPN type %d", n->subtype);
+}
diff --git a/lib/evpn.h b/lib/evpn.h
new file mode 100644
index 00000000..c340c385
--- /dev/null
+++ b/lib/evpn.h
@@ -0,0 +1,50 @@
+/*
+ *     BIRD Internet Routing Daemon -- EVPN Net Type
+ *
+ *     (c) 2023 Ondrej Zajicek <[email protected]>
+ *     (c) 2023 CZ.NIC z.s.p.o.
+ *
+ *     Can be freely distributed and used under the terms of the GNU GPL.
+ */
+
+#ifndef _BIRD_EVPN_NET_H_
+#define _BIRD_EVPN_NET_H_
+
+enum evpn_net_type {
+  NET_EVPN_EAD         =  1,
+  NET_EVPN_MAC         =  2,
+  NET_EVPN_IMET                =  3,
+  NET_EVPN_ES          =  4,
+  NET_EVPN_MAX
+};
+
+enum evpn_esi_type {
+  EVPN_ESI_MANUAL      = 0,
+  EVPN_ESI_LACP                = 1,
+  EVPN_ESI_MAX
+};
+
+typedef struct evpn_esi {
+  u8 type;
+  u8 value[9];
+} evpn_esi;
+
+typedef struct mac_addr {
+  u8 addr[6];
+} mac_addr;
+
+#define MAC_NONE ((mac_addr){ })
+
+static inline int mac_zero(mac_addr a)
+{ return !memcmp(&a, &MAC_NONE, sizeof(mac_addr)); }
+
+static inline int mac_nonzero(mac_addr a)
+{ return !mac_zero(a); }
+
+static inline int mac_compare(mac_addr a, mac_addr b)
+{ return memcmp(&a, &b, sizeof(mac_addr)); }
+
+union net_addr_evpn;
+uint evpn_format(char *buf, uint blen, const union net_addr_evpn *n);
+
+#endif
diff --git a/lib/net.c b/lib/net.c
index 64cf9e04..43e6cb15 100644
--- a/lib/net.c
+++ b/lib/net.c
@@ -15,8 +15,10 @@ const char * const net_label[] = {
   [NET_FLOW4]  = "flow4",
   [NET_FLOW6]  = "flow6",
   [NET_IP6_SADR]= "ipv6-sadr",
+  [NET_ETH]    = "eth",
   [NET_MPLS]   = "mpls",
   [NET_ASPA]   = "aspa",
+  [NET_EVPN]   = "evpn",
 };
 
 const u16 net_addr_length[] = {
@@ -29,8 +31,10 @@ const u16 net_addr_length[] = {
   [NET_FLOW4]  = 0,
   [NET_FLOW6]  = 0,
   [NET_IP6_SADR]= sizeof(net_addr_ip6_sadr),
+  [NET_ETH]    = sizeof(net_addr_eth),
   [NET_MPLS]   = sizeof(net_addr_mpls),
   [NET_ASPA]   = sizeof(net_addr_aspa),
+  [NET_EVPN]   = 0,
 };
 
 const u8 net_max_prefix_length[] = {
@@ -43,8 +47,10 @@ const u8 net_max_prefix_length[] = {
   [NET_FLOW4]  = IP4_MAX_PREFIX_LENGTH,
   [NET_FLOW6]  = IP6_MAX_PREFIX_LENGTH,
   [NET_IP6_SADR]= IP6_MAX_PREFIX_LENGTH,
+  [NET_ETH]    = 0,
   [NET_MPLS]   = 0,
   [NET_ASPA]   = 0,
+  [NET_EVPN]   = 0,
 };
 
 const u16 net_max_text_length[] = {
@@ -57,8 +63,10 @@ const u16 net_max_text_length[] = {
   [NET_FLOW4]  = 0,    /* "flow4 { ... }" */
   [NET_FLOW6]  = 0,    /* "flow6 { ... }" */
   [NET_IP6_SADR]= 92,  /* "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff/128 from 
ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff/128" */
+  [NET_ETH]    = 28,   /* "11:22:33:44:55:66 vlan 65535" */
   [NET_MPLS]   = 7,    /* "1048575" */
   [NET_ASPA]   = 10,   /* "4294967295" */
+  [NET_EVPN]   = 0,
 };
 
 /* There should be no implicit padding in net_addr structures */
@@ -72,6 +80,7 @@ STATIC_ASSERT(sizeof(net_addr_roa6)   == 28);
 STATIC_ASSERT(sizeof(net_addr_flow4)   ==  8);
 STATIC_ASSERT(sizeof(net_addr_flow6)   == 20);
 STATIC_ASSERT(sizeof(net_addr_ip6_sadr)        == 40);
+STATIC_ASSERT(sizeof(net_addr_eth)     == 12);
 STATIC_ASSERT(sizeof(net_addr_mpls)    ==  8);
 STATIC_ASSERT(sizeof(net_addr_aspa)    ==  8);
 
@@ -90,6 +99,13 @@ STATIC_ASSERT(alignof(net_addr_ip6_sadr) == 
alignof(net_addr));
 STATIC_ASSERT(alignof(net_addr_mpls)   == alignof(net_addr));
 STATIC_ASSERT(alignof(net_addr_aspa)   == alignof(net_addr));
 
+STATIC_ASSERT(sizeof(net_addr_evpn_ead)                == 32);
+STATIC_ASSERT(sizeof(net_addr_evpn_mac)                == 24);
+STATIC_ASSERT(sizeof(net_addr_evpn_mac_ip)     == 40);
+STATIC_ASSERT(sizeof(net_addr_evpn_imet)       == 32);
+STATIC_ASSERT(sizeof(net_addr_evpn_es)         == 48);
+STATIC_ASSERT(sizeof(net_addr_evpn)            == 48);
+
 
 int
 rd_format(const vpn_rd rd_, char *buf, int buflen)
@@ -143,10 +159,22 @@ net_format(const net_addr *N, char *buf, int buflen)
     return flow6_net_format(buf, buflen, &n->flow6);
   case NET_IP6_SADR:
     return bsnprintf(buf, buflen, "%I6/%d from %I6/%d", 
n->ip6_sadr.dst_prefix, n->ip6_sadr.dst_pxlen, n->ip6_sadr.src_prefix, 
n->ip6_sadr.src_pxlen);
+  case NET_ETH:
+  {
+    int c = bsnprintf(buf, buflen, "%6b", &n->eth.mac);
+    ADVANCE(buf, buflen, c);
+
+    if (n->eth.vid)
+      c += bsnprintf(buf, buflen, " vlan %d", (int) n->eth.vid);
+
+    return c;
+  }
   case NET_MPLS:
     return bsnprintf(buf, buflen, "%u", n->mpls.label);
   case NET_ASPA:
     return bsnprintf(buf, buflen, "%u", n->aspa.asn);
+  case NET_EVPN:
+    return evpn_format(buf, buflen, &n->evpn);;
   }
 
   bug("unknown network type");
@@ -170,8 +198,10 @@ net_pxmask(const net_addr *a)
   case NET_IP6_SADR:
     return ipa_from_ip6(ip6_mkmask(net6_pxlen(a)));
 
+  case NET_ETH:
   case NET_MPLS:
   case NET_ASPA:
+  case NET_EVPN:
   default:
     return IPA_NONE;
   }
@@ -203,10 +233,14 @@ net_compare(const net_addr *a, const net_addr *b)
     return net_compare_flow6((const net_addr_flow6 *) a, (const net_addr_flow6 
*) b);
   case NET_IP6_SADR:
     return net_compare_ip6_sadr((const net_addr_ip6_sadr *) a, (const 
net_addr_ip6_sadr *) b);
+  case NET_ETH:
+    return net_compare_eth((const net_addr_eth *) a, (const net_addr_eth *) b);
   case NET_MPLS:
     return net_compare_mpls((const net_addr_mpls *) a, (const net_addr_mpls *) 
b);
   case NET_ASPA:
     return net_compare_aspa((const net_addr_aspa *) a, (const net_addr_aspa *) 
b);
+  case NET_EVPN:
+    return net_compare_evpn((const net_addr_evpn *) a, (const net_addr_evpn *) 
b);
   }
   return 0;
 }
@@ -227,8 +261,10 @@ net_hash(const net_addr *n)
   case NET_FLOW4: return NET_HASH(n, flow4);
   case NET_FLOW6: return NET_HASH(n, flow6);
   case NET_IP6_SADR: return NET_HASH(n, ip6_sadr);
+  case NET_ETH: return NET_HASH(n, eth);
   case NET_MPLS: return NET_HASH(n, mpls);
   case NET_ASPA: return NET_HASH(n, aspa);
+  case NET_EVPN: return NET_HASH(n, evpn);
   default: bug("invalid type");
   }
 }
@@ -250,8 +286,10 @@ net_validate(const net_addr *n)
   case NET_FLOW4: return NET_VALIDATE(n, flow4);
   case NET_FLOW6: return NET_VALIDATE(n, flow6);
   case NET_IP6_SADR: return NET_VALIDATE(n, ip6_sadr);
+  case NET_ETH: return NET_VALIDATE(n, eth);
   case NET_MPLS: return NET_VALIDATE(n, mpls);
   case NET_ASPA: return NET_VALIDATE(n, aspa);
+  case NET_EVPN: return NET_VALIDATE(n, evpn);
   default: return 0;
   }
 }
@@ -278,8 +316,10 @@ net_normalize(net_addr *N)
   case NET_IP6_SADR:
     return net_normalize_ip6_sadr(&n->ip6_sadr);
 
+  case NET_ETH:
   case NET_MPLS:
   case NET_ASPA:
+  case NET_EVPN:
     return;
   }
 }
@@ -306,8 +346,10 @@ net_classify(const net_addr *N)
   case NET_IP6_SADR:
     return ip6_zero(n->ip6_sadr.dst_prefix) ? (IADDR_HOST | SCOPE_UNIVERSE) : 
ip6_classify(&n->ip6_sadr.dst_prefix);
 
+  case NET_ETH:
   case NET_MPLS:
   case NET_ASPA:
+  case NET_EVPN:       /* ?? */
     return IADDR_HOST | SCOPE_UNIVERSE;
   }
 
@@ -340,8 +382,10 @@ ipa_in_netX(const ip_addr a, const net_addr *n)
     return ip6_zero(ip6_and(ip6_xor(ipa_to_ip6(a), net6_prefix(n)),
                            ip6_mkmask(net6_pxlen(n))));
 
+  case NET_ETH:
   case NET_MPLS:
   case NET_ASPA:
+  case NET_EVPN:
   default:
     return 0;
   }
diff --git a/lib/net.h b/lib/net.h
index a1fd0291..8d6f58bf 100644
--- a/lib/net.h
+++ b/lib/net.h
@@ -11,6 +11,8 @@
 #define _BIRD_NET_H_
 
 #include "lib/ip.h"
+#include "lib/evpn.h"
+#include "lib/hash.h"
 
 
 #define NET_IP4                1
@@ -22,9 +24,11 @@
 #define NET_FLOW4      7
 #define NET_FLOW6      8
 #define NET_IP6_SADR   9
-#define NET_MPLS       10
-#define NET_ASPA       11
-#define NET_MAX                12
+#define NET_ETH                10
+#define NET_MPLS       11
+#define NET_ASPA       12
+#define NET_EVPN       13
+#define NET_MAX                14
 
 #define NB_IP4         (1 << NET_IP4)
 #define NB_IP6         (1 << NET_IP6)
@@ -35,14 +39,16 @@
 #define NB_FLOW4       (1 << NET_FLOW4)
 #define NB_FLOW6       (1 << NET_FLOW6)
 #define NB_IP6_SADR    (1 << NET_IP6_SADR)
+#define NB_ETH         (1 << NET_ETH)
 #define NB_MPLS                (1 << NET_MPLS)
 #define NB_ASPA                (1 << NET_ASPA)
+#define NB_EVPN                (1 << NET_EVPN)
 
 #define NB_IP          (NB_IP4 | NB_IP6)
 #define NB_VPN         (NB_VPN4 | NB_VPN6)
 #define NB_ROA         (NB_ROA4 | NB_ROA6)
 #define NB_FLOW                (NB_FLOW4 | NB_FLOW6)
-#define NB_DEST                (NB_IP | NB_IP6_SADR | NB_VPN | NB_MPLS)
+#define NB_DEST                (NB_IP | NB_IP6_SADR | NB_VPN | NB_ETH | 
NB_MPLS | NB_EVPN)
 #define NB_ANY         0xffffffff
 
 
@@ -119,6 +125,14 @@ typedef struct net_addr_flow6 {
   byte data[0];
 } net_addr_flow6;
 
+typedef struct net_addr_eth {
+  u8 type;
+  u8 pxlen;
+  u16 length;
+  mac_addr mac;
+  u16 vid;
+} net_addr_eth;
+
 typedef struct net_addr_mpls {
   u8 type;
   u8 pxlen;
@@ -142,6 +156,78 @@ typedef struct net_addr_ip6_sadr {
   ip6_addr src_prefix;
 } net_addr_ip6_sadr;
 
+typedef struct net_addr_evpn_ead {
+  u8 type;
+  u8 subtype;
+  u16 length;
+  u32 tag;
+  u64 rd;
+
+  evpn_esi esi;
+  u8 padding[6];
+} net_addr_evpn_ead;
+
+typedef struct net_addr_evpn_mac {
+  u8 type;
+  u8 subtype;
+  u16 length;
+  u32 tag;
+  u64 rd;
+
+  mac_addr mac;
+  u16 padding;
+} net_addr_evpn_mac;
+
+typedef struct net_addr_evpn_mac_ip {
+  u8 type;
+  u8 subtype;
+  u16 length;
+  u32 tag;
+  u64 rd;
+
+  mac_addr mac;
+  u16 padding0;
+  ip_addr ip;
+} net_addr_evpn_mac_ip;
+
+typedef struct net_addr_evpn_imet {
+  u8 type;
+  u8 subtype;
+  u16 length;
+  u32 tag;
+  u64 rd;
+
+  ip_addr rtr;
+} net_addr_evpn_imet;
+
+typedef struct net_addr_evpn_es {
+  u8 type;
+  u8 subtype;
+  u16 length;
+  u32 tag;     /* unused */
+  u64 rd;
+
+  evpn_esi esi;
+  u8 padding[6];
+  ip_addr rtr;
+} net_addr_evpn_es;
+
+typedef union net_addr_evpn {
+  struct {
+    u8 type;
+    u8 subtype;
+    u16 length;
+    u32 tag;
+    u64 rd;
+    byte data[0];
+  };
+  net_addr_evpn_ead ead;
+  net_addr_evpn_mac mac;
+  net_addr_evpn_mac_ip mac_ip;
+  net_addr_evpn_imet imet;
+  net_addr_evpn_es es;
+} net_addr_evpn;
+
 typedef union net_addr_union {
   net_addr n;
   net_addr_ip4 ip4;
@@ -153,8 +239,10 @@ typedef union net_addr_union {
   net_addr_flow4 flow4;
   net_addr_flow6 flow6;
   net_addr_ip6_sadr ip6_sadr;
+  net_addr_eth eth;
   net_addr_mpls mpls;
   net_addr_aspa aspa;
+  net_addr_evpn evpn;
 } net_addr_union;
 
 
@@ -193,6 +281,9 @@ extern const u16 net_max_text_length[];
 #define NET_ADDR_IP6_SADR(dst_prefix,dst_pxlen,src_prefix,src_pxlen) \
   ((net_addr_ip6_sadr) { NET_IP6_SADR, dst_pxlen, sizeof(net_addr_ip6_sadr), 
dst_prefix, src_pxlen, src_prefix })
 
+#define NET_ADDR_ETH(mac, vid) \
+  ((net_addr_eth) { NET_ETH, 48, sizeof(net_addr_eth), mac, vid })
+
 #define NET_ADDR_ASPA(asn) \
   ((net_addr_aspa) { NET_ASPA, 32, sizeof(net_addr_aspa), asn })
 
@@ -200,6 +291,22 @@ extern const u16 net_max_text_length[];
   ((net_addr_mpls) { NET_MPLS, 20, sizeof(net_addr_mpls), label })
 
 
+#define NET_ADDR_EVPN_EAD(rd, tag, esi) \
+  ((net_addr_evpn_ead) { NET_EVPN, NET_EVPN_EAD, sizeof(net_addr_evpn_ead), 
tag, rd, .esi = esi })
+
+#define NET_ADDR_EVPN_MAC(rd, tag, mac) \
+  ((net_addr_evpn_mac) { NET_EVPN, NET_EVPN_MAC, sizeof(net_addr_evpn_mac), 
tag, rd, .mac = mac })
+
+#define NET_ADDR_EVPN_MAC_IP(rd, tag, mac, ip) \
+  ((net_addr_evpn_mac_ip) { NET_EVPN, NET_EVPN_MAC, 
sizeof(net_addr_evpn_mac_ip), tag, rd, .mac = mac, .ip = ip })
+
+#define NET_ADDR_EVPN_IMET(rd, tag, rtr) \
+  ((net_addr_evpn_imet) { NET_EVPN, NET_EVPN_IMET, sizeof(net_addr_evpn_imet), 
tag, rd, .rtr = rtr })
+
+#define NET_ADDR_EVPN_ES(rd, esi, rtr) \
+  ((net_addr_evpn_es) { NET_EVPN, NET_EVPN_ES, sizeof(net_addr_evpn_es), 0, 
rd, .esi = esi, .rtr = rtr })
+
+
 static inline void net_fill_ip4(net_addr *a, ip4_addr prefix, uint pxlen)
 { *(net_addr_ip4 *)a = NET_ADDR_IP4(prefix, pxlen); }
 
@@ -221,12 +328,30 @@ static inline void net_fill_roa6(net_addr *a, ip6_addr 
prefix, uint pxlen, uint
 static inline void net_fill_ip6_sadr(net_addr *a, ip6_addr dst_prefix, uint 
dst_pxlen, ip6_addr src_prefix, uint src_pxlen)
 { *(net_addr_ip6_sadr *)a = NET_ADDR_IP6_SADR(dst_prefix, dst_pxlen, 
src_prefix, src_pxlen); }
 
+static inline void net_fill_eth(net_addr *a, mac_addr mac, u16 vid)
+{ *(net_addr_eth *)a = NET_ADDR_ETH(mac, vid); }
+
 static inline void net_fill_mpls(net_addr *a, u32 label)
 { *(net_addr_mpls *)a = NET_ADDR_MPLS(label); }
 
 static inline void net_fill_aspa(net_addr *a, u32 asn)
 { *(net_addr_aspa *)a = NET_ADDR_ASPA(asn); }
 
+static inline void net_fill_evpn_ead(net_addr *a, u64 rd, u32 tag, evpn_esi 
esi)
+{ *(net_addr_evpn_ead *)a = NET_ADDR_EVPN_EAD(rd, tag, esi); }
+
+static inline void net_fill_evpn_mac(net_addr *a, u64 rd, u32 tag, mac_addr 
mac)
+{ *(net_addr_evpn_mac *)a = NET_ADDR_EVPN_MAC(rd, tag, mac); }
+
+static inline void net_fill_evpn_mac_ip(net_addr *a, u64 rd, u32 tag, mac_addr 
mac, ip_addr ip)
+{ *(net_addr_evpn_mac_ip *)a = NET_ADDR_EVPN_MAC_IP(rd, tag, mac, ip); }
+
+static inline void net_fill_evpn_imet(net_addr *a, u64 rd, u32 tag, ip_addr 
rtr)
+{ *(net_addr_evpn_imet *)a = NET_ADDR_EVPN_IMET(rd, tag, rtr); }
+
+static inline void net_fill_evpn_es(net_addr *a, u64 rd, evpn_esi esi, ip_addr 
rtr)
+{ *(net_addr_evpn_es *)a = NET_ADDR_EVPN_ES(rd, esi, rtr); }
+
 static inline void net_fill_ipa(net_addr *a, ip_addr prefix, uint pxlen)
 {
   if (ipa_is_ip4(prefix))
@@ -314,19 +439,25 @@ static inline ip_addr net_prefix(const net_addr *a)
   case NET_IP6_SADR:
     return ipa_from_ip6(net6_prefix(a));
 
+  case NET_ETH:
   case NET_MPLS:
   case NET_ASPA:
+  case NET_EVPN:
   default:
     return IPA_NONE;
   }
 }
 
-static inline u32 net_mpls(const net_addr *a)
+static inline mac_addr net_mac_addr(const net_addr *a)
 {
-  if (a->type == NET_MPLS)
-    return ((net_addr_mpls *) a)->label;
+  ASSERT_DIE(a->type == NET_ETH);
+  return ((net_addr_eth *) a)->mac;
+}
 
-  bug("Can't call net_mpls on non-mpls net_addr");
+static inline u32 net_mpls(const net_addr *a)
+{
+  ASSERT_DIE(a->type == NET_MPLS);
+  return ((net_addr_mpls *) a)->label;
 }
 
 static inline uint net4_pxlen(const net_addr *a)
@@ -348,6 +479,8 @@ static inline vpn_rd net_rd(const net_addr *a)
     return ((net_addr_vpn4 *)a)->rd;
   case NET_VPN6:
     return ((net_addr_vpn6 *)a)->rd;
+  case NET_EVPN:
+    return rd_from_u64(((net_addr_evpn *)a)->rd);
   }
   return RD_NONE;
 }
@@ -383,12 +516,18 @@ static inline int net_equal_flow6(const net_addr_flow6 
*a, const net_addr_flow6
 static inline int net_equal_ip6_sadr(const net_addr_ip6_sadr *a, const 
net_addr_ip6_sadr *b)
 { return !memcmp(a, b, sizeof(net_addr_ip6_sadr)); }
 
+static inline int net_equal_eth(const net_addr_eth *a, const net_addr_eth *b)
+{ return !memcmp(a, b, sizeof(net_addr_eth)); }
+
 static inline int net_equal_mpls(const net_addr_mpls *a, const net_addr_mpls 
*b)
 { return !memcmp(a, b, sizeof(net_addr_mpls)); }
 
 static inline int net_equal_aspa(const net_addr_aspa *a, const net_addr_aspa 
*b)
 { return !memcmp(a, b, sizeof(net_addr_aspa)); }
 
+static inline int net_equal_evpn(const net_addr_evpn *a, const net_addr_evpn 
*b)
+{ return net_equal((const net_addr *) a, (const net_addr *) b); }
+
 
 static inline int net_equal_prefix_roa4(const net_addr_roa4 *a, const 
net_addr_roa4 *b)
 { return ip4_equal(a->prefix, b->prefix) && (a->pxlen == b->pxlen); }
@@ -427,6 +566,9 @@ static inline int net_zero_flow4(const net_addr_flow4 *a)
 static inline int net_zero_flow6(const net_addr_flow6 *a)
 { return !a->pxlen && ip6_zero(a->prefix) && (a->length == 
sizeof(net_addr_flow6)); }
 
+static inline int net_zero_eth(const net_addr_eth *a)
+{ return mac_zero(a->mac) && !a->vid; }
+
 static inline int net_zero_mpls(const net_addr_mpls *a)
 { return !a->label; }
 
@@ -465,12 +607,22 @@ static inline int net_compare_ip6_sadr(const 
net_addr_ip6_sadr *a, const net_add
     ip6_compare(a->src_prefix, b->src_prefix) ?: uint_cmp(a->src_pxlen, 
b->src_pxlen);
 }
 
+static inline int net_compare_eth(const net_addr_eth *a, const net_addr_eth *b)
+{ return uint_cmp(a->vid, b->vid) ?: mac_compare(a->mac, b->mac); }
+
 static inline int net_compare_mpls(const net_addr_mpls *a, const net_addr_mpls 
*b)
 { return uint_cmp(a->label, b->label); }
 
 static inline int net_compare_aspa(const net_addr_aspa *a, const net_addr_aspa 
*b)
 { return uint_cmp(a->asn, b->asn); }
 
+static inline int net_compare_evpn(const net_addr_evpn *a, const net_addr_evpn 
*b)
+{
+  return
+    uint_cmp(a->subtype, b->subtype) ?: u64_cmp(a->rd, b->rd) ?: 
uint_cmp(a->tag, b->tag) ?:
+    uint_cmp(a->length, b->length) ?: memcmp(a->data, b->data, a->length - 
OFFSETOF(net_addr_evpn, data));
+}
+
 int net_compare(const net_addr *a, const net_addr *b);
 
 
@@ -504,12 +656,18 @@ static inline void net_copy_flow6(net_addr_flow6 *dst, 
const net_addr_flow6 *src
 static inline void net_copy_ip6_sadr(net_addr_ip6_sadr *dst, const 
net_addr_ip6_sadr *src)
 { memcpy(dst, src, sizeof(net_addr_ip6_sadr)); }
 
+static inline void net_copy_eth(net_addr_eth *dst, const net_addr_eth *src)
+{ memcpy(dst, src, sizeof(net_addr_eth)); }
+
 static inline void net_copy_mpls(net_addr_mpls *dst, const net_addr_mpls *src)
 { memcpy(dst, src, sizeof(net_addr_mpls)); }
 
 static inline void net_copy_aspa(net_addr_aspa *dst, const net_addr_aspa *src)
 { memcpy(dst, src, sizeof(net_addr_aspa)); }
 
+static inline void net_copy_evpn(net_addr_evpn *dst, const net_addr_evpn *src)
+{ memcpy(dst, src, src->length); }
+
 
 static inline u32 px4_hash(ip4_addr prefix, u32 pxlen)
 { return ip4_hash(prefix) ^ (pxlen << 26); }
@@ -550,12 +708,18 @@ static inline u32 net_hash_flow6(const net_addr_flow6 *n)
 static inline u32 net_hash_ip6_sadr(const net_addr_ip6_sadr *n)
 { return px6_hash(n->dst_prefix, n->dst_pxlen); }
 
+static inline u32 net_hash_eth(const net_addr_eth *n)
+{ u64 x = 0; memcpy(&x, (char *) n + OFFSETOF(net_addr_eth, mac), 8); return 
u64_hash(x); }
+
 static inline u32 net_hash_mpls(const net_addr_mpls *n)
 { return u32_hash(n->label); }
 
 static inline u32 net_hash_aspa(const net_addr_aspa *n)
 { return u32_hash(n->asn); }
 
+static inline u32 net_hash_evpn(const net_addr_evpn *n)
+{ return mem_hash(&n->tag, n->length - OFFSETOF(net_addr_evpn, tag)); }
+
 u32 net_hash(const net_addr *a);
 
 
@@ -602,12 +766,18 @@ static inline int net_validate_flow4(const net_addr_flow4 
*n)
 static inline int net_validate_flow6(const net_addr_flow6 *n)
 { return net_validate_px6(n->prefix, n->pxlen); }
 
+static inline int net_validate_eth(const net_addr_eth *n)
+{ return n->vid < (1 << 12); }
+
 static inline int net_validate_mpls(const net_addr_mpls *n)
 { return n->label < (1 << 20); }
 
 static inline int net_validate_aspa(const net_addr_aspa *n)
 { return n->asn > 0; }
 
+static inline int net_validate_evpn(const net_addr_evpn *n UNUSED)
+{ return 1; /* XXX */ }
+
 static inline int net_validate_ip6_sadr(const net_addr_ip6_sadr *n)
 { return net_validate_px6(n->dst_prefix, n->dst_pxlen) && 
net_validate_px6(n->src_prefix, n->src_pxlen); }
 
diff --git a/lib/printf.c b/lib/printf.c
index 0d2f95e8..01cf20fe 100644
--- a/lib/printf.c
+++ b/lib/printf.c
@@ -230,6 +230,24 @@ int bvsnprintf(char *buf, int size, const char *fmt, 
va_list args)
                if (field_width > size)
                        return -1;
                switch (*fmt) {
+               case 'b': {
+                       const char *digits="0123456789abcdef";
+                       const byte *bs = va_arg(args, const byte *);
+                       len = field_width;
+
+                       if (3*len > size)
+                               return -1;
+
+                       for (i = 0; i < len; i++) {
+                               const byte b = *bs++;
+                               *str++ = digits[b >> 4];
+                               *str++ = digits[b & 0xf];
+                               *str++ = ':';
+                       }
+
+                       str -= !!i;
+                       continue;
+               }
                case 'c':
                        if (!(flags & LEFT))
                                while (--field_width > 0)
diff --git a/nest/config.Y b/nest/config.Y
index 4a3b45b5..991a4377 100644
--- a/nest/config.Y
+++ b/nest/config.Y
@@ -115,7 +115,7 @@ CF_DECLS
 
 CF_KEYWORDS(ROUTER, ID, HOSTNAME, PROTOCOL, TEMPLATE, PREFERENCE, DISABLED, 
DEBUG, ALL, OFF, DIRECT)
 CF_KEYWORDS(INTERFACE, IMPORT, EXPORT, FILTER, NONE, VRF, DEFAULT, TABLE, 
TABLES, STATES, ROUTES, FILTERS)
-CF_KEYWORDS(IPV4, IPV6, VPN4, VPN6, ROA4, ROA6, FLOW4, FLOW6, SADR, MPLS, ASPA)
+CF_KEYWORDS(IPV4, IPV6, VPN4, VPN6, ROA4, ROA6, FLOW4, FLOW6, SADR, ETH, MPLS, 
ASPA)
 CF_KEYWORDS(RECEIVE, LIMIT, ACTION, WARN, BLOCK, RESTART, DISABLE, KEEP, 
FILTERED, RPKI)
 CF_KEYWORDS(PASSWORD, KEY, FROM, PASSIVE, TO, ID, EVENTS, PACKETS, PROTOCOLS, 
CHANNELS, INTERFACES)
 CF_KEYWORDS(ALGORITHM, KEYED, HMAC, MD5, SHA1, SHA256, SHA384, SHA512, 
BLAKE2S128, BLAKE2S256, BLAKE2B256, BLAKE2B512)
@@ -132,7 +132,7 @@ CF_KEYWORDS(ASPA_PROVIDERS)
 /* For r_args_channel */
 CF_KEYWORDS(IPV4, IPV4_MC, IPV4_MPLS, IPV6, IPV6_MC, IPV6_MPLS, IPV6_SADR, 
VPN4, VPN4_MC, VPN4_MPLS, VPN6, VPN6_MC, VPN6_MPLS, ROA4, ROA6, FLOW4, FLOW6, 
MPLS, PRI, SEC, ASPA)
 
-CF_ENUM(T_ENUM_NET_TYPE, NET_, IP4, IP6, VPN4, VPN6, ROA4, ROA6, FLOW4, FLOW6, 
IP6_SADR, MPLS, ASPA)
+CF_ENUM(T_ENUM_NET_TYPE, NET_, IP4, IP6, VPN4, VPN6, ROA4, ROA6, FLOW4, FLOW6, 
IP6_SADR, ETH, MPLS, ASPA, EVPN)
 CF_ENUM(T_ENUM_RTS, RTS_, STATIC, INHERIT, DEVICE, STATIC_DEVICE, REDIRECT,
        RIP, OSPF, OSPF_IA, OSPF_EXT1, OSPF_EXT2, BGP, PIPE, BABEL, RPKI, L3VPN,
        AGGREGATED)
@@ -142,6 +142,7 @@ CF_ENUM(T_ENUM_ROA, ROA_, UNKNOWN, VALID, INVALID)
 CF_ENUM(T_ENUM_ASPA, ASPA_, UNKNOWN, VALID, INVALID)
 CF_ENUM_PX(T_ENUM_AF, AF_, AFI_, IPV4, IPV6)
 CF_ENUM(T_ENUM_MPLS_POLICY, MPLS_POLICY_, NONE, STATIC, PREFIX, AGGREGATE, VRF)
+CF_ENUM(T_ENUM_EVPN_TYPE, NET_EVPN_, EAD, MAC, IMET, ES)
 
 %type <f> imexport
 %type <r> rtable
@@ -191,7 +192,9 @@ net_type_base:
  | ROA6 { $$ = NET_ROA6; }
  | FLOW4{ $$ = NET_FLOW4; }
  | FLOW6{ $$ = NET_FLOW6; }
+ | ETH  { $$ = NET_ETH; }
  | ASPA { $$ = NET_ASPA; }
+ | EVPN { $$ = NET_EVPN; }
  ;
 
 net_type:
diff --git a/nest/protocol.h b/nest/protocol.h
index 97a2ba40..c2e7f02c 100644
--- a/nest/protocol.h
+++ b/nest/protocol.h
@@ -45,8 +45,10 @@ enum protocol_class {
   PROTOCOL_BFD,
   PROTOCOL_BGP,
   PROTOCOL_BMP,
+  PROTOCOL_BRIDGE,
   PROTOCOL_DEVICE,
   PROTOCOL_DIRECT,
+  PROTOCOL_EVPN,
   PROTOCOL_KERNEL,
   PROTOCOL_L3VPN,
   PROTOCOL_OSPF,
@@ -105,8 +107,8 @@ 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_device, proto_radv, proto_rip, proto_static, proto_mrt, proto_evpn,
+  proto_ospf, proto_perf, proto_l3vpn, proto_aggregator, proto_bridge,
   proto_pipe, proto_bgp, proto_bmp, proto_bfd, proto_babel, proto_rpki;
 
 /*
diff --git a/nest/route.h b/nest/route.h
index 5a9e7fa1..80c558b8 100644
--- a/nest/route.h
+++ b/nest/route.h
@@ -484,7 +484,9 @@ 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_EVPN 19
+#define RTS_MAX 20
 
 #define RTD_NONE 0                     /* Undefined next hop */
 #define RTD_UNICAST 1                  /* Next hop is neighbor router */
diff --git a/nest/rt-attr.c b/nest/rt-attr.c
index e10e1ecb..6aa74d3d 100644
--- a/nest/rt-attr.c
+++ b/nest/rt-attr.c
@@ -78,6 +78,8 @@ const char * const rta_src_names[RTS_MAX] = {
   [RTS_PERF]           = "Perf",
   [RTS_L3VPN]          = "L3VPN",
   [RTS_AGGREGATED]     = "aggregated",
+  [RTS_BRIDGE]         = "bridge",
+  [RTS_EVPN]           = "EVPN",
 };
 
 const char * rta_dest_names[RTD_MAX] = {
@@ -1324,7 +1326,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", "RTS_EVPN" };
   static char *rtd[] = { "", " DEV", " HOLE", " UNREACH", " PROHIBIT" };
 
   RDUMP("pref=%d uc=%d %s %s%s h=%04x",
diff --git a/nest/rt-fib.c b/nest/rt-fib.c
index 688d0b96..1eb5cc7f 100644
--- a/nest/rt-fib.c
+++ b/nest/rt-fib.c
@@ -278,8 +278,10 @@ fib_find(struct fib *f, const net_addr *a)
   case NET_FLOW4: return FIB_FIND(f, a, flow4);
   case NET_FLOW6: return FIB_FIND(f, a, flow6);
   case NET_IP6_SADR: return FIB_FIND(f, a, ip6_sadr);
+  case NET_ETH: return FIB_FIND(f, a, eth);
   case NET_MPLS: return FIB_FIND(f, a, mpls);
   case NET_ASPA: return FIB_FIND(f, a, aspa);
+  case NET_EVPN: return FIB_FIND(f, a, evpn);
   default: bug("invalid type");
   }
 }
@@ -300,8 +302,10 @@ fib_insert(struct fib *f, const net_addr *a, struct 
fib_node *e)
   case NET_FLOW4: FIB_INSERT(f, a, e, flow4); return;
   case NET_FLOW6: FIB_INSERT(f, a, e, flow6); return;
   case NET_IP6_SADR: FIB_INSERT(f, a, e, ip6_sadr); return;
+  case NET_ETH: FIB_INSERT(f, a, e, eth); return;
   case NET_MPLS: FIB_INSERT(f, a, e, mpls); return;
   case NET_ASPA: FIB_INSERT(f, a, e, aspa); return;
+  case NET_EVPN: FIB_INSERT(f, a, e, evpn); return;
   default: bug("invalid type");
   }
 }
diff --git a/nest/rt-table.c b/nest/rt-table.c
index ed364d35..239ce764 100644
--- a/nest/rt-table.c
+++ b/nest/rt-table.c
@@ -1103,6 +1103,10 @@ rte_validate(rte *e)
     if (net_is_flow(n->n.addr) && (e->attrs->dest == RTD_UNREACHABLE))
       return 1;
 
+    /* XXX hack */
+    if (n->n.addr->type == NET_EVPN)
+      return 1;
+
     log(L_WARN "Ignoring route %N with invalid dest %d received via %s",
        n->n.addr, e->attrs->dest, e->sender->proto->name);
     return 0;
diff --git a/proto/aggregator/aggregator.c b/proto/aggregator/aggregator.c
index e5c2a176..73836911 100644
--- a/proto/aggregator/aggregator.c
+++ b/proto/aggregator/aggregator.c
@@ -540,6 +540,9 @@ aggregator_rt_notify(struct proto *P, struct channel 
*src_ch, net *net, rte *new
        case T_IP:
          MX(ip);
          break;
+       case T_MAC:
+         MX(mac);
+         break;
        case T_NET:
          mem_hash_mix_num(&haux, net_hash(IT(net)));
          break;
diff --git a/proto/bgp/attrs.c b/proto/bgp/attrs.c
index e853624b..bdead04f 100644
--- a/proto/bgp/attrs.c
+++ b/proto/bgp/attrs.c
@@ -203,6 +203,30 @@ bgp_encode_raw(struct bgp_write_state *s UNUSED, eattr *a, 
byte *buf, uint size)
 }
 
 
+/*
+ *     PMSI tunnel handling
+ */
+
+adata *
+bgp_pmsi_new_ingress_replication(linpool *pool, ip_addr addr, u32 label)
+{
+  int v4 = ipa_is_ip4(addr);
+  uint dlen = 5 + (v4 ? sizeof(ip4_addr) : sizeof(ip6_addr));
+  adata *ad = lp_alloc_adata(pool, dlen);
+
+  ad->data[0] = 0;
+  ad->data[1] = BGP_PMSI_TYPE_INGRESS_REPLICATION;
+  put_u24(ad->data + 2, label);
+
+  if (v4)
+    put_ip4(ad->data + 5, ipa_to_ip4(addr));
+  else
+    put_ip6(ad->data + 5, ipa_to_ip6(addr));
+
+  return ad;
+}
+
+
 /*
  *     AIGP handling
  */
@@ -849,6 +873,64 @@ bgp_decode_as4_path(struct bgp_parse_state *s, uint code 
UNUSED, uint flags, byt
 }
 
 
+static void
+bgp_decode_pmsi_tunnel(struct bgp_parse_state *s, uint code UNUSED, uint 
flags, byte *data, uint len, ea_list **to)
+{
+  if (len < 5)
+    WITHDRAW(BAD_LENGTH, "PMSI_TUNNEL", len);
+
+  uint dlen = len - 5;
+
+  switch (data[1])
+  {
+  case BGP_PMSI_TYPE_NO_INFO:
+    if (dlen != 0)
+      WITHDRAW(BAD_LENGTH, "PMSI_TUNNEL", len);
+    break;
+
+  case BGP_PMSI_TYPE_INGRESS_REPLICATION:
+    if ((dlen != sizeof(ip4_addr)) && (dlen != sizeof(ip6_addr)))
+      WITHDRAW(BAD_LENGTH, "PMSI_TUNNEL", len);
+    break;
+
+  default:
+    flags |= BAF_PARTIAL;
+  }
+
+  bgp_set_attr_data(to, s->pool, BA_PMSI_TUNNEL, flags, data, len);
+}
+
+static void
+bgp_format_pmsi_tunnel(const eattr *a, byte *buf, uint size)
+{
+  const adata *ad = a->u.ptr;
+  uint type = bgp_pmsi_get_type(ad);
+  uint label = bgp_pmsi_get_label(ad);
+
+  char mpls[16] = {};
+  if (label)
+    bsprintf(mpls, " mpls %u", label);
+
+  switch (type)
+  {
+  case BGP_PMSI_TYPE_NO_INFO:
+    bsnprintf(buf, size, "no-info%s", mpls);
+    break;
+
+  case BGP_PMSI_TYPE_INGRESS_REPLICATION:;
+    ip_addr a = bgp_pmsi_ir_get_endpoint(ad);
+    bsnprintf(buf, size, "ingress-replication %I%s", a, mpls);
+    break;
+
+  default:;
+    int n = bsnprintf(buf, size, "type %u%s ", type, mpls);
+    ADVANCE(buf, size, n);
+    bstrbintohex(ad->data + 5, ad->length - 5, buf, size, ':');
+    break;
+  }
+}
+
+
 static void
 bgp_export_aigp(struct bgp_export_state *s, eattr *a)
 {
@@ -1112,6 +1194,14 @@ static const struct bgp_attr_desc bgp_attr_table[] = {
     .decode = bgp_decode_as4_aggregator,
     .format = bgp_format_aggregator,
   },
+  [BA_PMSI_TUNNEL] = {
+    .name = "pmsi_tunnel",
+    .type = EAF_TYPE_OPAQUE,
+    .flags = BAF_OPTIONAL | BAF_TRANSITIVE,
+    .encode = bgp_encode_raw,
+    .decode = bgp_decode_pmsi_tunnel,
+    .format = bgp_format_pmsi_tunnel,
+  },
   [BA_AIGP] = {
     .name = "aigp",
     .type = EAF_TYPE_OPAQUE,
diff --git a/proto/bgp/bgp.c b/proto/bgp/bgp.c
index 0a68acb9..03f91730 100644
--- a/proto/bgp/bgp.c
+++ b/proto/bgp/bgp.c
@@ -2593,6 +2593,9 @@ bgp_channel_start(struct channel *C)
 
     if (bgp_channel_is_ipv6(c) && (ipa_is_ip6(src) || c->ext_next_hop))
       c->next_hop_addr = src;
+
+    if (bgp_channel_is_l2vpn(c))
+      c->next_hop_addr = src;
   }
 
   /* Use preferred addresses associated with interface / source address */
@@ -2945,10 +2948,10 @@ bgp_postconfig(struct proto_config *CF)
     /* Default values of IGP tables */
     if ((cc->gw_mode == GW_RECURSIVE) && !cc->desc->no_igp)
     {
-      if (!cc->igp_table_ip4 && (bgp_cc_is_ipv4(cc) || cc->ext_next_hop))
+      if (!cc->igp_table_ip4 && (bgp_cc_is_ipv4(cc) || bgp_cc_is_l2vpn(cc) || 
cc->ext_next_hop))
        cc->igp_table_ip4 = bgp_default_igp_table(cf, cc, NET_IP4);
 
-      if (!cc->igp_table_ip6 && (bgp_cc_is_ipv6(cc) || cc->ext_next_hop))
+      if (!cc->igp_table_ip6 && (bgp_cc_is_ipv6(cc) || bgp_cc_is_l2vpn(cc) || 
cc->ext_next_hop))
        cc->igp_table_ip6 = bgp_default_igp_table(cf, cc, NET_IP6);
 
       if (cc->igp_table_ip4 && bgp_cc_is_ipv6(cc) && !cc->ext_next_hop)
@@ -3620,7 +3623,7 @@ struct protocol proto_bgp = {
   .template =          "bgp%d",
   .class =             PROTOCOL_BGP,
   .preference =        DEF_PREF_BGP,
-  .channel_mask =      NB_IP | NB_VPN | NB_FLOW | NB_MPLS,
+  .channel_mask =      NB_IP | NB_VPN | NB_FLOW | NB_EVPN | NB_MPLS,
   .proto_size =                sizeof(struct bgp_proto),
   .config_size =       sizeof(struct bgp_config),
   .postconfig =                bgp_postconfig,
diff --git a/proto/bgp/bgp.h b/proto/bgp/bgp.h
index 9a206cfe..f6d303b1 100644
--- a/proto/bgp/bgp.h
+++ b/proto/bgp/bgp.h
@@ -27,10 +27,12 @@ struct eattr;
 
 #define BGP_AFI_IPV4           1
 #define BGP_AFI_IPV6           2
+#define BGP_AFI_L2VPN          25
 
 #define BGP_SAFI_UNICAST       1
 #define BGP_SAFI_MULTICAST     2
 #define BGP_SAFI_MPLS          4
+#define BGP_SAFI_EVPN          70
 #define BGP_SAFI_MPLS_VPN      128
 #define BGP_SAFI_VPN_MULTICAST 129
 #define BGP_SAFI_FLOW          133
@@ -53,6 +55,7 @@ struct eattr;
 #define BGP_AF_VPN6_MC         BGP_AF( BGP_AFI_IPV6, BGP_SAFI_VPN_MULTICAST )
 #define BGP_AF_FLOW4           BGP_AF( BGP_AFI_IPV4, BGP_SAFI_FLOW )
 #define BGP_AF_FLOW6           BGP_AF( BGP_AFI_IPV6, BGP_SAFI_FLOW )
+#define BGP_AF_EVPN            BGP_AF( BGP_AFI_L2VPN, BGP_SAFI_EVPN )
 
 
 struct bgp_write_state;
@@ -582,12 +585,18 @@ static inline int bgp_channel_is_ipv4(struct bgp_channel 
*c)
 static inline int bgp_channel_is_ipv6(struct bgp_channel *c)
 { return BGP_AFI(c->afi) == BGP_AFI_IPV6; }
 
+static inline int bgp_channel_is_l2vpn(struct bgp_channel *c)
+{ return BGP_AFI(c->afi) == BGP_AFI_L2VPN; }
+
 static inline int bgp_cc_is_ipv4(struct bgp_channel_config *c)
 { return BGP_AFI(c->afi) == BGP_AFI_IPV4; }
 
 static inline int bgp_cc_is_ipv6(struct bgp_channel_config *c)
 { return BGP_AFI(c->afi) == BGP_AFI_IPV6; }
 
+static inline int bgp_cc_is_l2vpn(struct bgp_channel_config *c)
+{ return BGP_AFI(c->afi) == BGP_AFI_L2VPN; }
+
 static inline int bgp_channel_is_role_applicable(struct bgp_channel *c)
 { return (c->afi == BGP_AF_IPV4 || c->afi == BGP_AF_IPV6); }
 
@@ -701,10 +710,37 @@ void bgp_rt_notify(struct proto *P, struct channel *C, 
net *n, rte *new, rte *ol
 int bgp_preexport(struct channel *, struct rte *);
 int bgp_get_attr(const struct eattr *e, byte *buf, int buflen);
 void bgp_get_route_info(struct rte *, byte *buf);
+adata * bgp_pmsi_new_ingress_replication(linpool *pool, ip_addr addr, u32 
label);
 int bgp_total_aigp_metric_(rte *e, u64 *metric, const struct adata **ad);
 
 byte * bgp_bmp_encode_rte(struct bgp_channel *c, byte *buf, byte *end, const 
net_addr *n, const struct rte *new, const struct rte_src *src);
 
+#define BGP_PMSI_TYPE_NO_INFO  0
+#define BGP_PMSI_TYPE_INGRESS_REPLICATION      6
+
+static inline uint
+bgp_pmsi_get_type(const adata *ad)
+{ return ad->data[1]; }
+
+static inline u32
+bgp_pmsi_get_label(const adata *ad)
+{ return get_u24(ad->data + 2); }
+
+static inline ip_addr
+bgp_pmsi_ir_get_endpoint(const adata *ad)
+{
+  uint dlen = ad->length - 5;
+  const byte *data = ad->data + 5;
+
+  if (dlen == sizeof(ip4_addr))
+    return ipa_from_ip4(get_ip4(data));
+  else if (dlen == sizeof(ip6_addr))
+    return ipa_from_ip6(get_ip6(data));
+  else
+    return IPA_NONE;
+}
+
+
 #define BGP_AIGP_METRIC                1
 #define BGP_AIGP_MAX           U64(0xffffffffffffffff)
 
@@ -771,6 +807,7 @@ byte *bgp_create_end_mark_(struct bgp_channel *c, byte 
*buf);
 #define BA_EXT_COMMUNITY       0x10    /* RFC 4360 */
 #define BA_AS4_PATH             0x11   /* RFC 6793 */
 #define BA_AS4_AGGREGATOR       0x12   /* RFC 6793 */
+#define BA_PMSI_TUNNEL         0x16    /* RFC 6514 */
 #define BA_AIGP                        0x1a    /* RFC 7311 */
 #define BA_LARGE_COMMUNITY     0x20    /* RFC 8092 */
 #define BA_ONLY_TO_CUSTOMER    0x23    /* RFC 9234 */
diff --git a/proto/bgp/config.Y b/proto/bgp/config.Y
index 4ffc49f6..7cf499bc 100644
--- a/proto/bgp/config.Y
+++ b/proto/bgp/config.Y
@@ -36,7 +36,7 @@ CF_KEYWORDS(BGP, LOCAL, NEIGHBOR, AS, HOLD, TIME, CONNECT, 
RETRY, KEEPALIVE,
        DYNAMIC, RANGE, NAME, DIGITS, BGP_AIGP, AIGP, ORIGINATE, COST, ENFORCE,
        FIRST, FREE, VALIDATE, BASE, ROLE, ROLES, PEER, PROVIDER, CUSTOMER,
        RS_SERVER, RS_CLIENT, REQUIRE, BGP_OTC, GLOBAL, SEND, RECV, MIN, MAX,
-       AUTHENTICATION, NONE, MD5, AO, FORMAT, NATIVE, SINGLE, DOUBLE)
+       AUTHENTICATION, NONE, MD5, AO, FORMAT, NATIVE, SINGLE, DOUBLE, 
BGP_PMSI_TUNNEL)
 
 CF_KEYWORDS(KEY, KEYS, SECRET, DEPRECATED, PREFERRED, ALGORITHM, CMAC, AES128)
 
@@ -265,6 +265,7 @@ bgp_afi:
  | VPN6 MULTICAST      { $$ = BGP_AF_VPN6_MC; }
  | FLOW4               { $$ = BGP_AF_FLOW4; }
  | FLOW6               { $$ = BGP_AF_FLOW6; }
+ | EVPN                        { $$ = BGP_AF_EVPN; }
  ;
 
 tcp_ao_key_start: KEY {
@@ -520,6 +521,8 @@ dynamic_attr: BGP_CLUSTER_LIST
        { $$ = f_new_dynamic_attr(EAF_TYPE_INT_SET, T_CLIST, 
EA_CODE(PROTOCOL_BGP, BA_CLUSTER_LIST));   $$.flags = BAF_OPTIONAL; } ;
 dynamic_attr: BGP_EXT_COMMUNITY
        { $$ = f_new_dynamic_attr(EAF_TYPE_EC_SET, T_ECLIST, 
EA_CODE(PROTOCOL_BGP, BA_EXT_COMMUNITY));  $$.flags = BAF_OPTIONAL | 
BAF_TRANSITIVE; } ;
+dynamic_attr: BGP_PMSI_TUNNEL
+       { $$ = f_new_dynamic_attr(EAF_TYPE_OPAQUE, T_ENUM_EMPTY, 
EA_CODE(PROTOCOL_BGP, BA_PMSI_TUNNEL)); } ;
 dynamic_attr: BGP_AIGP
        { $$ = f_new_dynamic_attr(EAF_TYPE_OPAQUE, T_ENUM_EMPTY, 
EA_CODE(PROTOCOL_BGP, BA_AIGP));       $$.flags = BAF_OPTIONAL; } ;
 dynamic_attr: BGP_LARGE_COMMUNITY
diff --git a/proto/bgp/packets.c b/proto/bgp/packets.c
index fcaff745..2c2edb08 100644
--- a/proto/bgp/packets.c
+++ b/proto/bgp/packets.c
@@ -34,6 +34,7 @@
 #define BGP_RR_END             2
 
 #define BGP_NLRI_MAX           (4 + 1 + 32)
+#define BGP_NLRI_EVPN_MAX      (4 + 2 + 52)
 
 #define BGP_MPLS_BOS           1       /* Bottom-of-stack bit */
 #define BGP_MPLS_MAX           10      /* Max number of labels that 24*n <= 
255 */
@@ -1075,6 +1076,25 @@ bgp_rx_open(struct bgp_conn *conn, byte *pkt, uint len)
 
 #define MISMATCHED_AF  " - mismatched address family (%I for %s)"
 
+static int
+bgp_channel_match_next_hop_af(struct bgp_channel *c, ip_addr nh)
+{
+  switch (BGP_AFI(c->afi))
+  {
+  case BGP_AFI_IPV4:
+    return ipa_is_ip4(nh) || c->ext_next_hop;
+
+  case BGP_AFI_IPV6:
+    return ipa_is_ip6(nh) || c->ext_next_hop;
+
+  case BGP_AFI_L2VPN:
+    return 1;
+
+  default:
+    return 0;
+  }
+}
+
 static void
 bgp_apply_next_hop(struct bgp_parse_state *s, rta *a, ip_addr gw, ip_addr ll)
 {
@@ -1308,12 +1328,11 @@ bgp_update_next_hop_ip(struct bgp_export_state *s, 
eattr *a, ea_list **to)
     REJECT(BAD_NEXT_HOP " - neighbor address %I", peer);
 
   /* Forbid next hop with non-matching AF */
-  if ((ipa_is_ip4(nh[0]) != bgp_channel_is_ipv4(s->channel)) &&
-      !s->channel->ext_next_hop)
+  if (!bgp_channel_match_next_hop_af(s->channel, nh[0]))
     REJECT(BAD_NEXT_HOP MISMATCHED_AF, nh[0], s->channel->desc->name);
 
-  /* Just check if MPLS stack */
-  if (s->mpls && !bgp_find_attr(*to, BA_MPLS_LABEL_STACK))
+  /* Just check if there is MPLS stack - not applicable for EVPN */
+  if (s->mpls && (s->channel->afi != BGP_AF_EVPN) && !bgp_find_attr(*to, 
BA_MPLS_LABEL_STACK))
     REJECT(NO_LABEL_STACK);
 }
 
@@ -1422,7 +1441,7 @@ bgp_decode_next_hop_ip(struct bgp_parse_state *s, byte 
*data, uint len, rta *a)
   if (ipa_zero(nh[1]))
     ad->length = 16;
 
-  if ((bgp_channel_is_ipv4(c) != ipa_is_ip4(nh[0])) && !c->ext_next_hop)
+  if (!bgp_channel_match_next_hop_af(c, nh[0]))
     WITHDRAW(BAD_NEXT_HOP MISMATCHED_AF, nh[0], c->desc->name);
 
   // XXXX validate next hop
@@ -1513,7 +1532,7 @@ bgp_decode_next_hop_vpn(struct bgp_parse_state *s, byte 
*data, uint len, rta *a)
   if ((get_u64(data) != 0) || ((len == 48) && (get_u64(data+24) != 0)))
     bgp_parse_error(s, 9);
 
-  if ((bgp_channel_is_ipv4(c) != ipa_is_ip4(nh[0])) && !c->ext_next_hop)
+  if (!bgp_channel_match_next_hop_af(c, nh[0]))
     WITHDRAW(BAD_NEXT_HOP MISMATCHED_AF, nh[0], c->desc->name);
 
   // XXXX validate next hop
@@ -2222,6 +2241,407 @@ bgp_decode_nlri_flow6(struct bgp_parse_state *s, byte 
*pos, uint len, rta *a)
   }
 }
 
+static inline void
+bgp_encode_evpn_ip(byte **pos, uint *size, ip_addr ip)
+{
+  if (ipa_is_ip4(ip))
+  {
+    **pos = IP4_MAX_PREFIX_LENGTH;
+    put_ip4(*pos+1, ipa_to_ip4(ip));
+    ADVANCE(*pos, *size, 1+4);
+  }
+  else
+  {
+    **pos = IP6_MAX_PREFIX_LENGTH;
+    put_ip6(*pos+1, ipa_to_ip6(ip));
+    ADVANCE(*pos, *size, 1+16);
+  }
+}
+
+static inline ip_addr
+bgp_decode_evpn_ip(struct bgp_parse_state *s, byte **pos, uint *len)
+{
+  uint alen = **pos;   /* Assume this is validated by caller */
+  uint blen = 1 + (alen >> 3);
+
+  if (*len < blen)
+    bgp_parse_error(s, 1);
+
+  ip_addr ip;
+  if (alen == IP4_MAX_PREFIX_LENGTH)
+    ip = ipa_from_ip4(get_ip4(*pos + 1));
+  else if (alen == IP6_MAX_PREFIX_LENGTH)
+    ip = ipa_from_ip6(get_ip6(*pos + 1));
+  else
+    bgp_parse_error(s, 10); /* ? */
+
+  ADVANCE(*pos, *len, blen);
+  return ip;
+}
+
+static inline u32 bgp_label_ready(const adata *m, uint pos)
+{ return m && (m->length >= 4*(pos+1)); }
+
+static inline u32 bgp_get_label_(const adata *m, uint pos)
+{ return ((u32 *) m->data)[pos]; }
+
+static inline u32 bgp_get_label(const adata *m, uint pos)
+{ return bgp_label_ready(m, pos) ? bgp_get_label_(m, pos) : 0; }
+
+static uint
+bgp_encode_evpn_ead(struct bgp_write_state *s UNUSED, const net_addr_evpn 
*net, byte *buf, uint size)
+{
+  byte *pos = buf;
+
+  /* Encode route distinguisher */
+  put_u64(pos, net->rd);
+  ADVANCE(pos, size, 8);
+
+  /* Encode ethernet segment ID */
+  memcpy(pos, &net->ead.esi, 10);
+  ADVANCE(pos, size, 10);
+
+  /* Encode ethernet tag ID */
+  put_u32(pos, net->tag);
+  ADVANCE(pos, size, 4);
+
+  /* Encode MPLS label */
+  u32 label = bgp_get_label(s->mpls_labels, 0);
+  put_u24(pos, label); // << 4);
+  ADVANCE(pos, size, 3);
+
+  return pos - buf;
+}
+
+static void
+bgp_decode_evpn_ead(struct bgp_parse_state *s, net_addr_evpn *net, byte *pos, 
uint len)
+{
+  if (len < (8+10+4+3))
+    bgp_parse_error(s, 1);
+
+  /* Decode route distinguisher */
+  u64 rd = get_u64(pos);
+  ADVANCE(pos, len, 8);
+
+  /* Decode ethernet segment ID */
+  evpn_esi esi;
+  memcpy(&esi, pos, 10);
+  ADVANCE(pos, len, 10);
+
+  /* Decode ethernet tag ID */
+  u32 tag = get_u32(pos);
+  ADVANCE(pos, len, 4);
+
+  /* Decode MPLS label */
+  u32 label = get_u24(pos); // >> 4;
+  ADVANCE(pos, len, 3);
+
+  s->mpls_labels = lp_alloc_adata(s->pool, 4);
+  memcpy(s->mpls_labels->data, &label, 4);
+
+  if (len)
+    bgp_parse_error(s, 1);
+
+  net->ead = NET_ADDR_EVPN_EAD(rd, tag, esi);
+}
+
+static uint
+bgp_encode_evpn_mac(struct bgp_write_state *s UNUSED, const net_addr_evpn 
*net, byte *buf, uint size)
+{
+  byte *pos = buf;
+
+  /* Encode route distinguisher */
+  put_u64(pos, net->rd);
+  ADVANCE(pos, size, 8);
+
+  /* Encode ethernet segment ID - XXX */
+  memset(pos, 0, 10);
+  ADVANCE(pos, size, 10);
+
+  /* Encode ethernet tag ID */
+  put_u32(pos, net->tag);
+  ADVANCE(pos, size, 4);
+
+  /* Encode MAC address */
+  pos[0] = 48;
+  memcpy(pos+1, &net->mac.mac, 6);
+  ADVANCE(pos, size, 7);
+
+  /* Encode IP address */
+  pos[0] = 0;
+  if (net->length == sizeof(net_addr_evpn_mac_ip))
+    bgp_encode_evpn_ip(&pos, &size, net->mac_ip.ip);
+  else
+    ADVANCE(pos, size, 1);
+
+  /* Encode MPLS label */
+  u32 label1 = bgp_get_label(s->mpls_labels, 0);
+  put_u24(pos, label1); // << 4);
+  ADVANCE(pos, size, 3);
+
+  if (bgp_label_ready(s->mpls_labels, 1))
+  {
+    u32 label2 = bgp_get_label_(s->mpls_labels, 1);
+    put_u24(pos, label2); // << 4);
+    ADVANCE(pos, size, 3);
+  }
+
+  return pos - buf;
+}
+
+static void
+bgp_decode_evpn_mac(struct bgp_parse_state *s, net_addr_evpn *net, byte *pos, 
uint len)
+{
+  if (len < (8+10+4+7+1))
+    bgp_parse_error(s, 1);
+
+  /* Decode route distinguisher */
+  u64 rd = get_u64(pos);
+  ADVANCE(pos, len, 8);
+
+  /* Decode ethernet segment ID - XXX */
+  evpn_esi esi;
+  memcpy(&esi, pos, 10);
+  ADVANCE(pos, len, 10);
+
+  /* Decode ethernet tag ID */
+  u32 tag = get_u32(pos);
+  ADVANCE(pos, len, 4);
+
+  /* Decode MAC address */
+  if (pos[0] != 48)
+    bgp_parse_error(s, 10); /* ? */
+
+  mac_addr mac;
+  memcpy(&mac, pos+1, 6);
+  ADVANCE(pos, len, 7);
+
+  /* Decode IP address */
+  ip_addr ip = IPA_NONE;
+  if (pos[0])
+    ip = bgp_decode_evpn_ip(s, &pos, &len);
+  else
+    ADVANCE(pos, len, 1);
+
+  /* Decode MPLS labels */
+  if (len < 3)
+    bgp_parse_error(s, 1);
+
+  u32 label[2], lnum = 1;
+  label[0] = get_u24(pos); // >> 4;
+  ADVANCE(pos, len, 3);
+
+  if (len >= 3)
+  {
+    label[1] = get_u24(pos); // >> 4;
+    ADVANCE(pos, len, 3);
+    lnum++;
+  }
+
+  s->mpls_labels = lp_alloc_adata(s->pool, 4 * lnum);
+  memcpy(s->mpls_labels->data, label, 4 * lnum);
+
+  if (len)
+    bgp_parse_error(s, 1);
+
+  if (ipa_zero(ip))
+    net->mac = NET_ADDR_EVPN_MAC(rd, tag, mac);
+  else
+    net->mac_ip = NET_ADDR_EVPN_MAC_IP(rd, tag, mac, ip);
+}
+
+static uint
+bgp_encode_evpn_imet(struct bgp_write_state *s UNUSED, const net_addr_evpn 
*net, byte *buf, uint size)
+{
+  byte *pos = buf;
+
+  /* Encode route distinguisher */
+  put_u64(pos, net->rd);
+  ADVANCE(pos, size, 8);
+
+  /* Encode ethernet tag ID */
+  put_u32(pos, net->tag);
+  ADVANCE(pos, size, 4);
+
+  /* Encode router IP address */
+  bgp_encode_evpn_ip(&pos, &size, net->imet.rtr);
+
+  return pos - buf;
+}
+
+static void
+bgp_decode_evpn_imet(struct bgp_parse_state *s, net_addr_evpn *net, byte *pos, 
uint len)
+{
+  if (len < (8+4+1))
+    bgp_parse_error(s, 1);
+
+  /* Decode route distinguisher */
+  u64 rd = get_u64(pos);
+  ADVANCE(pos, len, 8);
+
+  /* Decode ethernet tag ID */
+  u32 tag = get_u32(pos);
+  ADVANCE(pos, len, 4);
+
+  /* Decode router IP address */
+  ip_addr rtr = bgp_decode_evpn_ip(s, &pos, &len);
+
+  if (len)
+    bgp_parse_error(s, 1);
+
+  net->imet = NET_ADDR_EVPN_IMET(rd, tag, rtr);
+}
+
+static uint
+bgp_encode_evpn_es(struct bgp_write_state *s UNUSED, const net_addr_evpn *net, 
byte *buf, uint size)
+{
+  byte *pos = buf;
+
+  /* Encode route distinguisher */
+  put_u64(pos, net->rd);
+  ADVANCE(pos, size, 8);
+
+  /* Encode ethernet segment ID */
+  memcpy(pos, &net->es.esi, 10);
+  ADVANCE(pos, size, 10);
+
+  /* Encode router IP address */
+  bgp_encode_evpn_ip(&pos, &size, net->es.rtr);
+
+  return pos - buf;
+}
+
+static void
+bgp_decode_evpn_es(struct bgp_parse_state *s, net_addr_evpn *net, byte *pos, 
uint len)
+{
+  if (len < (8+10+1))
+    bgp_parse_error(s, 1);
+
+  /* Decode route distinguisher */
+  u64 rd = get_u64(pos);
+  ADVANCE(pos, len, 8);
+
+  /* Decode ethernet segment ID */
+  evpn_esi esi;
+  memcpy(&esi, pos, 10);
+  ADVANCE(pos, len, 10);
+
+  /* Decode router IP address */
+  ip_addr rtr = bgp_decode_evpn_ip(s, &pos, &len);
+
+  if (len)
+    bgp_parse_error(s, 1);
+
+  net->es = NET_ADDR_EVPN_ES(rd, esi, rtr);
+}
+
+static uint
+bgp_encode_nlri_evpn(struct bgp_write_state *s, struct bgp_bucket *buck, byte 
*buf, uint size)
+{
+  byte *pos = buf;
+
+  while (!EMPTY_LIST(buck->prefixes) && (size >= BGP_NLRI_EVPN_MAX))
+  {
+    struct bgp_prefix *px = HEAD(buck->prefixes);
+    const net_addr_evpn *net = (void *) px->net;
+
+    /* Encode path ID */
+    if (s->add_path)
+    {
+      put_u32(pos, px->path_id);
+      ADVANCE(pos, size, 4);
+    }
+
+    /* Encode EVPN header */
+    pos[0] = net->subtype;
+    pos[1] = 0;
+    ADVANCE(pos, size, 2);
+
+    uint rlen;
+    switch (net->subtype)
+    {
+    case NET_EVPN_EAD: rlen = bgp_encode_evpn_ead(s, net, pos, size); break;
+    case NET_EVPN_MAC: rlen = bgp_encode_evpn_mac(s, net, pos, size); break;
+    case NET_EVPN_IMET:        rlen = bgp_encode_evpn_imet(s, net, pos, size); 
break;
+    case NET_EVPN_ES:  rlen = bgp_encode_evpn_es(s, net, pos, size); break;
+    }
+
+    /* Fix length */
+    pos[-1] = rlen;
+
+    ADVANCE(pos, size, rlen);
+
+    if (!s->sham)
+      bgp_free_prefix(s->channel, px);
+    else
+      rem_node(&px->buck_node);
+  }
+
+  return pos - buf;
+}
+
+static void
+bgp_decode_nlri_evpn(struct bgp_parse_state *s, byte *pos, uint len, rta *a)
+{
+  ea_list *base_eattrs = a ? a->eattrs : NULL;
+
+  while (len)
+  {
+    net_addr_evpn net;
+    u32 path_id = 0;
+
+    s->mpls_labels = NULL;
+
+    /* Reset attributes */
+    if (a)
+      a->eattrs = base_eattrs;
+
+    /* Decode path ID */
+    if (s->add_path)
+    {
+      if (len < 5)
+       bgp_parse_error(s, 1);
+
+      path_id = get_u32(pos);
+      ADVANCE(pos, len, 4);
+    }
+
+    if (len < 2)
+      bgp_parse_error(s, 1);
+
+    /* Decode EVPN header */
+    uint type = pos[0];
+    uint rlen = pos[1];
+    ADVANCE(pos, len, 2);
+
+    if (len < rlen)
+      bgp_parse_error(s, 1);
+
+    switch (type)
+    {
+    case NET_EVPN_EAD: bgp_decode_evpn_ead(s, &net, pos, rlen); break;
+    case NET_EVPN_MAC: bgp_decode_evpn_mac(s, &net, pos, rlen); break;
+    case NET_EVPN_IMET:        bgp_decode_evpn_imet(s, &net, pos, rlen); break;
+    case NET_EVPN_ES:  bgp_decode_evpn_es(s, &net, pos, rlen); break;
+    default: net = (net_addr_evpn){}; // XXX
+    }
+
+    ADVANCE(pos, len, rlen);
+
+    if (a && s->mpls_labels)
+    {
+      adata *m = s->mpls_labels;
+      bgp_set_attr_ptr(&(a->eattrs), s->pool, BA_MPLS_LABEL_STACK, 0, m);
+      bgp_apply_mpls_labels(s, a, (u32 *) m->data, m->length / 4);
+    }
+
+    bgp_rte_update(s, (net_addr *) &net, path_id, a);
+
+    rta_free(s->cached_rta);
+    s->cached_rta = NULL;
+  }
+}
+
 
 static const struct bgp_af_desc bgp_af_table[] = {
   {
@@ -2350,6 +2770,17 @@ static const struct bgp_af_desc bgp_af_table[] = {
     .decode_next_hop = bgp_decode_next_hop_none,
     .update_next_hop = bgp_update_next_hop_none,
   },
+  {
+    .afi = BGP_AF_EVPN,
+    .net = NET_EVPN,
+    .mpls = 1,
+    .name = "evpn",
+    .encode_nlri = bgp_encode_nlri_evpn,
+    .decode_nlri = bgp_decode_nlri_evpn,
+    .encode_next_hop = bgp_encode_next_hop_ip,
+    .decode_next_hop = bgp_decode_next_hop_ip,
+    .update_next_hop = bgp_update_next_hop_ip,
+  },
 };
 
 const struct bgp_af_desc *
diff --git a/proto/bridge/Doc b/proto/bridge/Doc
new file mode 100644
index 00000000..ca0136f9
--- /dev/null
+++ b/proto/bridge/Doc
@@ -0,0 +1 @@
+S bridge.c
diff --git a/proto/bridge/Makefile b/proto/bridge/Makefile
new file mode 100644
index 00000000..82755ee7
--- /dev/null
+++ b/proto/bridge/Makefile
@@ -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
index 00000000..5e0a8488
--- /dev/null
+++ b/proto/bridge/bridge.c
@@ -0,0 +1,285 @@
+/*
+ *     BIRD -- Linux Bridge Interface
+ *
+ *     (c) 2023 Ondrej Zajicek <[email protected]>
+ *     (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
index 00000000..0022eae4
--- /dev/null
+++ b/proto/bridge/bridge.h
@@ -0,0 +1,53 @@
+/*
+ *     BIRD -- Linux Bridge Interface
+ *
+ *     (c) 2023 Ondrej Zajicek <[email protected]>
+ *     (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
index 00000000..f978bda9
--- /dev/null
+++ b/proto/bridge/config.Y
@@ -0,0 +1,59 @@
+/*
+ *     BIRD -- Linux Bridge Interface
+ *
+ *     (c) 2023 Ondrej Zajicek <[email protected]>
+ *     (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
diff --git a/proto/evpn/Doc b/proto/evpn/Doc
new file mode 100644
index 00000000..84f282fa
--- /dev/null
+++ b/proto/evpn/Doc
@@ -0,0 +1 @@
+S evpn.c
diff --git a/proto/evpn/Makefile b/proto/evpn/Makefile
new file mode 100644
index 00000000..7c7dfc92
--- /dev/null
+++ b/proto/evpn/Makefile
@@ -0,0 +1,6 @@
+src := evpn.c
+obj := $(src-o-files)
+$(all-daemon)
+$(cf-local)
+
+tests_objs := $(tests_objs) $(src-o-files)
diff --git a/proto/evpn/config.Y b/proto/evpn/config.Y
new file mode 100644
index 00000000..db5156de
--- /dev/null
+++ b/proto/evpn/config.Y
@@ -0,0 +1,86 @@
+/*
+ *     BIRD -- BGP/MPLS Ethernet Virtual Private Networks (EVPN)
+ *
+ *     (c) 2023 Ondrej Zajicek <[email protected]>
+ *     (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/evpn/evpn.h"
+
+
+CF_DEFINES
+
+#define EVPN_CFG ((struct evpn_config *) this_proto)
+
+
+CF_DECLS
+
+CF_KEYWORDS(EVPN, ROUTE, IMPORT, EXPORT, TARGET, RD, DISTINGUISHER, TUNNEL, 
DEVICE, VNI, VID)
+
+%type <e> evpn_targets
+%type <cc> evpn_channel_start evpn_channel
+
+
+CF_GRAMMAR
+
+proto: evpn_proto;
+
+
+evpn_channel_start: net_type_base
+{
+  /* Redefining proto_channel to change default values */
+  $$ = this_channel = channel_config_get(NULL, net_label[$1], $1, this_proto);
+  if (!this_channel->copy)
+  {
+    this_channel->out_filter = FILTER_ACCEPT;
+    this_channel->preference = ($1 == NET_ETH) ?
+      DEF_PREF_L3VPN_IMPORT :
+      DEF_PREF_L3VPN_EXPORT;
+  }
+};
+
+evpn_channel: evpn_channel_start channel_opt_list channel_end;
+
+evpn_proto_start: proto_start EVPN
+{
+  this_proto = proto_config_new(&proto_evpn, $1);
+};
+
+
+evpn_proto_item:
+   proto_item
+ | evpn_channel
+ | mpls_channel
+ | RD VPN_RD { EVPN_CFG->rd = rd_to_u64($2); }
+ | ROUTE DISTINGUISHER VPN_RD { EVPN_CFG->rd = rd_to_u64($3); }
+ | IMPORT TARGET evpn_targets { EVPN_CFG->import_target = $3; }
+ | EXPORT TARGET evpn_targets { EVPN_CFG->export_target = $3; }
+ | ROUTE TARGET evpn_targets { EVPN_CFG->import_target = 
EVPN_CFG->export_target = $3; }
+ | TUNNEL DEVICE text { EVPN_CFG->tunnel_dev = if_get_by_name($3); }
+ | ROUTER ADDRESS ipa { EVPN_CFG->router_addr = $3; }
+ | VNI expr { EVPN_CFG->vni = $2; }
+ | VID expr { EVPN_CFG->vid = $2; if ($2 > 4095) cf_error("VID must be in 
range 0-4095"); }
+ ;
+
+evpn_proto_opts:
+   /* empty */
+ | evpn_proto_opts evpn_proto_item ';'
+ ;
+
+evpn_proto:
+   evpn_proto_start proto_name '{' evpn_proto_opts '}';
+
+
+evpn_targets:
+   ec_item { f_tree_only_rt($1); $$ = $1; }
+ | '[' ec_items ']' { f_tree_only_rt($2); $$ = build_tree($2, true); }
+ ;
+
+
+CF_CODE
+
+CF_END
diff --git a/proto/evpn/evpn.c b/proto/evpn/evpn.c
new file mode 100644
index 00000000..1c8cad92
--- /dev/null
+++ b/proto/evpn/evpn.c
@@ -0,0 +1,572 @@
+/*
+ *     BIRD -- BGP/MPLS Ethernet Virtual Private Networks (EVPN)
+ *
+ *     (c) 2023 Ondrej Zajicek <[email protected]>
+ *     (c) 2023 CZ.NIC z.s.p.o.
+ *
+ *     Can be freely distributed and used under the terms of the GNU GPL.
+ */
+
+/**
+ * DOC: BGP/MPLS Ethernet Virtual Private Networks (EVPN)
+ *
+ * The EVPN protocol implements RFC 7432 BGP Etherent VPNs using VXLAN 
overlays.
+ * It works similarly to L3VPN. It connects ethernet table (one per VRF) with
+ * (global) EVPN table. Routes passed from EVPN table to ethernet table are
+ * stripped of RD and filtered by import targets, routes passed in the other
+ * direction are extended with RD, MPLS/VNI labels, and export targets in
+ * extended communities.
+ *
+ * The EVPN protocol supports MAC (type 2) and IMET (type 3) EVPN routes, there
+ * is no support for EAD / ES routes, or routes with non-zero tag. There is 
also
+ * no support for MPLS backbone, just VXLAN overlays.
+ *
+ * Supported standards:
+ * RFC 7432 - BGP MPLS-Based Ethernet VPN
+ * RFC 8365 - Network Virtualization Using Ethernet VPN
+ */
+
+/*
+ * TODO:
+ * - Encapsulation community handling
+ * - MAC mobility community handling
+ * - Review preference handling
+ * - Wait for existence (and active state) of the tunnel device
+ * - Learn VNI / router address from the tunnel device
+ * - Improved VLAN handling
+ * - MPLS encapsulation mode
+ */
+
+#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 "evpn.h"
+
+#include "proto/bgp/bgp.h"
+
+#define EA_BGP_NEXT_HOP                EA_CODE(PROTOCOL_BGP, BA_NEXT_HOP)
+#define EA_BGP_EXT_COMMUNITY   EA_CODE(PROTOCOL_BGP, BA_EXT_COMMUNITY)
+#define EA_BGP_PMSI_TUNNEL     EA_CODE(PROTOCOL_BGP, BA_PMSI_TUNNEL)
+#define EA_BGP_MPLS_LABEL_STACK        EA_CODE(PROTOCOL_BGP, 
BA_MPLS_LABEL_STACK)
+
+static inline const struct adata * ea_get_adata(ea_list *e, uint id)
+{ eattr *a = ea_find(e, id); return a ? a->u.ptr : &null_adata; }
+
+static inline int
+mpls_valid_nexthop(const rta *a)
+{
+  /* MPLS does not support special blackhole targets */
+  if (a->dest != RTD_UNICAST)
+    return 0;
+
+  /* MPLS does not support ARP / neighbor discovery */
+  for (const struct nexthop *nh = &a->nh; nh ; nh = nh->next)
+    if (ipa_zero(nh->gw) && (nh->iface->flags & IF_MULTIACCESS))
+      return 0;
+
+  return 1;
+}
+
+static int
+evpn_import_targets(struct evpn_proto *p, const struct adata *list)
+{
+  return (p->import_target_one) ?
+    ec_set_contains(list, p->import_target->from.val.ec) :
+    eclist_match_set(list, p->import_target);
+}
+
+static struct adata *
+evpn_export_targets(struct evpn_proto *p, const struct adata *src)
+{
+  u32 *s = int_set_get_data(src);
+  int len = int_set_get_size(src);
+
+  struct adata *dst = lp_alloc(tmp_linpool, sizeof(struct adata) + (len + 
p->export_target_length) * sizeof(u32));
+  u32 *d = int_set_get_data(dst);
+  int end = 0;
+
+  for (int i = 0; i < len; i += 2)
+  {
+    /* Remove existing route targets */
+    uint type = s[i] >> 16;
+    if (ec_type_is_rt(type))
+      continue;
+
+    d[end++] = s[i];
+    d[end++] = s[i+1];
+  }
+
+  /* Add new route targets */
+  memcpy(d + end, p->export_target_data, p->export_target_length * 
sizeof(u32));
+  end += p->export_target_length;
+
+  /* Set length */
+  dst->length = end * sizeof(u32);
+
+  return dst;
+}
+
+static inline void
+evpn_prepare_import_targets(struct evpn_proto *p)
+{
+  const struct f_tree *t = p->import_target;
+  p->import_target_one = !t->left && !t->right && (t->from.val.ec == 
t->to.val.ec);
+}
+
+static void
+evpn_add_ec(const struct f_tree *t, void *P)
+{
+  struct evpn_proto *p = P;
+  ec_put(p->export_target_data, p->export_target_length, t->from.val.ec);
+  p->export_target_length += 2;
+}
+
+static void
+evpn_prepare_export_targets(struct evpn_proto *p)
+{
+  if (p->export_target_data)
+    mb_free(p->export_target_data);
+
+  uint len = 2 * tree_node_count(p->export_target);
+  p->export_target_data = mb_alloc(p->p.pool, len * sizeof(u32));
+  p->export_target_length = 0;
+  tree_walk(p->export_target, evpn_add_ec, p);
+  ASSERT(p->export_target_length == len);
+}
+
+static void
+evpn_announce_mac(struct evpn_proto *p, const net_addr_eth *n0, rte *new)
+{
+  struct channel *c = p->evpn_channel;
+
+  net_addr *n = alloca(sizeof(net_addr_evpn_mac));
+  net_fill_evpn_mac(n, p->rd, 0, n0->mac);
+
+  if (new)
+  {
+    rta *a = alloca(RTA_MAX_SIZE);
+    *a = (rta) {
+      .source = RTS_EVPN,
+      .scope = SCOPE_UNIVERSE,
+      .pref = c->preference,
+    };
+
+    struct adata *ad = evpn_export_targets(p, &null_adata);
+    ea_set_attr_ptr(&a->eattrs, tmp_linpool, EA_BGP_EXT_COMMUNITY, 0, 
EAF_TYPE_EC_SET, ad);
+
+    ea_set_attr_u32(&a->eattrs, tmp_linpool, EA_MPLS_LABEL, 0, EAF_TYPE_INT, 
p->vni);
+
+    rte *e = rte_get_temp(a, p->p.main_source);
+    rte_update2(c, n, e, p->p.main_source);
+  }
+  else
+  {
+    rte_update2(c, n, NULL, p->p.main_source);
+  }
+}
+
+static void
+evpn_announce_imet(struct evpn_proto *p, int new)
+{
+  struct channel *c = p->evpn_channel;
+
+  net_addr *n = alloca(sizeof(net_addr_evpn_imet));
+  net_fill_evpn_imet(n, p->rd, 0, p->router_addr);
+
+  if (new)
+  {
+    rta *a = alloca(RTA_MAX_SIZE);
+    *a = (rta) {
+      .source = RTS_EVPN,
+      .scope = SCOPE_UNIVERSE,
+      .pref = c->preference,
+    };
+
+    struct adata *ad = evpn_export_targets(p, &null_adata);
+    ea_set_attr_ptr(&a->eattrs, tmp_linpool, EA_BGP_EXT_COMMUNITY, 0, 
EAF_TYPE_EC_SET, ad);
+
+    ad = bgp_pmsi_new_ingress_replication(tmp_linpool, p->router_addr, p->vni);
+    ea_set_attr_ptr(&a->eattrs, tmp_linpool, EA_BGP_PMSI_TUNNEL, 0, 
EAF_TYPE_OPAQUE, ad);
+
+    rte *e = rte_get_temp(a, p->p.main_source);
+    rte_update2(c, n, e, p->p.main_source);
+  }
+  else
+  {
+    rte_update2(c, n, NULL, p->p.main_source);
+  }
+}
+
+#define BAD(msg, args...) \
+  ({ log(L_ERR "%s: " msg, p->p.name, ## args); goto withdraw; })
+
+
+static void
+evpn_receive_mac(struct evpn_proto *p, const net_addr_evpn_mac *n0, rte *new)
+{
+  struct channel *c = p->eth_channel;
+
+  net_addr *n = alloca(sizeof(net_addr_eth));
+  net_fill_eth(n, n0->mac, p->vid);
+
+  if (new && rte_resolvable(new))
+  {
+    eattr *nh = ea_find(new->attrs->eattrs, EA_BGP_NEXT_HOP);
+    if (!nh)
+      BAD("Missing NEXT_HOP attribute in %N", n0);
+
+    eattr *ms = ea_find(new->attrs->eattrs, EA_BGP_MPLS_LABEL_STACK);
+    if (!ms)
+      BAD("Missing MPLS label stack in %N", n0);
+
+    rta *a = alloca(RTA_MAX_SIZE);
+    *a = (rta) {
+      .source = RTS_EVPN,
+      .scope = SCOPE_UNIVERSE,
+      .dest = RTD_UNICAST,
+      .pref = c->preference,
+      .nh.gw = *((ip_addr *) nh->u.ptr->data),
+      .nh.iface = p->tunnel_dev,
+    };
+
+    a->nh.labels = MIN(ms->u.ptr->length / 4, MPLS_MAX_LABEL_STACK);
+    memcpy(a->nh.label, ms->u.ptr->data, a->nh.labels * 4);
+
+    rte *e = rte_get_temp(a, p->p.main_source);
+    rte_update2(c, n, e, p->p.main_source);
+  }
+  else
+  {
+  withdraw:
+    rte_update2(c, n, NULL, p->p.main_source);
+  }
+}
+
+static void
+evpn_receive_imet(struct evpn_proto *p, const net_addr_evpn_imet *n0, rte *new)
+{
+  struct channel *c = p->eth_channel;
+  struct rte_src *s = rt_get_source(&p->p, n0->rd);
+
+  net_addr *n = alloca(sizeof(net_addr_eth));
+  net_fill_eth(n, MAC_NONE, p->vid);
+
+  if (new && rte_resolvable(new))
+  {
+    eattr *pt = ea_find(new->attrs->eattrs, EA_BGP_PMSI_TUNNEL);
+    if (!pt)
+      BAD("Missing PMSI_TUNNEL attribute in %N", n0);
+
+    uint pmsi_type = bgp_pmsi_get_type(pt->u.ptr);
+    if (pmsi_type != BGP_PMSI_TYPE_INGRESS_REPLICATION)
+      BAD("Unsupported PMSI_TUNNEL type %u in %N", pmsi_type, n0);
+
+    rta *a = alloca(RTA_MAX_SIZE);
+    *a = (rta) {
+      .source = RTS_EVPN,
+      .scope = SCOPE_UNIVERSE,
+      .dest = RTD_UNICAST,
+      .pref = c->preference,
+      .nh.gw = bgp_pmsi_ir_get_endpoint(pt->u.ptr),
+      .nh.iface = p->tunnel_dev,
+    };
+
+    a->nh.labels = 1;
+    a->nh.label[0] = bgp_pmsi_get_label(pt->u.ptr);
+
+    rte *e = rte_get_temp(a, s);
+    rte_update2(c, n, e, s);
+  }
+  else
+  {
+  withdraw:
+    rte_update2(c, n, NULL, s);
+  }
+}
+
+
+
+static void
+evpn_rt_notify(struct proto *P, struct channel *c0 UNUSED, net *net, rte *new, 
rte *old UNUSED)
+{
+  struct evpn_proto *p = (void *) P;
+  const net_addr *n = net->n.addr;
+
+  switch (n->type)
+  {
+  case NET_ETH:
+    evpn_announce_mac(p, (const net_addr_eth *) n, new);
+    return;
+
+  case NET_EVPN:
+    switch (((const net_addr_evpn *) n)->subtype)
+    {
+    case NET_EVPN_MAC:
+      evpn_receive_mac(p, (const net_addr_evpn_mac *) n, new);
+      return;
+
+    case NET_EVPN_IMET:
+      evpn_receive_imet(p, (const net_addr_evpn_imet *) n, new);;
+      return;
+    }
+    return;
+
+  case NET_MPLS:
+    return;
+  }
+}
+
+
+static int
+evpn_preexport(struct channel *C, rte *e)
+{
+  struct evpn_proto *p = (void *) C->proto;
+  struct proto *pp = e->sender->proto;
+  const net_addr *n = e->net->n.addr;
+
+  if (pp == C->proto)
+    return -1; /* Avoid local loops automatically */
+
+  switch (n->type)
+  {
+  case NET_ETH:
+    if (((const net_addr_eth *) n)->vid != p->vid)
+      return -1;
+
+    return 0;
+
+  case NET_EVPN:
+    return evpn_import_targets(p, ea_get_adata(e->attrs->eattrs, 
EA_BGP_EXT_COMMUNITY)) ? 0 : -1;
+
+  case NET_MPLS:
+    return -1;
+
+  default:
+    bug("invalid type");
+  }
+}
+
+static void
+evpn_reload_routes(struct channel *C)
+{
+  struct evpn_proto *p = (void *) C->proto;
+
+  /* Route reload on one channel is just refeed on the other */
+  switch (C->net_type)
+  {
+  case NET_ETH:
+    channel_request_feeding(p->evpn_channel);
+    break;
+
+  case NET_EVPN:
+    channel_request_feeding(p->eth_channel);
+    break;
+
+  case NET_MPLS:
+    channel_request_feeding(p->eth_channel);
+    break;
+  }
+}
+
+static inline u32
+evpn_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
+evpn_rte_better(rte *new, rte *old)
+{
+  /* This is hack, we should have full BGP-style comparison */
+  return evpn_metric(new) < evpn_metric(old);
+}
+
+static void
+evpn_postconfig(struct proto_config *CF)
+{
+  struct evpn_config *cf = (void *) CF;
+
+  if (!proto_cf_find_channel(CF, NET_ETH))
+    cf_error("Ethernet channel not specified");
+
+  if (!proto_cf_find_channel(CF, NET_EVPN))
+    cf_error("EVPN channel not specified");
+
+//  if (!proto_cf_find_channel(CF, NET_MPLS))
+//    cf_error("MPLS channel not specified");
+
+  if (!cf->rd)
+    cf_error("Route distinguisher not specified");
+
+  if (!cf->import_target && !cf->export_target)
+    cf_error("Route target not specified");
+
+  if (!cf->import_target)
+    cf_error("Import target not specified");
+
+  if (!cf->export_target)
+    cf_error("Export target not specified");
+}
+
+static struct proto *
+evpn_init(struct proto_config *CF)
+{
+  struct proto *P = proto_new(CF);
+  struct evpn_proto *p = (void *) P;
+  // struct evpn_config *cf = (void *) CF;
+
+  proto_configure_channel(P, &p->eth_channel, proto_cf_find_channel(CF, 
NET_ETH));
+  proto_configure_channel(P, &p->evpn_channel, proto_cf_find_channel(CF, 
NET_EVPN));
+  proto_configure_channel(P, &P->mpls_channel, proto_cf_find_channel(CF, 
NET_MPLS));
+
+  P->rt_notify = evpn_rt_notify;
+  P->preexport = evpn_preexport;
+  P->reload_routes = evpn_reload_routes;
+  P->rte_better = evpn_rte_better;
+
+  return P;
+}
+
+static int
+evpn_start(struct proto *P)
+{
+  struct evpn_proto *p = (void *) P;
+  struct evpn_config *cf = (void *) P->cf;
+
+  p->rd = cf->rd;
+  p->import_target = cf->import_target;
+  p->export_target = cf->export_target;
+  p->export_target_data = NULL;
+
+  p->tunnel_dev = cf->tunnel_dev;
+  p->router_addr = cf->router_addr;
+  p->vni = cf->vni;
+  p->vid = cf->vid;
+
+  evpn_prepare_import_targets(p);
+  evpn_prepare_export_targets(p);
+
+  proto_setup_mpls_map(P, RTS_EVPN, 1);
+
+  // XXX ?
+  if (P->vrf_set)
+    P->mpls_map->vrf_iface = P->vrf;
+
+  proto_notify_state(P, PS_UP);
+
+  evpn_announce_imet(p, 1);
+
+  return PS_UP;
+}
+
+static int
+evpn_shutdown(struct proto *P)
+{
+  // struct evpn_proto *p = (void *) P;
+
+  proto_shutdown_mpls_map(P, 1);
+
+  return PS_DOWN;
+}
+
+static int
+evpn_reconfigure(struct proto *P, struct proto_config *CF)
+{
+  struct evpn_proto *p = (void *) P;
+  struct evpn_config *cf = (void *) CF;
+
+  if (!proto_configure_channel(P, &p->eth_channel, proto_cf_find_channel(CF, 
NET_ETH)) ||
+      !proto_configure_channel(P, &p->evpn_channel, proto_cf_find_channel(CF, 
NET_EVPN)) ||
+      !proto_configure_channel(P, &P->mpls_channel, proto_cf_find_channel(CF, 
NET_MPLS)))
+    return 0;
+
+  if ((p->rd != cf->rd) ||
+      (p->tunnel_dev != cf->tunnel_dev) ||
+      (!ipa_equal(p->router_addr, cf->router_addr)) ||
+      (p->vni != cf->vni) ||
+      (p->vid != cf->vid))
+    return 0;
+
+  int import_changed = !same_tree(p->import_target, cf->import_target);
+  int export_changed = !same_tree(p->export_target, cf->export_target);
+
+  /* Update pointers to config structures */
+  p->import_target = cf->import_target;
+  p->export_target = cf->export_target;
+
+  proto_setup_mpls_map(P, RTS_EVPN, 1);
+
+  if (import_changed)
+  {
+    TRACE(D_EVENTS, "Import target changed");
+
+    evpn_prepare_import_targets(p);
+
+    if (p->evpn_channel && (p->evpn_channel->channel_state == CS_UP))
+      channel_request_feeding(p->evpn_channel);
+  }
+
+  if (export_changed)
+  {
+    TRACE(D_EVENTS, "Export target changed");
+
+    evpn_prepare_export_targets(p);
+
+    if (p->eth_channel && (p->eth_channel->channel_state == CS_UP))
+      channel_request_feeding(p->eth_channel);
+  }
+
+  return 1;
+}
+
+static void
+evpn_copy_config(struct proto_config *dest UNUSED, struct proto_config *src 
UNUSED)
+{
+  /* Just a shallow copy, not many items here */
+}
+
+/*
+static void
+evpn_get_route_info(rte *rte, byte *buf)
+{
+  u32 metric = evpn_metric(rte);
+  if (metric < IGP_METRIC_UNKNOWN)
+    bsprintf(buf, " (%u/%u)", rte->attrs->pref, metric);
+  else
+    bsprintf(buf, " (%u/?)", rte->attrs->pref);
+}
+*/
+
+
+struct protocol proto_evpn = {
+  .name =              "EVPN",
+  .template =          "evpn%d",
+  .class =             PROTOCOL_EVPN,
+  .channel_mask =      NB_ETH | NB_EVPN | NB_MPLS,
+  .proto_size =                sizeof(struct evpn_proto),
+  .config_size =       sizeof(struct evpn_config),
+  .postconfig =                evpn_postconfig,
+  .init =              evpn_init,
+  .start =             evpn_start,
+  .shutdown =          evpn_shutdown,
+  .reconfigure =       evpn_reconfigure,
+  .copy_config =       evpn_copy_config,
+//  .get_route_info =  evpn_get_route_info
+};
+
+void
+evpn_build(void)
+{
+  proto_build(&proto_evpn);
+}
diff --git a/proto/evpn/evpn.h b/proto/evpn/evpn.h
new file mode 100644
index 00000000..e511cd0f
--- /dev/null
+++ b/proto/evpn/evpn.h
@@ -0,0 +1,44 @@
+/*
+ *     BIRD -- BGP/MPLS Ethernet Virtual Private Networks (EVPN)
+ *
+ *     (c) 2023 Ondrej Zajicek <[email protected]>
+ *     (c) 2023 CZ.NIC z.s.p.o.
+ *
+ *     Can be freely distributed and used under the terms of the GNU GPL.
+ */
+
+#ifndef _BIRD_EVPN_H_
+#define _BIRD_EVPN_H_
+
+struct evpn_config {
+  struct proto_config c;
+
+  u64 rd;
+  struct f_tree *import_target;
+  struct f_tree *export_target;
+
+  struct iface *tunnel_dev;
+  ip_addr router_addr;
+  u32 vni;
+  u32 vid;
+};
+
+struct evpn_proto {
+  struct proto p;
+  struct channel *eth_channel;
+  struct channel *evpn_channel;
+
+  u64 rd;
+  struct f_tree *import_target;
+  struct f_tree *export_target;
+  u32 *export_target_data;
+  uint export_target_length;
+  uint import_target_one;
+
+  struct iface *tunnel_dev;
+  ip_addr router_addr;
+  u32 vni;
+  u32 vid;
+};
+
+#endif
diff --git a/sysdep/linux/netlink.c b/sysdep/linux/netlink.c
index 8be5112c..55f45a73 100644
--- a/sysdep/linux/netlink.c
+++ b/sysdep/linux/netlink.c
@@ -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)

Reply via email to