commit 803c85906d7a506c39040af758b40a33608c8ed9
Author: Quantum <quantum2048@gmail.com>
Date:   Sat Sep 16 22:29:30 2023 -0400

    Initial commit

diff --git a/README.md b/README.md
new file mode 100644
index 0000000..90e8572
--- /dev/null
+++ b/README.md
@@ -0,0 +1,245 @@
+# Quantum's `bird` Filter Library
+
+This is meant to be a starter repository containing sample `bird` 2.x config
+files that you can use to build your own BGP filters. Filters are provided as
+composable `bird` functions and enables you to harness the full power of the
+`bird` filter mini-programming language, as an alternative to a more declarative
+solution like [PathVector][pv].
+
+## Quick start
+
+1. Make sure `bird` 2.x is installed, e.g. on Debian or Ubuntu, through
+   `sudo apt install bird2`.
+2. Clone this repository:
+   ```
+   git clone https://github.com/quantum5/bird-filter.git
+   cd bird-filter
+   ```
+3. Customize [`filter_bgp.conf`][filter] by editing it. Pay special attention
+   to anything tagged `FIXME`.
+4. Install `filter_bgp.conf` into your `bird` configuration directory
+   (`/etc/bird` by default):
+   ```
+   sudo cp filter_bgp.conf /etc/bird
+   ```
+
+## Defining BGP sessions
+
+You can use [`skeleton.conf`][skeleton] as a basic `bird` starting config. Note
+that in this config, static protocol routes are internal to `bird` and will not
+be exported to the kernel routing table. You can change this by changing the
+export rules for `protocol kernel`.
+
+This filter library makes use of two basic static protocols:
+* `node_v4`: IPv4 routes to be exported by the `export_cone` helper.
+* `node_v6`: IPv6 routes to be exported by the `export_cone` helper.
+
+For example, to advertise `198.51.100.0/24` and `2001:db8:1000::/36`:
+
+```
+protocol static node_v4 {
+    ipv4 {};
+    route 198.51.100.0/24 reject;
+}
+
+protocol static node_v6 {
+    ipv6 {};
+    route 2001:db8:1000::/36 reject;
+}
+```
+
+Two additional static protocols are used to aid with traffic engineering for
+anycast prefixes:
+* `node_v4_anycast`: IPv4 routes to be exported by the `export_anycast` helper.
+* `node_v6_anycast`: IPv6 routes to be exported by the `export_anycast` helper.
+
+You can add `protocol` blocks to this config for each BGP neighbour. This is
+dependent on the neighbour type.
+
+In the follow examples, we assume the following local preferences:
+* 50 for upstreams;
+* 90 for IXPs;
+* 100 for direct peers; and
+* 120 for downstreams.
+
+### Upstreams
+
+```
+protocol bgp example_upstream_v4 {
+    description "Example Upstream (IPv4)";
+    local 192.0.2.25 as 64500;
+    neighbor 192.0.2.24 as 64501;
+    default bgp_local_pref 50;
+
+    ipv4 {
+        import keep filtered;
+        import where import_transit(64501, false);
+        export where export_cone(64501);
+    };
+}
+
+protocol bgp example_upstream_v6 {
+    description "Example Upstream (IPv6)";
+    local 2001:db8:2000::2 as 64500;
+    neighbor 2001:db8:2000::1 as 64501;
+    default bgp_local_pref 50;
+
+    ipv6 {
+        import keep filtered;
+        import where import_transit(64501, false);
+        export where export_cone(64501);
+    };
+}
+```
+
+The example above assumes you are AS64500 and establishes BGP sessions over
+both IPv4 and IPv6 with an upstream AS64501 and exports your entire cone. It
+also assumes your upstream is sending you a full table and filters out the
+default route. If you expect a default route instead, use
+`import where import_transit(64501, true)`.
+
+To export your anycast as well, you can simply do
+`export where export_cone(64501) || export_anycast()`.
+
+### Peers
+
+```
+protocol bgp example_peer_v4 {
+    description "Example Peer (IPv4)";
+    local 192.0.2.25 as 64500;
+    neighbor 192.0.2.28 as 64502;
+    default bgp_local_pref 100;
+
+    ipv4 {
+        import keep filtered;
+        import where import_peer_trusted(64502);
+        export where export_cone(64502);
+    };
+}
+
+protocol bgp example_peer_v6 {
+    description "Example Peer (IPv6)";
+    local 2001:db8:2000::2 as 64500;
+    neighbor 2001:db8:2000::10 as 64502;
+    default bgp_local_pref 100;
+
+    ipv6 {
+        import keep filtered;
+        import where import_peer_trusted(64502);
+        export where export_cone(64502);
+    };
+}
+```
+
+The example above assumes you are AS64500 and establishes BGP sessions over
+both IPv4 and IPv6 with a peer AS64502 and exports your entire cone. It assumes
+your peer is trusted and doesn't provide any IRR filtering. If you don't trust
+your peer, see the [IRR filtering](#irr-filtering) section below.
+
+### IXP route servers
+
+```
+protocol bgp example_ixp_v4 {
+    description "Example IXP Route Servers (IPv4)";
+    local 203.0.113.3 as 64500;
+    neighbor 203.0.113.1 as 64503;
+    default bgp_local_pref 90;
+
+    ipv4 {
+        import keep filtered;
+        import where import_ixp_trusted(64503);
+        export where export_cone(64503);
+    };
+}
+
+protocol bgp example_ixp_v6 {
+    description "Example IXP Route Servers (IPv6)";
+    local 2001:db8:3000::3 as 64500;
+    neighbor 2001:db8:3000::1 as 64503;
+    default bgp_local_pref 90;
+
+    ipv6 {
+        import keep filtered;
+        import where import_ixp_trusted(64503);
+        export where export_cone(64503);
+    };
+}
+```
+
+The example above assumes you are AS64500 and establishes BGP sessions over
+both IPv4 and IPv6 with the IXP route server whose ASN is 64503 and exports
+your entire cone. It assumes your IXP is trusted and doesn't provide any IRR
+filtering. If you don't trust your IXP, see the [IRR filtering](#irr-filtering)
+section below.
+
+### Downstreams
+
+```
+protocol bgp example_downstream_v4 {
+    description "Example Downstream (IPv4)";
+    local 203.0.113.3 as 64500;
+    neighbor 203.0.113.7 as 64504;
+    default bgp_local_pref 120;
+
+    ipv4 {
+        import keep filtered;
+        import where import_downstream(64504, IRR_DOWNSTREAM_V4, IRR_DOWNSTREAM_ASN);
+        export where export_to_downstream();
+    };
+}
+
+protocol bgp example_downstream_v6 {
+    description "Example Downstream (IPv6)";
+    local 2001:db8:3000::3 as 64500;
+    neighbor 2001:db8:3000::7 as 64504;
+    default bgp_local_pref 120;
+
+    ipv6 {
+        import keep filtered;
+        import where import_downstream(64504, IRR_DOWNSTREAM_V6, IRR_DOWNSTREAM_ASN);
+        export where export_to_downstream();
+    };
+}
+```
+
+The example above assumes you are AS64500 and establishes BGP sessions over
+both IPv4 and IPv6 with a downstream whose ASN is 64504 and exports all your
+routes. For your protection, downstream imports without IRR is *not* supported.
+For details about setting up IRR, see the [IRR filtering](#irr-filtering)
+section below.
+
+## BGP communities
+
+The following large informational communities are implemented by default:
+* `YOUR_ASN:1:x`: route received from IXP with ID x;
+* `YOUR_ASN:2:x`: route received from neighbour with ASN x;
+* `YOUR_ASN:3:100`: route received from peer;
+* `YOUR_ASN:3:101`: route received from IXP route server;
+* `YOUR_ASN:3:102`: route received from upstream; and
+* `YOUR_ASN:3:103`: route received from downstream.
+
+The following large control communities are implemented by default and can be
+used by downstreams:
+* `YOUR_ASN:10:x`: do not export route to ASx;
+* `YOUR_ASN:11:x`: prepend `YOUR_ASN` once upon export to ASx;
+* `YOUR_ASN:12:x`: prepend `YOUR_ASN` twice upon export to ASx; and
+* `YOUR_ASN:12:x`: prepend `YOUR_ASN` thrice upon export to ASx.
+
+## IRR filtering
+
+1. Follow [`irr-filters.example`][irr-conf] and create `/etc/bird/irr-filters`
+   for the peers you would like to filter. (To use alternative locations, edit
+   [`make-irr-filter`][irr-script] accordingly.)
+2. Run `make-irr-filter` to re-generate IRR filters.
+3. Add `include "filter_irr.conf";` into your `bird.conf`.
+4. Instead of `import_peer_trusted(asn)` or `import_ixp_trusted(ixp_id)`, use
+   `import_peer(asn, IRR_PEER_V4, IRR_PEER_ASN)` or
+   `import_peer(asn, IRR_PEER_V6, IRR_PEER_ASN)`, and similarly for IXPs.
+5. Create a cron job that runs `make-irr-filter` followed by `birdc configure`.
+   Daily is a reasonable cadence.
+
+  [pv]: https://pathvector.io/
+  [filter]: filter_bgp.conf
+  [skeleton]: skeleton.conf
+  [irr-conf]: irr-filters.example
+  [irr-script]: make-irr-filter
diff --git a/filter_bgp.conf b/filter_bgp.conf
new file mode 100644
index 0000000..1818f8c
--- /dev/null
+++ b/filter_bgp.conf
@@ -0,0 +1,288 @@
+roa4 table rpki4;
+roa6 table rpki6;
+attribute int export_downstream;
+
+protocol static default_v4 {
+    ipv4 {};
+    route 0.0.0.0/0 reject;
+}
+
+protocol static default_v6 {
+    ipv6 {};
+    route ::/0 reject;
+}
+
+# FIXME: Change this to your ASN.
+define MY_ASN = 64500;
+
+define LC_IXP_ID    = 1;
+define LC_PEER_ASN  = 2;
+define LC_INFO      = 3;
+
+define LC_NO_EXPORT = 10;
+define LC_PREPEND_1 = 11;
+define LC_PREPEND_2 = 12;
+define LC_PREPEND_3 = 13;
+
+define LC_DOWNSTREAM_START  = 10;
+define LC_DOWNSTREAM_END    = 13;
+
+# FIXME: define your IXPs here:
+# define IXP_EXAMPLE1 = 100;
+# define IXP_EXAMPLE2 = 101;
+
+define INFO_PEER        = 100;
+define INFO_IXP_RS      = 101;
+define INFO_TRANSIT     = 102;
+define INFO_DOWNSTREAM  = 103;
+
+define IPV4_BOGON = [
+    0.0.0.0/8+,         # RFC 1122 'this' network
+    10.0.0.0/8+,        # RFC 1918 private space
+    100.64.0.0/10+,     # RFC 6598 Carrier grade nat space
+    127.0.0.0/8+,       # RFC 1122 localhost
+    169.254.0.0/16+,    # RFC 3927 link local
+    172.16.0.0/12+,     # RFC 1918 private space
+    192.0.2.0/24+,      # RFC 5737 TEST-NET-1
+    192.88.99.0/24+,    # RFC 7526 6to4 anycast relay
+    192.168.0.0/16+,    # RFC 1918 private space
+    198.18.0.0/15+,     # RFC 2544 benchmarking
+    198.51.100.0/24+,   # RFC 5737 TEST-NET-2
+    203.0.113.0/24+,    # RFC 5737 TEST-NET-3
+    224.0.0.0/4+,       # multicast
+    240.0.0.0/4+        # reserved
+];
+
+define IPV6_BOGON = [
+    ::/0,                   # Default
+    ::/96,                  # IPv4-compatible IPv6 address - deprecated by RFC4291
+    ::/128,                 # Unspecified address
+    ::1/128,                # Local host loopback address
+    ::ffff:0.0.0.0/96+,     # IPv4-mapped addresses
+    ::224.0.0.0/100+,       # Compatible address (IPv4 format)
+    ::127.0.0.0/104+,       # Compatible address (IPv4 format)
+    ::0.0.0.0/104+,         # Compatible address (IPv4 format)
+    ::255.0.0.0/104+,       # Compatible address (IPv4 format)
+    0000::/8+,              # Pool used for unspecified, loopback and embedded IPv4 addresses
+    0100::/8+,              # RFC 6666 - reserved for Discard-Only Address Block
+    0200::/7+,              # OSI NSAP-mapped prefix set (RFC4548) - deprecated by RFC4048
+    0400::/6+,              # RFC 4291 - Reserved by IETF
+    0800::/5+,              # RFC 4291 - Reserved by IETF
+    1000::/4+,              # RFC 4291 - Reserved by IETF
+    2001:10::/28+,          # RFC 4843 - Deprecated (previously ORCHID)
+    2001:20::/28+,          # RFC 7343 - ORCHIDv2
+    2001:db8::/32+,         # Reserved by IANA for special purposes and documentation
+    2002:e000::/20+,        # Invalid 6to4 packets (IPv4 multicast)
+    2002:7f00::/24+,        # Invalid 6to4 packets (IPv4 loopback)
+    2002:0000::/24+,        # Invalid 6to4 packets (IPv4 default)
+    2002:ff00::/24+,        # Invalid 6to4 packets
+    2002:0a00::/24+,        # Invalid 6to4 packets (IPv4 private 10.0.0.0/8 network)
+    2002:ac10::/28+,        # Invalid 6to4 packets (IPv4 private 172.16.0.0/12 network)
+    2002:c0a8::/32+,        # Invalid 6to4 packets (IPv4 private 192.168.0.0/16 network)
+    3ffe::/16+,             # Former 6bone, now decommissioned
+    4000::/3+,              # RFC 4291 - Reserved by IETF
+    5f00::/8+,              # RFC 5156 - used for the 6bone but was returned
+    6000::/3+,              # RFC 4291 - Reserved by IETF
+    8000::/3+,              # RFC 4291 - Reserved by IETF
+    a000::/3+,              # RFC 4291 - Reserved by IETF
+    c000::/3+,              # RFC 4291 - Reserved by IETF
+    e000::/4+,              # RFC 4291 - Reserved by IETF
+    f000::/5+,              # RFC 4291 - Reserved by IETF
+    f800::/6+,              # RFC 4291 - Reserved by IETF
+    fc00::/7+,              # Unicast Unique Local Addresses (ULA) - RFC 4193
+    fe80::/10+,             # Link-local Unicast
+    fec0::/10+,             # Site-local Unicast - deprecated by RFC 3879 (replaced by ULA)
+    ff00::/8+               # Multicast
+];
+
+define ASN_BOGON = [
+    0,                      # RFC 7607
+    23456,                  # RFC 4893 AS_TRANS
+    64496..64511,           # RFC 5398 and documentation/example ASNs
+    64512..65534,           # RFC 6996 Private ASNs
+    65535,                  # RFC 7300 Last 16 bit ASN
+    65536..65551,           # RFC 5398 and documentation/example ASNs
+    65552..131071,          # RFC IANA reserved ASNs
+    4200000000..4294967294, # RFC 6996 Private ASNs
+    4294967295              # RFC 7300 Last 32 bit ASN
+];
+
+function ip_bogon() {
+    case net.type {
+        NET_IP4: return net ~ IPV4_BOGON;
+        NET_IP6: return net ~ IPV6_BOGON;
+        else:    return true;
+    }
+}
+
+function rpki_invalid() {
+    case net.type {
+        NET_IP4: return roa_check(rpki4, net, bgp_path.last) = ROA_INVALID;
+        NET_IP6: return roa_check(rpki6, net, bgp_path.last) = ROA_INVALID;
+        else:    return false;
+    }
+}
+
+function is_default_route() {
+    case net.type {
+        NET_IP4: return net = 0.0.0.0/0;
+        NET_IP6: return net = ::/0;
+        else:    return false;
+    }
+}
+
+function bad_prefix_len() {
+    case net.type {
+        NET_IP4: return net.len > 24;
+        NET_IP6: return net.len > 48;
+        else:    return false;
+    }
+}
+
+function clean_own_communities() {
+    bgp_large_community.delete([(MY_ASN, *, *)]);
+}
+
+function honour_graceful_shutdown() {
+    # RFC 8326: Graceful BGP Session Shutdown
+    if (65535, 0) ~ bgp_community then bgp_local_pref = 0;
+}
+
+function handle_prepend(int dest_asn) {
+    if (MY_ASN, LC_PREPEND_1, dest_asn) ~ bgp_large_community then {
+        bgp_path.prepend(MY_ASN);
+    }
+
+    if (MY_ASN, LC_PREPEND_2, dest_asn) ~ bgp_large_community then {
+        bgp_path.prepend(MY_ASN);
+        bgp_path.prepend(MY_ASN);
+    }
+
+    if (MY_ASN, LC_PREPEND_3, dest_asn) ~ bgp_large_community then {
+        bgp_path.prepend(MY_ASN);
+        bgp_path.prepend(MY_ASN);
+        bgp_path.prepend(MY_ASN);
+    }
+}
+
+function import_safe(bool allow_default) {
+    if is_default_route()           then return allow_default;
+    if ip_bogon()                   then return false;
+    if bgp_path ~ ASN_BOGON         then return false;
+    if bgp_path.len > 50            then return false;
+    if bad_prefix_len()             then return false;
+    if rpki_invalid()               then return false;
+
+    export_downstream = 1;
+    honour_graceful_shutdown();
+
+    return true;
+}
+
+function import_peer_trusted(int peer_asn) {
+    clean_own_communities();
+    bgp_large_community.add((MY_ASN, LC_INFO, INFO_PEER));
+    bgp_large_community.add((MY_ASN, LC_PEER_ASN, peer_asn));
+
+    return import_safe(false);
+}
+
+function import_peer(int peer_asn; prefix set prefixes; int set as_set) {
+    if net !~ prefixes then return false;
+
+    for int path_asn in bgp_path do {
+        if path_asn !~ as_set then return false;
+    }
+
+    return import_peer_trusted(peer_asn);
+}
+
+function import_ixp_trusted(int ixp_id) {
+    clean_own_communities();
+    bgp_large_community.add((MY_ASN, LC_INFO, INFO_IXP_RS));
+    bgp_large_community.add((MY_ASN, LC_IXP_ID, ixp_id));
+
+    return import_safe(false);
+}
+
+function import_ixp(int ixp_id; prefix set prefixes; int set as_set) {
+    if net !~ prefixes then return false;
+
+    for int path_asn in bgp_path do {
+        if path_asn !~ as_set then return false;
+    }
+
+    return import_ixp_trusted(ixp_id);
+}
+
+function import_transit(int transit_asn; bool default_route) {
+    clean_own_communities();
+    bgp_large_community.add((MY_ASN, LC_INFO, INFO_TRANSIT));
+    bgp_large_community.add((MY_ASN, LC_PEER_ASN, transit_asn));
+
+    return import_safe(default_route);
+}
+
+function import_downstream(int downstream_asn; prefix set prefixes; int set as_set) {
+    if net !~ prefixes then return false;
+
+    for int path_asn in bgp_path do {
+        if path_asn !~ as_set then return false;
+    }
+
+    # If they don't want to export this to us, then we won't take it at all.
+    if (QUANTUM_ASN, LC_NO_EXPORT, QUANTUM_ASN) ~ bgp_large_community then return false;
+
+    bgp_large_community.delete([
+        (QUANTUM_ASN, 0..LC_DOWNSTREAM_START-1, *),
+        (QUANTUM_ASN, LC_DOWNSTREAM_END+1..0xFFFFFFFF, *)
+    ]);
+
+    bgp_large_community.add((QUANTUM_ASN, LC_INFO, INFO_DOWNSTREAM));
+    bgp_large_community.add((QUANTUM_ASN, LC_PEER_ASN, downstream_asn));
+
+    return import_safe(false);
+}
+
+function export_to_downstream() {
+    return (defined(export_downstream) && export_downstream = 1) ||
+        (source = RTS_STATIC && !bad_prefix_len() && (
+            proto = "node_v4" ||
+            proto = "node_v6" ||
+            proto = "node_v4_anycast" ||
+            proto = "node_v6_anycast"
+        ));
+}
+
+function export_monitoring() {
+    return export_to_downstream();
+}
+
+function export_cone(int dest_asn) {
+    if (MY_ASN, LC_NO_EXPORT, dest_asn) ~ bgp_large_community then return false;
+    handle_prepend(dest_asn);
+
+    if (MY_ASN, LC_INFO, INFO_DOWNSTREAM) ~ bgp_large_community then return true;
+
+    case net.type {
+        NET_IP4: return source = RTS_STATIC && proto = "node_v4";
+        NET_IP6: return source = RTS_STATIC && proto = "node_v6";
+        else:    return false;
+    }
+}
+
+function export_anycast() {
+    case net.type {
+        NET_IP4: return source = RTS_STATIC && proto = "node_v4_anycast";
+        NET_IP6: return source = RTS_STATIC && proto = "node_v6_anycast";
+        else:    return false;
+    }
+}
+
+function export_default() {
+    case net.type {
+        NET_IP4: return source = RTS_STATIC && proto = "default_v4";
+        NET_IP6: return source = RTS_STATIC && proto = "default_v6";
+        else:    return false;
+    }
+}
diff --git a/irr-filters.example b/irr-filters.example
new file mode 100644
index 0000000..072f7c8
--- /dev/null
+++ b/irr-filters.example
@@ -0,0 +1,12 @@
+# You can use # for comments.
+EXAMPLE     AS-EXAMPLE
+EXAMPLE2    RIPE::AS64500:AS-EXAMPLE2       v4
+EXAMPLE3    ARIN::AS-EXAMPLE3               v6
+# This will cause make-irr-filter to generate a configuration file containing:
+# * IRR_EXAMPLE_V4:     allowed IPv4 prefixes queried from AS-EXAMPLE.
+# * IRR_EXAMPLE_V6:     allowed IPv6 prefixes queried from AS-EXAMPLE.
+# * IRR_EXAMPLE_ASN:    allowed ASNs queried from AS-EXAMPLE.
+# * IRR_EXAMPLE2_V4:    allowed IPv4 prefixes queried from AS64500:AS-EXAMPLE2 defined in the RIPE database.
+# * IRR_EXAMPLE2_ASN:   allowed ASNs queried from RIPE::AS64500:AS-EXAMPLE2.
+# * IRR_EXAMPLE3_V6:    allowed IPv6 prefixes queried from AS-EXAMPLE3 defined in the ARIN database.
+# * IRR_EXAMPLE3_ASN:   allowed ASNs queried from ARIN::AS-EXAMPLE3.
diff --git a/make-irr-filter b/make-irr-filter
new file mode 100755
index 0000000..dd29d56
--- /dev/null
+++ b/make-irr-filter
@@ -0,0 +1,28 @@
+#!/bin/bash
+set -euo pipefail
+
+FILTER_SOURCE=/etc/bird/irr-filters
+FILTER_OUTPUT=/etc/bird/filter_irr.conf
+
+tmpfile="$(mktemp /tmp/bird-filter.XXXXXX)"
+cleanup() {
+  rm -f "$tmpfile"
+}
+trap cleanup EXIT
+
+grep -v '^#' "$FILTER_SOURCE" | while IFS=$'\t ' read -r -a line; do
+  name="${line[0]:-}"
+  asset="${line[1]:-}"
+  protocol="${line[2]:-}"
+  if [ -z "$name" ] || [ -z "$asset" ]; then
+    echo "Malformed line found" 1>&2
+    continue
+  fi
+
+  [ "$protocol" != v6 ] && bgpq4 -4Ab -m24 "$asset" -l "define IRR_${name}_V4"
+  [ "$protocol" != v4 ] && bgpq4 -6Ab -m48 "$asset" -l "define IRR_${name}_V6"
+  bgpq4 -tb "$asset" -l "define IRR_${name}_ASN"
+done > "$tmpfile"
+
+mv "$tmpfile" "$FILTER_OUTPUT"
+chmod a+r "$FILTER_OUTPUT"
diff --git a/skeleton.conf b/skeleton.conf
new file mode 100644
index 0000000..04b0fcf
--- /dev/null
+++ b/skeleton.conf
@@ -0,0 +1,41 @@
+log syslog all;
+
+# FIXME: Change this to one of your router's IPv4 addresses.
+# If you have none, pick something random from 240.0.0.0/4.
+router id 192.0.2.1;
+
+protocol kernel {
+    scan time 60;
+    ipv4 {
+        export where source != RTS_STATIC;
+    };
+}
+
+protocol kernel {
+    scan time 60;
+    ipv6 {
+        export where source != RTS_STATIC;
+    };
+}
+
+protocol device {
+    scan time 60;
+}
+
+include "filter_bgp.conf";
+
+protocol static node_v4 {
+    ipv4 {};
+}
+
+protocol static node_v6 {
+    ipv6 {};
+}
+
+protocol static node_v4_anycast {
+    ipv4 {};
+}
+
+protocol static node_v6_anycast {
+    ipv6 {};
+}