[PATCH v4 7/8] babel: Add MAC authentication support

Toke Høiland-Jørgensen toke at toke.dk
Fri Jan 15 16:52:57 CET 2021


From: Toke Høiland-Jørgensen <toke at toke.dk>

This implements support for MAC authentication in the Babel protocol, as
specified by RFC8967. The implementation seeks to follow the RFC as close
as possible, with the only deliberate deviation being the addition of
support for all the HMAC algorithms already supported by Bird, as well as
the Blake2b variant of the Blake algorithm.

The RFC describes the applicability of the MAC authentication scheme as
follows:

1.1.  Applicability

   The protocol defined in this document assumes that all interfaces on
   a given link are equally trusted and share a small set of symmetric
   keys (usually just one, and two during key rotation).  The protocol
   is inapplicable in situations where asymmetric keying is required,
   where the trust relationship is partial, or where large numbers of
   trusted keys are provisioned on a single link at the same time.

   This protocol supports incremental deployment (where an insecure
   Babel network is made secure with no service interruption), and it
   supports graceful key rotation (where the set of keys is changed with
   no service interruption).

   This protocol does not require synchronised clocks, it does not
   require persistently monotonic clocks, and it does not require
   persistent storage except for what might be required for storing
   cryptographic keys.

1.2.  Assumptions and Security Properties

   The correctness of the protocol relies on the following assumptions:

   *  that the Message Authentication Code (MAC) being used is
      invulnerable to forgery, i.e., that an attacker is unable to
      generate a packet with a correct MAC without access to the secret
      key;

   *  that a node never generates the same index or nonce twice over the
      lifetime of a key.

   The first assumption is a property of the MAC being used.  The second
   assumption can be met either by using a robust random number
   generator [RFC4086] and sufficiently large indices and nonces, by
   using a reliable hardware clock, or by rekeying often enough that
   collisions are unlikely.

   If the assumptions above are met, the protocol described in this
   document has the following properties:

   *  it is invulnerable to spoofing: any Babel packet accepted as
      authentic is the exact copy of a packet originally sent by an
      authorised node;

   *  locally to a single node, it is invulnerable to replay: if a node
      has previously accepted a given packet, then it will never again
      accept a copy of this packet or an earlier packet from the same
      sender;

   *  among different nodes, it is only vulnerable to immediate replay:
      if a node A has accepted an authentic packet from C, then a node B
      will only accept a copy of that packet if B has accepted an older
      packet from C, and B has received no later packet from C.

   While this protocol makes efforts to mitigate the effects of a denial
   of service attack, it does not fully protect against such attacks.

Signed-off-by: Toke Høiland-Jørgensen <toke at toke.dk>
---
 doc/bird.sgml         |   36 +++-
 lib/mac.h             |    3 
 proto/babel/Makefile  |    2 
 proto/babel/babel.c   |  156 +++++++++++++++
 proto/babel/babel.h   |   66 ++++++-
 proto/babel/config.Y  |   42 ++++
 proto/babel/packets.c |  496 ++++++++++++++++++++++++++++++++++++++++++++++++-
 7 files changed, 775 insertions(+), 26 deletions(-)

diff --git a/doc/bird.sgml b/doc/bird.sgml
index da109dbba3a2..451104100104 100644
--- a/doc/bird.sgml
+++ b/doc/bird.sgml
@@ -819,8 +819,8 @@ agreement").
 	<tag><label id="proto-pass-algorithm">algorithm ( keyed md5 | keyed sha1 | hmac sha1 | hmac sha256 | hmac sha384 | hmac sha512 | blake2s128 | blake2s256 | blake2b256 | blake2b512 )</tag>
 	The message authentication algorithm for the password when cryptographic
 	authentication is enabled. The default value depends on the protocol.
-	For RIP and OSPFv2 it is Keyed-MD5 (for compatibility), for OSPFv3
-	protocol it is HMAC-SHA-256.
+	For RIP and OSPFv2 it is Keyed-MD5 (for compatibility), for OSPFv3 and
+	Babel it is HMAC-SHA-256.
 
 </descrip>
 
@@ -1776,6 +1776,19 @@ protocol babel [<name>] {
 		check link <switch>;
 		next hop ipv4 <address>;
 		next hop ipv6 <address>;
+		authentication none|mac [permissive];
+		password "<text>";
+		password "<text>" {
+			id <num>;
+			generate from "<date>";
+			generate to "<date>";
+			accept from "<date>";
+			accept to "<date>";
+			from "<date>";
+			to "<date>";
+			algorithm ( hmac sha1 | hmac sha256 | hmac sha384 | hmac
+	sha512 | blake2s | blake2b );
+		};
 	};
 }
 </code>
