From 803c85906d7a506c39040af758b40a33608c8ed9 Mon Sep 17 00:00:00 2001 From: Quantum Date: Sat, 16 Sep 2023 22:29:30 -0400 Subject: [PATCH] Initial commit --- README.md | 245 +++++++++++++++++++++++++++++++++++++ filter_bgp.conf | 288 ++++++++++++++++++++++++++++++++++++++++++++ irr-filters.example | 12 ++ make-irr-filter | 28 +++++ skeleton.conf | 41 +++++++ 5 files changed, 614 insertions(+) create mode 100644 README.md create mode 100644 filter_bgp.conf create mode 100644 irr-filters.example create mode 100755 make-irr-filter create mode 100644 skeleton.conf 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 {}; +}