From 1cb1fe71f889eb37b5c958b4d980ae73d7ea23db Mon Sep 17 00:00:00 2001
From: Eugene Bogomazov <eb@qrator.net>
Date: Tue, 28 Jun 2022 11:57:35 +0300
Subject: [PATCH] [PATCH] Add RFC9234 implementation for bird 2.0.10

---
 doc/bird.sgml       | 26 +++++++++++++++++++
 proto/bgp/attrs.c   | 62 +++++++++++++++++++++++++++++++++++++++++++++
 proto/bgp/bgp.c     | 25 ++++++++++++++++++
 proto/bgp/bgp.h     | 18 +++++++++++++
 proto/bgp/config.Y  | 17 +++++++++++--
 proto/bgp/packets.c | 43 +++++++++++++++++++++++++++++++
 6 files changed, 189 insertions(+), 2 deletions(-)

diff --git a/doc/bird.sgml b/doc/bird.sgml
index 89b1541c..fa079b60 100644
--- a/doc/bird.sgml
+++ b/doc/bird.sgml
@@ -2377,6 +2377,7 @@ avoid routing loops.
 <item> <rfc id="8203"> - BGP Administrative Shutdown Communication
 <item> <rfc id="8212"> - Default EBGP Route Propagation Behavior without Policies
 <item> <rfc id="9117"> - Revised Validation Procedure for BGP Flow Specifications
+<item> <rfc id="9234"> - Route Leak Prevention and Detection Using Roles
 </itemize>
 
 <sect1>Route selection rules
@@ -2817,6 +2818,26 @@ using the following configuration parameters:
 	protocol itself (for example, if a route is received through eBGP and
 	therefore does not have such attribute). Default: 100 (0 in pre-1.2.0
 	versions of BIRD).
+
+	<tag><label id="bgp-role">local role <m/role-name/</tag>
+	BGP Roles are defines in <rfc id="9234">. This configuration parameter can
+	be used to simplify process of route leaks prevention, so you don't need
+	to define communities and ingress/egress filters. Instead, filters are
+	automatically configured with help of <cf/OTC/ attribute - a flag that
+	marks routes that should be sent only to customers. The same attribute
+	is also used to automatically detect and filter route leaks created by
+	the third parties.
+
+	Possible <cf><m/role-name/</cf> values are: <cf/provider/, <cf/rs_server/,
+	<cf/rs_client/, <cf/customer/ and <cf/peer/. Default: No local role
+	assigned.
+
+	<tag><label id="bgp-strict-mode">strict mode <m/switch/</tag>
+	If this option is set BGP session won't become established until BGP
+	neighbor set <ref id="bgp-role" name="local Role"> on its side. This
+	configuratoin parameter is defined in <rfc id="9234"> and used to
+	enforce corresponding configuration at your conter-part side. Default:
+	disabled.
 </descrip>
 
 <sect1>Channel configuration