@@ -1866,6 +1879,25 @@ protocol babel [<name>] {
       interface. If not set, the same link-local address that is used as the
       source for Babel packets will be used. In normal operation, it should not
       be necessary to set this option.
+
+      <tag><label id="babel-authentication">authentication none|mac [permissive]</tag>
+      Selects authentication method to be used. <cf/none/ means that packets
+      are not authenticated at all, <cf/mac/ means MAC authentication is
+      performed as described in <rfc id="8967">. If MAC authentication is
+      selected, the <cf/permissive/ suffix can be used to select an operation
+      mode where outgoing packets are signed, but incoming packets will be
+      accepted even if they fail authentication. This can be useful for
+      incremental deployment of MAC authentication across a network. If MAC
+      authentication is selected, a key must be specified with the
+      <cf/password/ configuration option. Default: none.
+
+      <tag><label id="babel-password">password "<m/text/"</tag> Specifies a
+      password used for authentication. See the <ref id="proto-pass"
+      name="password"> common option for a detailed description. The Babel
+      protocol will only accept HMAC-based algorithms or one of the Blake
+      algorithms, and the length of the supplied password string must match the
+      key size used by the selected algorithm.
+
 </descrip>
 
 <sect1>Attributes
diff --git a/lib/mac.h b/lib/mac.h
index 56653f8ac38a..5e224ac1066b 100644
--- a/lib/mac.h
+++ b/lib/mac.h
@@ -103,6 +103,9 @@ static inline const char *mac_type_name(uint id)
 static inline uint mac_type_length(uint id)
 { return mac_table[id].mac_length; }
 
+static inline uint mac_type_block_size(uint id)
+{ return mac_table[id].block_size; }
+
 void mac_type_validate_key(uint id, const byte *key, uint keylen);
 
 static inline const char *mac_get_name(struct mac_context *ctx)
diff --git a/proto/babel/Makefile b/proto/babel/Makefile
index a5b4a13b2a8b..06b58e95b955 100644
--- a/proto/babel/Makefile
+++ b/proto/babel/Makefile
@@ -3,4 +3,4 @@ obj := $(src-o-files)
 $(all-daemon)
 $(cf-local)
 
-tests_objs := $(tests_objs) $(src-o-files)
\ No newline at end of file
+tests_objs := $(tests_objs) $(src-o-files)
diff --git a/proto/babel/babel.c b/proto/babel/babel.c
index 4b6b9d7f9f6f..f5f956ed4e26 100644
--- a/proto/babel/babel.c
+++ b/proto/babel/babel.c
@@ -38,6 +38,8 @@
 #include <stdlib.h>
 #include "babel.h"
 
+#define LOG_PKT_AUTH(msg, args...) \
+  log_rl(&p->log_pkt_tbf, L_AUTH "%s: " msg, p->p.name, args)
 
 /*
  * Is one number greater or equal than another mod 2^16? This is based on the
@@ -55,6 +57,7 @@ static void babel_send_seqno_request(struct babel_proto *p, struct babel_entry *
 static void babel_update_cost(struct babel_neighbor *n);
 static inline void babel_kick_timer(struct babel_proto *p);
 static inline void babel_iface_kick_timer(struct babel_iface *ifa);
+static void babel_auth_init_neighbor(struct babel_neighbor *n);
 
 static inline void babel_lock_neighbor(struct babel_neighbor *nbr)
 { if (nbr) nbr->uc++; }
@@ -429,6 +432,7 @@ babel_get_neighbor(struct babel_iface *ifa, ip_addr addr)
   init_list(&nbr->routes);
   babel_lock_neighbor(nbr);
   add_tail(&ifa->neigh_list, NODE nbr);
+  babel_auth_init_neighbor(nbr);
 
   return nbr;
 }
@@ -503,11 +507,13 @@ babel_expire_neighbors(struct babel_proto *p)
 
       if (nbr->hello_expiry && nbr->hello_expiry <= now_)
         babel_expire_hello(p, nbr, now_);
+
+      if (nbr->auth_expiry && nbr->auth_expiry <= now_)
+        babel_flush_neighbor(p, nbr);
     }
   }
 }
 
-
 /*
  *	Best route selection
  */
@@ -1382,6 +1388,127 @@ babel_handle_seqno_request(union babel_msg *m, struct babel_iface *ifa)
   }
 }
 
