Initial commit

This commit is contained in:
Quantum 2023-09-16 22:29:30 -04:00
commit 803c85906d
5 changed files with 614 additions and 0 deletions

245
README.md Normal file
View file

@ -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

288
filter_bgp.conf Normal file
View file

@ -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;
}
}

12
irr-filters.example Normal file
View file

@ -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.

28
make-irr-filter Executable file
View file

@ -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"

41
skeleton.conf Normal file
View file

@ -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 {};
}