@@ -3124,6 +3145,11 @@ some of them (marked with `<tt/O/') are optional.
 	This attribute contains accumulated IGP metric, which is a total
 	distance to the destination through multiple autonomous systems.
 	Currently, the attribute is not accessible from filters.
+
+	<tag><label id="bgp-otc">int bgp_otc [O]</tag>
+	This attribute is defined in <rfc id="9234">. OTC is a flag that marks
+	routes that should be sent only to customers. If <ref id="bgp-role"
+	name="local Role"> is configured it set automatically.
 </descrip>
 
 <sect1>Example
diff --git a/proto/bgp/attrs.c b/proto/bgp/attrs.c
index 9dd9fb1a..2fa9b0a2 100644
--- a/proto/bgp/attrs.c
+++ b/proto/bgp/attrs.c
@@ -406,6 +406,15 @@ bgp_format_origin(const eattr *a, byte *buf, uint size UNUSED)
   bsprintf(buf, (a->u.data <= 2) ? bgp_origin_names[a->u.data] : "?");
 }
 
+static void
+bgp_decode_otc(struct bgp_parse_state *s, uint code UNUSED, uint flags, byte *data UNUSED, uint len, ea_list **to)
+{
+  if (len != 4)
+    WITHDRAW(BAD_LENGTH, "OTC", len);
+
+  u32 val = get_u32(data);
+  bgp_set_attr_u32(to, s->pool, BA_ONLY_TO_CUSTOMER, flags, val);
+}
 
 static inline int
 bgp_as_path_first_as_equal(const byte *data, uint len, u32 asn)
@@ -1117,6 +1126,13 @@ static const struct bgp_attr_desc bgp_attr_table[] = {
     .decode = bgp_decode_mpls_label_stack,
     .format = bgp_format_mpls_label_stack,
   },
+  [BA_ONLY_TO_CUSTOMER] = {
+    .name = "OTC",
+    .type = EAF_TYPE_INT,
+    .flags = BAF_OPTIONAL | BAF_TRANSITIVE,
+    .encode = bgp_encode_u32,
+    .decode = bgp_decode_otc,
+  },
 };
 
 static inline int
@@ -1445,6 +1461,35 @@ bgp_finish_attrs(struct bgp_parse_state *s, rta *a)
     REPORT("Discarding AIGP attribute received on non-AIGP session");
     bgp_unset_attr(&a->eattrs, s->pool, BA_AIGP);
   }
+
+  if (bgp_channel_is_role_applicable(s->channel))
+  {
+    /* Reject routes from down neighbors if they are leaked */
+    struct bgp_proto *p = s->proto;
+    eattr *e = bgp_find_attr(a->eattrs, BA_ONLY_TO_CUSTOMER);
+    if (e &&
+         (p->cf->local_role == BGP_ROLE_PROVIDER ||
+          p->cf->local_role == BGP_ROLE_RS_SERVER))
+      goto withdraw;
+
+    if (e && e->u.data != p->cf->remote_as && p->cf->local_role == BGP_ROLE_PEER)
+      goto withdraw;
+
+    /* Mark routes from up neighbors if it doesn't happened before */
+    if (!BIT32_TEST(s->attrs_seen, BA_ONLY_TO_CUSTOMER) &&
+         (p->cf->local_role == BGP_ROLE_CUSTOMER ||
+          p->cf->local_role == BGP_ROLE_PEER ||
+          p->cf->local_role == BGP_ROLE_RS_CLIENT))
+      bgp_set_attr_u32(&a->eattrs, s->pool, BA_ONLY_TO_CUSTOMER, 0, p->cf->remote_as);
+  }
+  return;
+
+withdraw:
+  if (!s->ip_reach_len && !s->mp_reach_len)
+    bgp_parse_error(s, 1);
+
+  s->err_withdraw = 1;
+  return;
 }
 
 
@@ -1725,6 +1770,14 @@ bgp_preexport(struct channel *C, rte **new, struct linpool *pool UNUSED)
       return -1;
   }
 
+  /* Do not export routes which are marked with OTC */
+  c = ea_find(e->attrs->eattrs, EA_CODE(PROTOCOL_BGP, BA_ONLY_TO_CUSTOMER));
+  if (bgp_channel_is_role_applicable((struct bgp_channel *)C) && c &&
+      (p->cf->local_role==BGP_ROLE_CUSTOMER ||
+       p->cf->local_role==BGP_ROLE_PEER ||
+       p->cf->local_role==BGP_ROLE_RS_CLIENT))
+    return -1;
+
   return 0;
 }
 
@@ -1834,6 +1887,15 @@ bgp_update_attrs(struct bgp_proto *p, struct bgp_channel *c, rte *e, ea_list *at
     }
   }
 
+  /* Mark routes with OTC to the down directions */
+  a = bgp_find_attr(attrs, BA_ONLY_TO_CUSTOMER);
+  if (bgp_channel_is_role_applicable(c) && ! a) {
+    if (p->cf->local_role == BGP_ROLE_PROVIDER ||
+        p->cf->local_role == BGP_ROLE_PEER ||
+        p->cf->local_role == BGP_ROLE_RS_SERVER)
+      bgp_set_attr_u32(&attrs, pool, BA_ONLY_TO_CUSTOMER, 0, p->local_as);
+  }
+
   /*
    * Presence of mandatory attributes ORIGIN and AS_PATH is ensured by above
    * conditions. Presence and validity of quasi-mandatory NEXT_HOP attribute
diff --git a/proto/bgp/bgp.c b/proto/bgp/bgp.c
index 89507f1a..a59b5e0a 100644
--- a/proto/bgp/bgp.c
+++ b/proto/bgp/bgp.c
@@ -1983,6 +1983,17 @@ bgp_postconfig(struct proto_config *CF)
   if (internal && cf->rs_client)
     cf_error("Only external neighbor can be RS client");
 
+  if (internal &&
+        (cf->local_role==BGP_ROLE_PEER ||
+         cf->local_role==BGP_ROLE_CUSTOMER ||
+         cf->local_role==BGP_ROLE_PROVIDER ||
+         cf->local_role==BGP_ROLE_RS_CLIENT ||
+         cf->local_role==BGP_ROLE_RS_SERVER))
+    cf_error("External roles can be set only on external connection");
+
+  if (cf->strict_mode && cf->local_role==BGP_ROLE_UNDEFINED)
+    cf_error("Role must be set when using strict mode");
+
   if (!cf->confederation && cf->confederation_member)
     cf_error("Confederation ID must be set for member sessions");
 
@@ -2345,6 +2356,17 @@ bgp_show_afis(int code, char *s, u32 *afis, uint count)
   cli_msg(code, b.start);
 }
 
+
+static const char *
+bgp_format_role_name(u8 role)
+{
+  static const char *bgp_role_names[] = { "provider", "rs_server", "rs_client", "customer", "peer" };
+  if (role == BGP_ROLE_UNDEFINED) return "undefine";
+  if (role <= 5) return bgp_role_names[role];
+  return "?";
+}
+
+
 static void
 bgp_show_capabilities(struct bgp_proto *p UNUSED, struct bgp_caps *caps)
 {
@@ -2473,6 +2495,9 @@ bgp_show_capabilities(struct bgp_proto *p UNUSED, struct bgp_caps *caps)
 
   if (caps->hostname)
     cli_msg(-1006, "      Hostname: %s", caps->hostname);
+
+  if (caps->role != BGP_ROLE_UNDEFINED)
+    cli_msg(-1006, "      Role %s", bgp_format_role_name(caps->role));
 }
 
 static void
diff --git a/proto/bgp/bgp.h b/proto/bgp/bgp.h
index 7cd1c27d..0cf3347b 100644
--- a/proto/bgp/bgp.h
+++ b/proto/bgp/bgp.h
@@ -114,6 +114,8 @@ struct bgp_config {
   int gr_mode;				/* Graceful restart mode (BGP_GR_*) */
   int llgr_mode;			/* Long-lived graceful restart mode (BGP_LLGR_*) */
   int setkey;				/* Set MD5 password to system SA/SP database */
+  u8  local_role;			/* Set peering role with neighbor */
+  int strict_mode;			/* Is setting role on both sides is mandatory */
   /* Times below are in seconds */
   unsigned gr_time;			/* Graceful restart timeout */
   unsigned llgr_time;			/* Long-lived graceful restart stale time */
@@ -167,6 +169,13 @@ struct bgp_channel_config {
 #define BGP_PT_INTERNAL		1
 #define BGP_PT_EXTERNAL		2
 
+#define BGP_ROLE_UNDEFINED 	255
+#define BGP_ROLE_PROVIDER 	0
+#define BGP_ROLE_RS_SERVER 	1
+#define BGP_ROLE_RS_CLIENT 	2
+#define BGP_ROLE_CUSTOMER 	3
+#define BGP_ROLE_PEER 		4
+
 #define NH_NO			0
 #define NH_ALL			1
 #define NH_IBGP			2
@@ -223,6 +232,7 @@ struct bgp_caps {
   u8 ext_messages;			/* Extended message length,  RFC draft */
   u8 route_refresh;			/* Route refresh capability, RFC 2918 */
   u8 enhanced_refresh;			/* Enhanced route refresh,   RFC 7313 */
+  u8 role;				/* BGP role capability,      RFC 9234 */
 
   u8 gr_aware;				/* Graceful restart capability, RFC 4724 */
   u8 gr_flags;				/* Graceful restart flags */
@@ -278,6 +288,7 @@ struct bgp_conn {
   u8 last_channel_count;		/* Number of times the last channel was used in succession */
   int notify_code, notify_subcode, notify_size;
   byte *notify_data;
+  u8 neighbor_role;
 
   uint hold_time, keepalive_time;	/* Times calculated from my and neighbor's requirements */
 };
@@ -485,6 +496,12 @@ static inline int bgp_cc_is_ipv4(struct bgp_channel_config *c)
 static inline int bgp_cc_is_ipv6(struct bgp_channel_config *c)
 { return BGP_AFI(c->afi) == BGP_AFI_IPV6; }
 
+static inline int bgp_channel_is_role_applicable(struct bgp_channel *c)
+{ return (c->afi == BGP_AF_IPV4 || c->afi == BGP_AF_IPV6); }
+
+static inline int bgp_cc_is_role_applicable(struct bgp_channel_config *c)
+{ return (c->afi == BGP_AF_IPV4 || c->afi == BGP_AF_IPV6); }
+
 static inline uint bgp_max_packet_length(struct bgp_conn *conn)
 { return conn->ext_messages ? BGP_MAX_EXT_MSG_LENGTH : BGP_MAX_MESSAGE_LENGTH; }
 
@@ -660,6 +677,7 @@ void bgp_update_next_hop(struct bgp_export_state *s, eattr *a, ea_list **to);
 #define BA_AS4_AGGREGATOR       0x12	/* RFC 6793 */
 #define BA_AIGP			0x1a	/* RFC 7311 */
 #define BA_LARGE_COMMUNITY	0x20	/* RFC 8092 */
+#define BA_ONLY_TO_CUSTOMER	0x23	/* RFC 9234 */
 
 /* Bird's private internal BGP attributes */
 #define BA_MPLS_LABEL_STACK	0xfe	/* MPLS label stack transfer attribute */
diff --git a/proto/bgp/config.Y b/proto/bgp/config.Y
index 241aa7c2..8a8152c8 100644
--- a/proto/bgp/config.Y
+++ b/proto/bgp/config.Y
@@ -31,7 +31,8 @@ CF_KEYWORDS(BGP, LOCAL, NEIGHBOR, AS, HOLD, TIME, CONNECT, RETRY, KEEPALIVE,
 	STRICT, BIND, CONFEDERATION, MEMBER, MULTICAST, FLOW4, FLOW6, LONG,
 	LIVED, STALE, IMPORT, IBGP, EBGP, MANDATORY, INTERNAL, EXTERNAL, SETS,
 	DYNAMIC, RANGE, NAME, DIGITS, BGP_AIGP, AIGP, ORIGINATE, COST, ENFORCE,
-	FIRST, FREE, VALIDATE, BASE)
+	FIRST, FREE, VALIDATE, BASE, LOCAL, ROLE, PEER, PROVIDER, CUSTOMER,
+	RS_SERVER, RS_CLIENT, STRICT, MODE)
 
 %type <i> bgp_nh
 %type <i32> bgp_afi
@@ -40,7 +41,7 @@ CF_KEYWORDS(CEASE, PREFIX, LIMIT, HIT, ADMINISTRATIVE, SHUTDOWN, RESET, PEER,
 	CONFIGURATION, CHANGE, DECONFIGURED, CONNECTION, REJECTED, COLLISION,
 	OUT, OF, RESOURCES)
 
-%type<i> bgp_cease_mask bgp_cease_list bgp_cease_flag
+%type<i> bgp_cease_mask bgp_cease_list bgp_cease_flag bgp_role_name
 
 CF_GRAMMAR
 
@@ -74,6 +75,8 @@ bgp_proto_start: proto_start BGP {
      BGP_CFG->setkey = 1;
      BGP_CFG->dynamic_name = "dynbgp";
      BGP_CFG->check_link = -1;
+     BGP_CFG->local_role = BGP_ROLE_UNDEFINED;
+     BGP_CFG->strict_mode = 0;
    }
  ;
 
@@ -114,6 +117,14 @@ bgp_cease_flag:
  | OUT OF RESOURCES		{ $$ = 1 << 8; }
  ;
 
+bgp_role_name:
+   PEER      { $$ = BGP_ROLE_PEER; }
+ | PROVIDER  { $$ = BGP_ROLE_PROVIDER; }
+ | CUSTOMER  { $$ = BGP_ROLE_CUSTOMER; }
+ | RS_SERVER { $$ = BGP_ROLE_RS_SERVER; }
+ | RS_CLIENT { $$ = BGP_ROLE_RS_CLIENT; }
+ ;
+
 bgp_proto:
    bgp_proto_start proto_name '{'
  | bgp_proto proto_item ';'
@@ -197,6 +208,8 @@ bgp_proto:
  | bgp_proto BFD GRACEFUL ';' { init_bfd_opts(&BGP_CFG->bfd); BGP_CFG->bfd->mode = BGP_BFD_GRACEFUL; }
  | bgp_proto BFD { open_bfd_opts(&BGP_CFG->bfd); } bfd_opts { close_bfd_opts(); } ';'
  | bgp_proto ENFORCE FIRST AS bool ';' { BGP_CFG->enforce_first_as = $5; }
+ | bgp_proto LOCAL ROLE bgp_role_name ';' { BGP_CFG->local_role = $4; }
+ | bgp_proto STRICT MODE bool ';' { BGP_CFG->strict_mode = $4; }
  ;
 
 bgp_afi:
diff --git a/proto/bgp/packets.c b/proto/bgp/packets.c
index f13625e2..3f5e1e8b 100644
--- a/proto/bgp/packets.c
+++ b/proto/bgp/packets.c
@@ -238,6 +238,7 @@ bgp_prepare_capabilities(struct bgp_conn *conn)
   caps->ext_messages = p->cf->enable_extended_messages;
   caps->route_refresh = p->cf->enable_refresh;
   caps->enhanced_refresh = p->cf->enable_refresh;
+  caps->role = p->cf->local_role;
 
   if (caps->as4_support)
     caps->as4_number = p->public_as;
@@ -350,6 +351,14 @@ bgp_write_capabilities(struct bgp_conn *conn, byte *buf)
     *buf++ = 0;			/* Capability data length */
   }
 
+  if (caps->role != BGP_ROLE_UNDEFINED)
+  {
+    *buf++ = 9;			/* Capability 9: Announce choosen BGP role */
+    *buf++ = 1;			/* Capability data length */
+    buf[0] = caps->role;
+    buf += 1;
+  }
+
   if (caps->gr_aware)
   {
     *buf++ = 64;		/* Capability 64: Support for graceful restart */
@@ -460,6 +469,7 @@ bgp_read_capabilities(struct bgp_conn *conn, byte *pos, int len)
     conn->remote_caps = NULL;
   }
 
+  caps->role = BGP_ROLE_UNDEFINED;
   caps->length += len;
 
   while (len > 0)
@@ -513,6 +523,20 @@ bgp_read_capabilities(struct bgp_conn *conn, byte *pos, int len)
       caps->ext_messages = 1;
       break;
 
+    case  9: /* Roles capability, RFC 9234 */
+      if (cl != 1)
+        goto err;
+
+      caps->role = pos[2];
+      if ((conn->neighbor_role != BGP_ROLE_UNDEFINED) && (conn->neighbor_role != caps->role))
+      {
+        mb_free(caps);
+        bgp_error(conn, 2, 11, NULL, 0);
+        return -1;
+      }
+      conn->neighbor_role = caps->role;
+      break;
+
     case 64: /* Graceful restart capability, RFC 4724 */
       if (cl % 4 != 2)
 	goto err;
@@ -704,6 +728,8 @@ bgp_read_options(struct bgp_conn *conn, byte *pos, uint len, uint rest)
   /* Length of option parameter header */
   uint hlen = ext ? 3 : 2;
 
+  conn->neighbor_role = BGP_ROLE_UNDEFINED;
+
   while (len > 0)
   {
     if (len < hlen)
@@ -854,6 +880,22 @@ bgp_rx_open(struct bgp_conn *conn, byte *pkt, uint len)
     conn->received_as = asn;
   }
 
+  u8 neigh_role = conn->neighbor_role;
+  u8 local_role = p->cf->local_role;
+
+  if ((neigh_role != BGP_ROLE_UNDEFINED) &&
+      (local_role != BGP_ROLE_UNDEFINED) && !(
+          (local_role == BGP_ROLE_PEER && neigh_role == BGP_ROLE_PEER) ||
+          (local_role == BGP_ROLE_CUSTOMER && neigh_role == BGP_ROLE_PROVIDER) ||
+          (local_role == BGP_ROLE_PROVIDER && neigh_role == BGP_ROLE_CUSTOMER) ||
+          (local_role == BGP_ROLE_RS_CLIENT && neigh_role == BGP_ROLE_RS_SERVER) ||
+          (local_role == BGP_ROLE_RS_SERVER && neigh_role == BGP_ROLE_RS_CLIENT)))
+
+      { bgp_error(conn, 2, 11, NULL, 0); return; }
+
+  if ((p->cf->strict_mode) && (neigh_role == BGP_ROLE_UNDEFINED))
+      { bgp_error(conn, 2, 11, NULL, 0); return; }
+
   /* Check the other connection */
   other = (conn == &p->outgoing_conn) ? &p->incoming_conn : &p->outgoing_conn;
   switch (other->state)
@@ -2984,6 +3026,7 @@ static struct {
   { 2, 6, "Unacceptable hold time" },
   { 2, 7, "Required capability missing" }, /* [RFC5492] */
   { 2, 8, "No supported AFI/SAFI" }, /* This error msg is nonstandard */
+  { 2,11, "Role mismatch" }, /* From Open Policy, RFC 9234 */
   { 3, 0, "Invalid UPDATE message" },
   { 3, 1, "Malformed attribute list" },
   { 3, 2, "Unrecognized well-known attribute" },
-- 
2.25.1