+/*
+ *      Authentication functions
+ */
+
+/**
+ * babel_auth_reset_index - Reset authentication index on interface
+ * @ifa: Interface to reset
+ *
+ * This function resets the authentication index and packet counter for an
+ * interface, and should be called on interface configuration, or when the
+ * packet counter overflows.
+ */
+void
+babel_auth_reset_index(struct babel_iface *ifa)
+{
+  random_bytes(ifa->auth_index, BABEL_AUTH_INDEX_LEN);
+  ifa->auth_pc = 1;
+}
+
+/**
+ * babel_auth_init_neighbor - Initialise authentication data for neighbor
+ * @n: Neighbor to initialise
+ *
+ * This function initialises the authentication-related state for a new neighbor
+ * that has just been created.
+ */
+void
+babel_auth_init_neighbor(struct babel_neighbor *n)
+{
+  if (n->ifa->cf->auth_type != BABEL_AUTH_NONE)
+    n->auth_expiry = current_time() + BABEL_AUTH_NEIGHBOR_TIMEOUT;
+}
+
+static void
+babel_auth_send_challenge(struct babel_iface *ifa, struct babel_neighbor *n)
+{
+  struct babel_proto *p = ifa->proto;
+  union babel_msg msg = {};
+
+  TRACE(D_PACKETS, "Sending AUTH challenge to %I on %s",
+	n->addr, ifa->ifname);
+
+  random_bytes(n->auth_nonce, BABEL_AUTH_NONCE_LEN);
+  n->auth_nonce_expiry = current_time() + BABEL_AUTH_CHALLENGE_TIMEOUT;
+  n->auth_next_challenge = current_time() + BABEL_AUTH_CHALLENGE_INTERVAL;
+
+  msg.type = BABEL_TLV_CHALLENGE_REQ;
+  msg.challenge.nonce_len = BABEL_AUTH_NONCE_LEN;
+  msg.challenge.nonce = n->auth_nonce;
+
+  babel_send_unicast(&msg, ifa, n->addr);
+}
+
+int
+babel_auth_check_pc(struct babel_iface *ifa, struct babel_msg_auth *msg)
+{
+  struct babel_proto *p = ifa->proto;
+  struct babel_neighbor *n;
+
+  TRACE(D_PACKETS, "Handling MAC check from %I on %s",
+        msg->sender,  ifa->ifname);
+
+  /* We create the neighbour entry at this point because it makes it easier to
+   * rate limit challenge replies; this is explicitly allowed by the spec (see
+   *  Section 4.3).
+   */
+  n = babel_get_neighbor(ifa, msg->sender);
+
+  if (msg->challenge_seen && n->auth_next_challenge_reply <= current_time())
+  {
+    union babel_msg resp = {};
+    TRACE(D_PACKETS, "Sending MAC challenge response to %I", msg->sender);
+    resp.type = BABEL_TLV_CHALLENGE_REPLY;
+    resp.challenge.nonce_len = msg->challenge_len;
+    resp.challenge.nonce = msg->challenge;
+    n->auth_next_challenge_reply = current_time() + BABEL_AUTH_CHALLENGE_INTERVAL;
+    babel_send_unicast(&resp, ifa, msg->sender);
+  }
+
+  if (msg->index_len > BABEL_AUTH_INDEX_LEN || !msg->pc_seen)
+  {
+    LOG_PKT_AUTH("Invalid index or no PC from %I on %s",
+                 msg->sender, ifa->ifname);
+    return 1;
+  }
+
+  /* On successful challenge, update PC and index to current values */
+  if (msg->challenge_reply_seen &&
+      n->auth_nonce_expiry &&
+      n->auth_nonce_expiry >= current_time() &&
+      !memcmp(msg->challenge_reply, n->auth_nonce, BABEL_AUTH_NONCE_LEN))
+  {
+    n->auth_index_len = msg->index_len;
+    memcpy(n->auth_index, msg->index, msg->index_len);
+    n->auth_pc = msg->pc;
+  }
+
+  /* If index differs, send challenge */
+  if ((n->auth_index_len != msg->index_len ||
+      memcmp(n->auth_index, msg->index, msg->index_len)) &&
+      n->auth_next_challenge <= current_time())
+  {
+    LOG_PKT_AUTH("Index mismatch from %I on %s; sending challenge",
+                 msg->sender, ifa->ifname);
+    babel_auth_send_challenge(ifa, n);
+    return 1;
+  }
+
+  /* Index matches; only accept if PC is greater than last */
+  if (n->auth_pc >= msg->pc)
+  {
+    LOG_PKT_AUTH("Packet counter too low from %I on %s",
+                 msg->sender, ifa->ifname);
+    return 1;
+  }
+
+  n->auth_pc = msg->pc;
+  n->auth_expiry = current_time() + BABEL_AUTH_NEIGHBOR_TIMEOUT;
+  n->auth_passed = 1;
+  return 0;
+}
 
 /*
  *	Babel interfaces
@@ -1550,6 +1677,8 @@ babel_iface_update_buffers(struct babel_iface *ifa)
   sk_set_tbsize(ifa->sk, tbsize);
 
   ifa->tx_length = tbsize - BABEL_OVERHEAD;
+
+  babel_auth_set_tx_overhead(ifa);
 }
 
 static struct babel_iface*
@@ -1609,6 +1738,9 @@ babel_add_iface(struct babel_proto *p, struct iface *new, struct babel_iface_con
   init_list(&ifa->neigh_list);
   ifa->hello_seqno = 1;
 
+  if (ic->auth_type != BABEL_AUTH_NONE)
+    babel_auth_reset_index(ifa);
+
   ifa->timer = tm_new_init(ifa->pool, babel_iface_timer, ifa, 0, 0);
 
   init_list(&ifa->msg_queue);
@@ -1705,6 +1837,9 @@ babel_reconfigure_iface(struct babel_proto *p, struct babel_iface *ifa, struct b
   ifa->next_hop_ip4 = ipa_nonzero(new->next_hop_ip4) ? new->next_hop_ip4 : addr4;
   ifa->next_hop_ip6 = ipa_nonzero(new->next_hop_ip6) ? new->next_hop_ip6 : ifa->addr;
 
+  if (new->auth_type != BABEL_AUTH_NONE && old->auth_type != new->auth_type)
+    babel_auth_reset_index(ifa);
+
   if (ipa_zero(ifa->next_hop_ip4) && p->ip4_channel)
     log(L_WARN "%s: Missing IPv4 next hop address for %s", p->p.name, ifa->ifname);
 
@@ -1894,8 +2029,8 @@ babel_show_interfaces(struct proto *P, const char *iff)
   }
 
   cli_msg(-1023, "%s:", p->p.name);
-  cli_msg(-1023, "%-10s %-6s %7s %6s %7s %-15s %s",
-	  "Interface", "State", "RX cost", "Nbrs", "Timer",
+  cli_msg(-1023, "%-10s %-6s %-5s %7s %6s %7s %-15s %s",
+	  "Interface", "State", "Auth", "RX cost", "Nbrs", "Timer",
 	  "Next hop (v4)", "Next hop (v6)");
 
   WALK_LIST(ifa, p->interfaces)
@@ -1908,8 +2043,10 @@ babel_show_interfaces(struct proto *P, const char *iff)
 	nbrs++;
 
     btime timer = MIN(ifa->next_regular, ifa->next_hello) - current_time();
-    cli_msg(-1023, "%-10s %-6s %7u %6u %7t %-15I %I",
+    cli_msg(-1023, "%-10s %-6s %-5s %7u %6u %7t %-15I %I",
 	    ifa->iface->name, (ifa->up ? "Up" : "Down"),
+            (ifa->cf->auth_type == BABEL_AUTH_MAC ?
+             (ifa->cf->auth_permissive ? "Perm" : "Yes") : "No"),
 	    ifa->cf->rxcost, nbrs, MAX(timer, 0),
 	    ifa->next_hop_ip4, ifa->next_hop_ip6);
   }
@@ -1930,8 +2067,8 @@ babel_show_neighbors(struct proto *P, const char *iff)
   }
 
   cli_msg(-1024, "%s:", p->p.name);
-  cli_msg(-1024, "%-25s %-10s %6s %6s %6s %7s",
-	  "IP address", "Interface", "Metric", "Routes", "Hellos", "Expires");
+  cli_msg(-1024, "%-25s %-10s %6s %6s %6s %7s %4s",
+	  "IP address", "Interface", "Metric", "Routes", "Hellos", "Expires", "Auth");
 
   WALK_LIST(ifa, p->interfaces)
   {
@@ -1945,9 +2082,10 @@ babel_show_neighbors(struct proto *P, const char *iff)
         rts++;
 
       uint hellos = u32_popcount(n->hello_map);
-      btime timer = n->hello_expiry - current_time();
-      cli_msg(-1024, "%-25I %-10s %6u %6u %6u %7t",
-	      n->addr, ifa->iface->name, n->cost, rts, hellos, MAX(timer, 0));
+      btime timer = (n->hello_expiry ?: n->auth_expiry) - current_time();
+      cli_msg(-1024, "%-25I %-10s %6u %6u %6u %7t %-4s",
+	      n->addr, ifa->iface->name, n->cost, rts, hellos, MAX(timer, 0),
+              n->auth_passed ? "Yes" : "No");
     }
   }
 }
diff --git a/proto/babel/babel.h b/proto/babel/babel.h
index e075024cb4dd..915c25999fe2 100644
--- a/proto/babel/babel.h
+++ b/proto/babel/babel.h
@@ -19,6 +19,7 @@
 #include "nest/route.h"
 #include "nest/protocol.h"
 #include "nest/locks.h"
+#include "nest/password.h"
 #include "lib/resource.h"
 #include "lib/lists.h"
 #include "lib/socket.h"
@@ -60,6 +61,14 @@
 #define BABEL_OVERHEAD		(IP6_HEADER_LENGTH+UDP_HEADER_LENGTH)
 #define BABEL_MIN_MTU		(512 + BABEL_OVERHEAD)
 
+#define BABEL_AUTH_NONE		0
+#define BABEL_AUTH_MAC			1
+#define BABEL_AUTH_NONCE_LEN		10	/* we send 80 bit nonces */
+#define BABEL_AUTH_MAX_NONCE_LEN	192	/* max allowed by spec */
+#define BABEL_AUTH_INDEX_LEN		32	/* max size in spec */
+#define BABEL_AUTH_NEIGHBOR_TIMEOUT	(300 S_)
+#define BABEL_AUTH_CHALLENGE_TIMEOUT	(30 S_)
+#define BABEL_AUTH_CHALLENGE_INTERVAL	(300 MS_) /* used for both challenges and replies */
 
 enum babel_tlv_type {
   BABEL_TLV_PAD1		= 0,
@@ -73,13 +82,10 @@ enum babel_tlv_type {
   BABEL_TLV_UPDATE		= 8,
   BABEL_TLV_ROUTE_REQUEST	= 9,
   BABEL_TLV_SEQNO_REQUEST	= 10,
-  /* extensions - not implemented
-  BABEL_TLV_TS_PC		= 11,
-  BABEL_TLV_HMAC		= 12,
-  BABEL_TLV_SS_UPDATE		= 13,
-  BABEL_TLV_SS_REQUEST		= 14,
-  BABEL_TLV_SS_SEQNO_REQUEST	= 15,
-  */
+  BABEL_TLV_MAC		= 16,
+  BABEL_TLV_PC			= 17,
+  BABEL_TLV_CHALLENGE_REQ	= 18,
+  BABEL_TLV_CHALLENGE_REPLY	= 19,
   BABEL_TLV_MAX
 };
 
@@ -137,6 +143,12 @@ struct babel_iface_config {
 
   ip_addr next_hop_ip4;
   ip_addr next_hop_ip6;
+
+  u8 auth_type;			/* Authentication type (BABEL_AUTH_*) */
+  u8 auth_permissive;			/* Don't drop packets failing auth check */
+  uint mac_num_keys;			/* Number of configured HMAC keys */
+  uint mac_total_len;			/* Total digest length for all configured keys */
+  list *passwords;			/* Passwords for authentication */
 };
 
 struct babel_proto {
@@ -184,6 +196,10 @@ struct babel_iface {
 
   u16 hello_seqno;			/* To be increased on each hello */
 
+  u32 auth_pc;
+  int auth_tx_overhead;
+  u8 auth_index[BABEL_AUTH_INDEX_LEN];
+
   btime next_hello;
   btime next_regular;
   btime next_triggered;
@@ -207,9 +223,20 @@ struct babel_neighbor {
   u16 hello_map;
   u16 next_hello_seqno;
   uint last_hello_int;
+
+  u32 auth_pc;
+  u8 auth_passed;
+  u8 auth_index_len;
+  u8 auth_index[BABEL_AUTH_INDEX_LEN];
+  u8 auth_nonce[BABEL_AUTH_NONCE_LEN];
+  btime auth_nonce_expiry;
+  btime auth_next_challenge;
+  btime auth_next_challenge_reply;
+
   /* expiry timers */
   btime hello_expiry;
   btime ihu_expiry;
+  btime auth_expiry;
 
   list routes;				/* Routes this neighbour has sent us (struct babel_route) */
 };
@@ -339,6 +366,12 @@ struct babel_msg_seqno_request {
   ip_addr sender;
 };
 
+struct babel_msg_challenge {
+  u8 type;
+  u8 nonce_len;
+  u8 *nonce;
+};
+
 union babel_msg {
   u8 type;
   struct babel_msg_ack_req ack_req;
@@ -348,6 +381,7 @@ union babel_msg {
   struct babel_msg_update update;
   struct babel_msg_route_request route_request;
   struct babel_msg_seqno_request seqno_request;
+  struct babel_msg_challenge challenge;
 };
 
 struct babel_msg_node {
@@ -355,6 +389,20 @@ struct babel_msg_node {
   union babel_msg msg;
 };
 
+/* only used for auth checking, so not a part of union above */
+struct babel_msg_auth {
+  ip_addr sender;
+  u32 pc;
+  u8 pc_seen;
+  u8 index_len;
+  u8 *index;
+  u8 challenge_reply_seen;
+  u8 challenge_reply[BABEL_AUTH_NONCE_LEN];
+  u8 challenge_seen;
+  u8 challenge_len;
+  u8 challenge[BABEL_AUTH_MAX_NONCE_LEN];
+};
+
 static inline int babel_sadr_enabled(struct babel_proto *p)
 { return p->ip6_rtable.addr_type == NET_IP6_SADR; }
 
@@ -373,11 +421,15 @@ void babel_show_neighbors(struct proto *P, const char *iff);
 void babel_show_entries(struct proto *P);
 void babel_show_routes(struct proto *P);
 
+void babel_auth_reset_index(struct babel_iface *ifa);
+int babel_auth_check_pc(struct babel_iface *ifa, struct babel_msg_auth *msg);
+
 /* packets.c */
 void babel_enqueue(union babel_msg *msg, struct babel_iface *ifa);
 void babel_send_unicast(union babel_msg *msg, struct babel_iface *ifa, ip_addr dest);
 int babel_open_socket(struct babel_iface *ifa);
 void babel_send_queue(void *arg);
+void babel_auth_set_tx_overhead(struct babel_iface *ifa);
 
 
 #endif
diff --git a/proto/babel/config.Y b/proto/babel/config.Y
index 2f3b637b4f6c..5e0710b5b759 100644
--- a/proto/babel/config.Y
+++ b/proto/babel/config.Y
@@ -25,7 +25,7 @@ CF_DECLS
 CF_KEYWORDS(BABEL, INTERFACE, METRIC, RXCOST, HELLO, UPDATE, INTERVAL, PORT,
 	TYPE, WIRED, WIRELESS, RX, TX, BUFFER, PRIORITY, LENGTH, CHECK, LINK,
 	NEXT, HOP, IPV4, IPV6, BABEL_METRIC, SHOW, INTERFACES, NEIGHBORS,
-	ENTRIES, RANDOMIZE, ROUTER, ID)
+	ENTRIES, RANDOMIZE, ROUTER, ID, AUTHENTICATION, NONE, MAC, PERMISSIVE)
 
 CF_GRAMMAR
 
@@ -59,6 +59,8 @@ babel_iface_start:
   this_ipatt = cfg_allocz(sizeof(struct babel_iface_config));
   add_tail(&BABEL_CFG->iface_list, NODE this_ipatt);
   init_list(&this_ipatt->ipn_list);
+  reset_passwords();
+
   BABEL_IFACE->port = BABEL_PORT;
   BABEL_IFACE->type = BABEL_IFACE_TYPE_WIRED;
   BABEL_IFACE->limit = BABEL_HELLO_LIMIT;
@@ -91,6 +93,40 @@ babel_iface_finish:
   BABEL_IFACE->ihu_interval = MIN_(BABEL_IFACE->hello_interval*BABEL_IHU_INTERVAL_FACTOR, BABEL_MAX_INTERVAL);
 
   BABEL_CFG->hold_time = MAX_(BABEL_CFG->hold_time, BABEL_IFACE->update_interval*BABEL_HOLD_TIME_FACTOR);
+
+  BABEL_IFACE->passwords = get_passwords();
+
+  if (!BABEL_IFACE->auth_type != !BABEL_IFACE->passwords)
+    cf_error("Authentication and password options should be used together");
+
+  if (BABEL_IFACE->passwords)
+  {
+    struct password_item *pass;
+    uint len = 0, i = 0;
+    WALK_LIST(pass, *BABEL_IFACE->passwords)
+    {
+      /* Set default crypto algorithm (HMAC-SHA256) */
+      if (!pass->alg)
+	pass->alg = ALG_HMAC_SHA256;
+
+      if (pass->alg & ALG_HMAC) {
+        if (pass->length < mac_type_length(pass->alg) ||
+            pass->length > mac_type_block_size(pass->alg))
+          cf_error("key length %d is not between output size %d and block size %d for algorithm %s",
+                   pass->length, mac_type_length(pass->alg),
+                   mac_type_block_size(pass->alg), mac_type_name(pass->alg));
+      } else if (!(pass->alg == ALG_BLAKE2S_128 || pass->alg == ALG_BLAKE2S_256 ||
+                   pass->alg == ALG_BLAKE2B_256 || pass->alg == ALG_BLAKE2B_512)) {
+	cf_error("Only HMAC and Blake algorithms are supported");
+      }
+
+      len += mac_type_length(pass->alg);
+      i++;
+    }
+    BABEL_IFACE->mac_num_keys = i;
+    BABEL_IFACE->mac_total_len = len;
+  }
+
 };
 
 
@@ -109,6 +145,10 @@ babel_iface_item:
  | CHECK LINK bool { BABEL_IFACE->check_link = $3; }
  | NEXT HOP IPV4 ipa { BABEL_IFACE->next_hop_ip4 = $4; if (!ipa_is_ip4($4)) cf_error("Must be an IPv4 address"); }
  | NEXT HOP IPV6 ipa { BABEL_IFACE->next_hop_ip6 = $4; if (!ipa_is_ip6($4)) cf_error("Must be an IPv6 address"); }
+ | AUTHENTICATION NONE { BABEL_IFACE->auth_type = BABEL_AUTH_NONE; }
+ | AUTHENTICATION MAC { BABEL_IFACE->auth_type = BABEL_AUTH_MAC; }
+ | AUTHENTICATION MAC PERMISSIVE { BABEL_IFACE->auth_type = BABEL_AUTH_MAC; BABEL_IFACE->auth_permissive = 1; }
+ | password_list	{ }
  ;
 
 babel_iface_opts:
diff --git a/proto/babel/packets.c b/proto/babel/packets.c
index e32d1bd1783e..77175b0f221b 100644
--- a/proto/babel/packets.c
+++ b/proto/babel/packets.c
@@ -11,7 +11,7 @@
  */
 
 #include "babel.h"
-
+#include "lib/mac.h"
 
 struct babel_pkt_header {
   u8 magic;
@@ -112,6 +112,31 @@ struct babel_subtlv_source_prefix {
   u8 addr[0];
 } PACKED;
 
+struct babel_tlv_pc {
+  u8 type;
+  u8 length;
+  u32 pc;
+  u8 index[0];
+} PACKED;
+
+struct babel_tlv_mac {
+  u8 type;
+  u8 length;
+  u8 mac[0];
+} PACKED;
+
+struct babel_tlv_challenge {
+  u8 type;
+  u8 length;
+  u8 nonce[0];
+} PACKED;
+
+struct babel_mac_pseudohdr {
+  u8 src_addr[16];
+  u16 src_port;
+  u8 dst_addr[16];
+  u16 dst_port;
+} PACKED;
 
 /* Hello flags */
 #define BABEL_HF_UNICAST	0x8000
@@ -146,6 +171,9 @@ struct babel_parse_state {
   u8 def_ip4_prefix_seen;	/* def_ip4_prefix is valid */
   u8 current_tlv_endpos;	/* End of self-terminating TLVs (offset from start) */
   u8 sadr_enabled;
+  u8 is_unicast;
+
+  struct babel_msg_auth auth;
 };
 
 enum parse_result {
@@ -168,6 +196,10 @@ struct babel_write_state {
 #define DROP1(DSC) do { err_dsc = DSC; goto drop; } while(0)
 #define LOG_PKT(msg, args...) \
   log_rl(&p->log_pkt_tbf, L_REMOTE "%s: " msg, p->p.name, args)
+#define LOG_WARN(msg, args...) \
+  log_rl(&p->log_pkt_tbf, L_WARN "%s: " msg, p->p.name, args)
+#define LOG_PKT_AUTH(msg, args...) \
+  log_rl(&p->log_pkt_tbf, L_AUTH "%s: " msg, p->p.name, args)
 
 #define FIRST_TLV(p) ((struct babel_tlv *) (((struct babel_pkt_header *) p) + 1))
 #define NEXT_TLV(t) ((struct babel_tlv *) (((byte *) t) + TLV_LENGTH(t)))
@@ -274,6 +306,17 @@ put_ip6_ll(void *p, ip6_addr addr)
   put_u32(p+4, _I3(addr));
 }
 
+/*
+ *      Authentication-related functions
+ */
+uint babel_auth_write_challenge(struct babel_tlv *hdr, union babel_msg *msg, struct babel_write_state *state, uint max_len);
+int babel_auth_add_tlvs(struct babel_iface *ifa, struct babel_tlv *tlv, int max_len);
+int babel_auth_sign(struct babel_iface *ifa, ip_addr dest);
+int babel_auth_check(struct babel_iface *ifa,
+                     ip_addr saddr, u16 sport,
+                     ip_addr daddr, u16 dport,
+                     struct babel_pkt_header *pkt,
+                     byte *start, uint len);
 
 /*
  *	TLV read/write functions
@@ -352,6 +395,16 @@ static const struct babel_tlv_data tlv_data[BABEL_TLV_MAX] = {
     babel_write_seqno_request,
     babel_handle_seqno_request
   },
+  [BABEL_TLV_CHALLENGE_REQ] = {
+    sizeof(struct babel_tlv),
+    NULL,
+    babel_auth_write_challenge,
+  },
+  [BABEL_TLV_CHALLENGE_REPLY] = {
+    sizeof(struct babel_tlv),
+    NULL,
+    babel_auth_write_challenge,
+  },
 };
 
 static const struct babel_tlv_data *get_packet_tlv_data(u8 type)
@@ -1225,7 +1278,6 @@ babel_write_tlv(struct babel_tlv *hdr,
   return tlv_data[msg->type].write_tlv(hdr, msg, state, max_len);
 }
 
-
 /*
  *	Packet RX/TX functions
  */
@@ -1237,6 +1289,8 @@ babel_send_to(struct babel_iface *ifa, ip_addr dest)
   struct babel_pkt_header *hdr = (void *) sk->tbuf;
   int len = get_u16(&hdr->length) + sizeof(struct babel_pkt_header);
 
+  len += babel_auth_sign(ifa, dest);
+
   DBG("Babel: Sending %d bytes to %I\n", len, dest);
   return sk_send_to(sk, len, dest, 0);
 }
@@ -1289,6 +1343,8 @@ babel_write_queue(struct babel_iface *ifa, list *queue)
     sl_free(p->msg_slab, msg);
   }
 
+  pos += babel_auth_add_tlvs(ifa, (struct babel_tlv *) pos, end-pos);
+
   uint plen = pos - (byte *) pkt;
   put_u16(&pkt->length, plen - sizeof(struct babel_pkt_header));
 
@@ -1367,10 +1423,13 @@ babel_enqueue(union babel_msg *msg, struct babel_iface *ifa)
 
 /**
  * babel_process_packet - process incoming data packet
+ * @ifa: Interface packet was received on.
  * @pkt: Pointer to the packet data
  * @len: Length of received packet
  * @saddr: Address of packet sender
- * @ifa: Interface packet was received on.
+ * @sport: Packet source port
+ * @daddr: Destination address of packet
+ * @dport: Packet destination port
  *
  * This function is the main processing hook of incoming Babel packets. It
  * checks that the packet header is well-formed, then processes the TLVs
@@ -1382,8 +1441,10 @@ babel_enqueue(union babel_msg *msg, struct babel_iface *ifa)
  * order.
  */
 static void
-babel_process_packet(struct babel_pkt_header *pkt, int len,
-                     ip_addr saddr, struct babel_iface *ifa)
+babel_process_packet(struct babel_iface *ifa,
+		     struct babel_pkt_header *pkt, int len,
+                     ip_addr saddr, u16 sport,
+		     ip_addr daddr, u16 dport)
 {
   u8 frame_err UNUSED = 0;
   struct babel_proto *p = ifa->proto;
@@ -1422,6 +1483,9 @@ babel_process_packet(struct babel_pkt_header *pkt, int len,
   TRACE(D_PACKETS, "Packet received from %I via %s",
         saddr, ifa->iface->name);
 
+  if (babel_auth_check(ifa, saddr, sport, daddr, dport, pkt, end, len-plen))
+    return;
+
   init_list(&msgs);
 
   /* First pass through the packet TLV by TLV, parsing each into internal data
@@ -1512,7 +1576,10 @@ babel_rx_hook(sock *sk, uint len)
   if (sk->flags & SKF_TRUNCATED)
     DROP("truncated", len);
 
-  babel_process_packet((struct babel_pkt_header *) sk->rbuf, len, sk->faddr, ifa);
+  babel_process_packet(ifa,
+		       (struct babel_pkt_header *) sk->rbuf, len,
+		       sk->faddr, sk->fport,
+		       sk->laddr, sk->dport);
   return 1;
 
 drop:
@@ -1562,3 +1629,420 @@ err:
   rfree(sk);
   return 0;
 }
+
+
+/* Authentication checks */
+static int
+babel_read_pc(struct babel_tlv *hdr, union babel_msg *m UNUSED,
+              struct babel_parse_state *state)
+{
+  struct babel_tlv_pc *tlv = (void *) hdr;
+
+  if (!state->auth.pc_seen)
+  {
+    state->auth.pc_seen = 1;
+    state->auth.pc = get_u32(&tlv->pc);
+    state->auth.index_len = TLV_OPT_LENGTH(tlv);
+    state->auth.index = tlv->index;
+  }
+
+  return PARSE_IGNORE;
+}
+
+static const struct babel_tlv_data pc_tlv_data = {
+  .min_length = sizeof(struct babel_tlv_pc),
+  .read_tlv = &babel_read_pc
+};
+
+static int
+babel_read_challenge_req(struct babel_tlv *hdr, union babel_msg *m UNUSED,
+			 struct babel_parse_state *state)
+{
+  struct babel_tlv_challenge *tlv = (void *) hdr;
+
+  if (!state->is_unicast)
+  {
+    DBG("Ignoring non-unicast challenge request from %I\n", state->saddr);
+    return PARSE_IGNORE;
+  }
+
+  if (tlv->length > BABEL_AUTH_MAX_NONCE_LEN)
+    return PARSE_IGNORE;
+
+  state->auth.challenge_len = tlv->length;
+  if (state->auth.challenge_len)
+    memcpy(state->auth.challenge, tlv->nonce, state->auth.challenge_len);
+  state->auth.challenge_seen = 1;
+
+  return PARSE_IGNORE;
+}
+
+static const struct babel_tlv_data challenge_req_tlv_data = {
+  .min_length = sizeof(struct babel_tlv_challenge),
+  .read_tlv = &babel_read_challenge_req,
+};
+
+static int
+babel_read_challenge_reply(struct babel_tlv *hdr, union babel_msg *m UNUSED,
+                           struct babel_parse_state *state)
+{
+  struct babel_tlv_challenge *tlv = (void *) hdr;
+
+  if (tlv->length != BABEL_AUTH_NONCE_LEN || state->auth.challenge_reply_seen)
+    return PARSE_IGNORE;
+
+  state->auth.challenge_reply_seen = 1;
+  memcpy(state->auth.challenge_reply, tlv->nonce, BABEL_AUTH_NONCE_LEN);
+
+  return PARSE_IGNORE;
+}
+
+static const struct babel_tlv_data challenge_reply_tlv_data = {
+  .min_length = sizeof(struct babel_tlv_challenge),
+  .read_tlv = &babel_read_challenge_reply,
+};
+
+static const struct babel_tlv_data *
+get_auth_tlv_data(u8 type)
+{
+  switch(type)
+  {
+  case BABEL_TLV_PC:
+    return &pc_tlv_data;
+  case BABEL_TLV_CHALLENGE_REQ:
+    return &challenge_req_tlv_data;
+  case BABEL_TLV_CHALLENGE_REPLY:
+    return &challenge_reply_tlv_data;
+  default:
+    return NULL;
+  }
+}
+
+uint
+babel_auth_write_challenge(struct babel_tlv *hdr, union babel_msg *m,
+                           struct babel_write_state *state UNUSED,uint max_len)
+{
+  struct babel_tlv_challenge *tlv = (void *) hdr;
+  struct babel_msg_challenge *msg = &m->challenge;
+
+  uint len = sizeof(struct babel_tlv_challenge) + msg->nonce_len;
+
+  if (len > max_len)
+    return 0;
+
+  TLV_HDR(tlv, msg->type, len);
+  memcpy(tlv->nonce, msg->nonce, msg->nonce_len);
+
+  return len;
+}
+
+static int
+babel_mac_hash(struct password_item *pass,
+               struct babel_mac_pseudohdr *phdr,
+               byte *pkt, uint pkt_len,
+               byte *buf, uint *buf_len)
+{
+  struct mac_context ctx;
+
+  if (mac_type_length(pass->alg) > *buf_len)
+    return 1;
+
+  mac_init(&ctx, pass->alg, pass->password, pass->length);
+  mac_update(&ctx, (byte *)phdr, sizeof(*phdr));
+  mac_update(&ctx, (byte *)pkt, pkt_len);
+
+  *buf_len = mac_get_length(&ctx);
+  memcpy(buf, mac_final(&ctx), *buf_len);
+
+  mac_cleanup(&ctx);
+
+  return 0;
+}
+
+static void
+babel_mac_build_phdr(struct babel_mac_pseudohdr *phdr,
+                     ip_addr saddr, u16 sport,
+                     ip_addr daddr, u16 dport)
+{
+  memset(phdr, 0, sizeof(*phdr));
+  put_ip6(phdr->src_addr, saddr);
+  put_u16(&phdr->src_port, sport);
+  put_ip6(phdr->dst_addr, daddr);
+  put_u16(&phdr->dst_port, dport);
+  DBG("MAC pseudo-header: %I %d %I %d\n", saddr, sport, daddr, dport);
+}
+
+static int
+babel_auth_check_mac(struct babel_iface *ifa, byte *pkt,
+                     byte *trailer, uint trailer_len,
+                     ip_addr saddr, u16 sport,
+                     ip_addr daddr, u16 dport)
+{
+  uint hash_len = (uint)(trailer - pkt);
+  struct babel_proto *p = ifa->proto;
+  byte *end = trailer + trailer_len;
+  btime now_ = current_real_time();
+  struct babel_mac_pseudohdr phdr;
+  struct password_item *pass;
+  struct babel_tlv *tlv;
+
+  if (trailer_len < sizeof(*tlv))
+  {
+    LOG_PKT_AUTH("No MAC signature on packet from %I on %s",
+                 saddr, ifa->ifname);
+    return 1;
+  }
+
+  babel_mac_build_phdr(&phdr, saddr, sport, daddr, dport);
+
+  WALK_LIST(pass, *ifa->cf->passwords)
+  {
+    byte mac_res[MAX_HASH_SIZE];
+    uint mac_len = MAX_HASH_SIZE;
+    u8 frame_err = 0;
+
+    if (pass->accfrom > now_ || pass->accto < now_)
+      continue;
+
+    if (babel_mac_hash(pass, &phdr,
+                       pkt, hash_len,
+                       mac_res, &mac_len))
+      continue;
+
+    WALK_TLVS((void *)trailer, end, tlv, frame_err, saddr, ifa->ifname)
+    {
+      struct babel_tlv_mac *mac = (void *)tlv;
+
+      if (tlv->type != BABEL_TLV_MAC)
+	continue;
+
+      if (tlv->length == mac_len && !memcmp(mac->mac, mac_res, mac_len))
+        return 0;
+
+      DBG("MAC mismatch key id %d pos %d len %d/%d\n",
+	  pass->id, (byte *)tlv - (byte *)pkt, mac_len, tlv->length);
+    }
+    WALK_TLVS_END;
+
+    if (frame_err) {
+      DBG("MAC trailer TLV framing error\n");
+      return 1;
+    }
+  }
+
+  LOG_PKT_AUTH("No MAC key matching packet from %I found on %s",
+               saddr, ifa->ifname);
+  return 1;
+}
+
+/**
+ * babel_auth_check - Check authentication for a packet
+ * @ifa: Interface holding the transmission buffer
+ * @saddr: Source address the packet was received from
+ * @sport: Source port the packet was received from
+ * @daddr: Destination address the packet was sent to
+ * @dport: Destination port the packet was sent to
+ * @pkt: Pointer to start of the packet data
+ * @trailer: Pointer to the packet trailer
+ * @trailer_len: Length of the packet trailer
+ *
+ * This function performs any necessary authentication checks on a packet and
+ * returns 0 if the packet should be accepted (either because it has been
+ * successfully authenticated or because authentication is disabled or
+ * configured in permissive mode), or 1 if the packet should be dropped without
+ * further processing.
+ */
+int
+babel_auth_check(struct babel_iface *ifa,
+                 ip_addr saddr, u16 sport,
+                 ip_addr daddr, u16 dport,
+                 struct babel_pkt_header *pkt,
+                 byte *trailer, uint trailer_len)
+{
+  u8 frame_err UNUSED = 0;
+  struct babel_proto *p = ifa->proto;
+  struct babel_tlv *tlv;
+
+  struct babel_parse_state state = {
+    .get_tlv_data = &get_auth_tlv_data,
+    .proto        = p,
+    .ifa          = ifa,
+    .saddr        = saddr,
+    .is_unicast     = !(ipa_classify(daddr) & IADDR_MULTICAST),
+    .auth = {
+      .sender = saddr,
+    },
+  };
+
+  if (ifa->cf->auth_type == BABEL_AUTH_NONE)
+    return 0;
+
+  TRACE(D_PACKETS, "Checking packet authentication signature");
+
+  if (babel_auth_check_mac(ifa, (byte *)pkt,
+                           trailer, trailer_len,
+                           saddr, sport,
+                           daddr, dport))
+    goto fail;
+
+  /* MAC verified; parse packet to check packet counter and challenge */
+  WALK_TLVS(FIRST_TLV(pkt), trailer, tlv, frame_err, saddr, ifa->iface->name)
+  {
+    union babel_msg msg;
+    enum parse_result res;
+
+    res = babel_read_tlv(tlv, &msg, &state);
+    if (res == PARSE_ERROR)
+    {
+      LOG_PKT_AUTH("Bad TLV from %I via %s type %d pos %d - parse error",
+                   saddr, ifa->iface->name, tlv->type, (byte *)tlv - (byte *)pkt);
+      goto fail;
+    }
+  }
+  WALK_TLVS_END;
+
+  if (babel_auth_check_pc(ifa, &state.auth))
+    goto fail;
+
+  TRACE(D_PACKETS, "Packet from %I via %s authenticated successfully",
+        saddr, ifa->ifname);
+  return 0;
+
+fail:
+  LOG_PKT_AUTH("Packet from %I via %s failed authentication%s",
+               saddr, ifa->ifname,
+               ifa->cf->auth_permissive ? " but accepted in permissive mode" : "");
+
+  return !ifa->cf->auth_permissive;
+}
+
+/**
+ * babel_auth_add_tlvs - Add authentication-related TLVs to a packet
+ * @ifa: Interface holding the transmission buffer
+ * @tlv: Pointer to the place where any new TLVs should be added
+ * @max_len: Maximum length available for adding new TLVs
+ *
+ * This function adds any new TLVs required by the authentication mode to a
+ * packet before it is shipped out. For MAC authentication, this is the packet
+ * counter TLV that must be included in every packet.
+ */
+int
+babel_auth_add_tlvs(struct babel_iface *ifa, struct babel_tlv *tlv, int max_len)
+{
+  struct babel_proto *p = ifa->proto;
+  struct babel_tlv_pc *msg;
+  int len;
+
+  if (ifa->cf->auth_type == BABEL_AUTH_NONE)
+    return 0;
+
+  msg = (void *)tlv;
+  len = sizeof(*msg) + BABEL_AUTH_INDEX_LEN;
+  max_len += ifa->auth_tx_overhead;
+
+  if (len > max_len)
+  {
+    LOG_WARN("Insufficient space to add MAC seqno TLV on iface %s: %d < %d",
+             ifa->ifname, max_len, len);
+    return 0;
+  }
+
+  msg->type = BABEL_TLV_PC;
+  msg->length = len - sizeof(struct babel_tlv);
+  put_u32(&msg->pc, ifa->auth_pc++);
+  memcpy(msg->index, ifa->auth_index, BABEL_AUTH_INDEX_LEN);
+
+  /* Reset index on overflow to 0 */
+  if (!ifa->auth_pc)
+    babel_auth_reset_index(ifa);
+
+  return len;
+}
+
+/**
+ * babel_auth_sign - Sign an outgoing packet before transmission
+ * @ifa: Interface holding the transmission buffer
+ * @dest: Destination address of the packet
+ *
+ * This function adds authentication signature(s) to the packet trailer for each
+ * of the configured authentication keys on the interface.
+ */
+int
+babel_auth_sign(struct babel_iface *ifa, ip_addr dest)
+{
+  struct babel_proto *p = ifa->proto;
+  struct babel_mac_pseudohdr phdr;
+  struct babel_pkt_header *hdr;
+  struct password_item *pass;
+  int tot_len = 0, i = 0;
+  struct babel_tlv *tlv;
+  sock *sk = ifa->sk;
+  byte *pos, *end;
+  btime now_;
+  int len;
+
+  if (ifa->cf->auth_type == BABEL_AUTH_NONE)
+    return 0;
+
+  hdr = (void *) sk->tbuf;
+  len = get_u16(&hdr->length) + sizeof(struct babel_pkt_header);
+
+  pos = (byte *)hdr + len;
+  end = (byte *)hdr + ifa->tx_length + ifa->auth_tx_overhead;
+  tlv = (void *)pos;
+  now_ = current_real_time();
+
+  babel_mac_build_phdr(&phdr, sk->saddr, sk->fport, dest, sk->dport);
+
+  WALK_LIST(pass, *ifa->cf->passwords)
+  {
+    struct babel_tlv_mac *msg = (void *)tlv;
+    uint buf_len = (uint) (end - (byte *)msg - sizeof(*msg));
+
+    if (pass->genfrom > now_ || pass->gento < now_)
+      continue;
+
+    if (babel_mac_hash(pass, &phdr,
+                       (byte *)hdr, len,
+                       msg->mac, &buf_len))
+    {
+      LOG_WARN("Insufficient space for MAC signatures on iface %s dest %I",
+               ifa->ifname, dest);
+      break;
+    }
+
+    msg->type = BABEL_TLV_MAC;
+    msg->length = buf_len;
+
+    tlv = NEXT_TLV(tlv);
+    tot_len += buf_len + sizeof(*msg);
+    i++;
+  }
+
+  DBG("Added %d MAC signatures (%d bytes) on ifa %s for dest %I\n",
+      i, tot_len, ifa->ifname, dest);
+
+  return tot_len;
+}
+
+/**
+ * babel_auth_set_tx_overhead - Set interface TX overhead for authentication
+ * @ifa: Interface to configure
+ *
+ * This function sets the TX overhead for an interface based on its
+ * authentication configuration.
+ */
+void
+babel_auth_set_tx_overhead(struct babel_iface *ifa)
+{
+  if (ifa->cf->auth_type == BABEL_AUTH_NONE)
+  {
+    ifa->auth_tx_overhead = 0;
+    return;
+  }
+
+  ifa->auth_tx_overhead = (sizeof(struct babel_tlv_pc) +
+                           sizeof(struct babel_tlv_mac) * ifa->cf->mac_num_keys +
+                           ifa->cf->mac_total_len);
+  ifa->tx_length -= ifa->auth_tx_overhead;
+}



More information about the Bird-users mailing list