Routing with BIRD

Routing with BIRD

As I mentioned in previous post I will like to dive into some Linux traffic routing and forwarding. So today we will be looking at one of routing daemons available for *nix OSes – BIRD.

What it is

BIRD is a routing daemon targeted at Linux and BSD distros supported by cz.nic, Czech domain name registry, which are responsible for a number of other very interesting projects [1]. Daemon support both IPv4 and IPv6, number of routing protocols (BGP, RIP, OSPF, Babel) and some extra features like multiple routing tables, static routes, IPv6 RA control and BFD [2]. BIRD is quite popular because of its configuration flexibility (especially BGP filters). Wikipedia states that some largest exchange points in the world use BIRD on they route servers [3]. Currently BIRD is provided in two flavours (or versions): 1.6 and 2.0. What’s difference? From project start there were distinction between IPv4 and IPv6 routing, so if you using version 1.6 you have two different processes (bird and bird6) which I find quite annoying. Latest version has both protocols merged in one binary and config file. So apart of major distro maintainers reluctance to package BIRD 2.0 there are no point in choosing 1.6 if you start fresh. Probably not so good part of BIRD is it’s documentation which I found kinda hard to comprehend. It is good starting point, but when I start to dive deeper I struggled to grasp some topics. There are a wiki too [4], but it’s probably dead.

Some concepts

When someone says “protocol” in terms of routing you probably think OSPF (or BGP). But speaking of BIRD, “protocol” here is really an instance of a routing protocol, or maybe a process in terms of Cisco. If I got two BGP sessions with upstream providers named “bgp_isp1” and “bgp_isp2” for example, I got 2 protocols running by BIRD. Protocols on one side listen on interfaces (Babel, OSPF, RIP) or sockets (BGP) and on the other binds to routing tables through channels. Channel is a way of transferring route records of one type to routing table. Most of the time you have “ipv4” and “ipv6” channels which transfer IPv4 and IPv6 routes respectively. But you can have other type of channels like “vpnv6 mpls” or “ipv4 multicast”. Two protocols support no channels – BFD and Device, both of theme not really a routing protocols. Each channel support input and output filters to restrict some routing information. For example imagine that we have iBGP connection which we named “ibgp_to_r2”, this is our protocol, we connect over IPv4 but exchange both v4 and v6 routes, so our protocol has two channels for both of it. And for both channels we define filters for example for “ipv4” channel it would be “ACCEPT_ALL” for input and “IBGP4_OUT” for output and for “ipv6” channel “ACCEPT_ALL” on input and “IBGP6_OUT” for output. Makes sense? But that’s routing only, BIRD is a routing daemon, remember? It’s sufficient to build a route server or route reflector, but how we can forward traffic through our Linux box? Forwarding is a matter of Linux kernel, so we must provide it with forwarding information. It is done in BIRD by means of special protocol – “Kernel”. It’s binds not to interfaces or sockets but to Linux API and synchronize BIRD routing tables to Linux’s ones. Think about it like downloading path information from RIB to FIB, so Linux kernel works with FIB and forward traffic. You can read more on this in “Architecture” chapter of documentation [5].

Setup

So let’s try BIRD in action using Vagrant. For such a task I will build a topology with 4 VMs. I will use Debian, because… don’t know, can’t say I fan of that distro, but why not? Setup will be like this:

routing with bird topology

I gonna use only BGP in this practice session, so there will be eBGP sessions – DBR2<->DBR3 and DBR2<->DBR4, 2 for each pair (v4 and v6). And there will be one iBGP session over IPv6 which will exchange both address family routes.

Get to work

Let’s start with one VM to get a feel of how to install and run BIRD. Create new project directory, switch inside and initialize Vagrant environment with vagrant init. For one Debian machine Vagrantfile will looks like:

Vagrant.configure("2") do |config|
config.vm.box = "generic/debian9"
end

Get this box run and SSH into it. Now, if we try to find BIRD package in official repository we can see only this (output truncated):

vagrant@debian9:~$ apt-cache showpkg bird
Package: bird
Versions:
1.6.3-2 (/var/lib/apt/lists/ftp.us.debian.org_debian_dists_stretch_main_binary-amd64_Packages)

Mehhh… 1.6 only, but it will not stop us. Let’s build BIRD from source code! To do this we need one additional package which absent from default Debian 9 VM and while we installing packages I prefer to get vim onboard. But if you use another text editor you can skip it (but don’t skip libreadline).

sudo apt-get install libreadline-dev vim

Now we need to download latest source code, unpack it, compile and install. Give attention to config options, I will change some directories in which BIRD put it’s files, so it will not land it’s binaries into some directory not included in PATH.

wget ftp://bird.network.cz/pub/bird/bird-2.0.2.tar.gz
tar xf bird-2.0.2.tar.gz
cd bird-2.0.2/
./configure --sbindir=/usr/local/bin --sysconfdir=/etc/bird
make
sudo make install
cd

BIRD installed, we can start it right now, but before that I like to make a little adjustment. Since we run Debian 9 which use systemd as it’s init system let’s get some systemd service file to control BIRD process. Writing systemd files is a distinct skill by itself (not a hard one really), so we skip diving in it here and adjust service file from official repo. We start with downloading that bird-1.6 package that we dismiss to install earlier, not installing it, just get the package.

wget http://ftp.us.debian.org/debian/pool/main/b/bird/bird_1.6.3-2_amd64.deb
ar vx bird_1.6.3-2_amd64.deb
unxz data.tar.xz
tar xf data.tar

ar vx command will unfold Debian package contents, pulling them out. Then unxz will uncompress archive with data files. Finally tar xf will unpack it. Now we have “etc”, “lib” and “usr” directories with package related files. We interested in bird.service file lying in lib/systemd/system, let’s use sed to adjust it’s contents.

sudo bash -c "sed 's/usr\/sbin/usr\/local\/bin/' lib/systemd/system/bird.service > /etc/systemd/system/bird.service"

What we done here is replacing any occurrence of /usr/sbin in that file to /usr/local/bin, where we installed BIRD binaries before, not making changes in original file, but saving them into /etc/systemd/system, where custom systemd services must go. Why wrap that construction into bash -c? Because otherwise sudo will not affect output redirection and saving that file will not work. Of course there are other methods to do this, but this one is one line long and will be useful for us later. That’s still not enough, we need some more files from that package. Let’s copy them:

sudo cp etc/bird/envvars /etc/bird/
sudo cp -r usr/lib/bird /usr/lib/

We need them because they mentioned in service file, first one contain variables used for BIRD start, second one is a directory with one script only that check for some prerequisites before starting daemon. Last stroke, we need user “bird” in group “bird” to run our daemon. Create it:

sudo useradd -s /bin/none -U bird

And now we are ready to run latest BIRD with systemd!

sudo systemctl start bird.service
systemctl status bird.service
sudo birdc show proto

Second command will show you daemon status and third will print out all configured protocols and they status. Kinda long way to get up and running, but who said that would be easy? But repeating all that hassle on three more VMs? No way! Let use some help from Vagrant here and do provision on our machines. But first let’s get out of that VM, destroy it and rewrite our Vagrantfile.

Vagrant.configure("2") do |config|
config.vm.define "r1" do |r1|
r1.vm.box = "generic/debian9"
r1.vm.hostname = "DBR1"
r1.vm.network "private_network", ip: "192.168.12.1", virtualbox_intnet: "nix12"
r1.vm.provision "shell", path: "deb_bird_prov"
end

config.vm.define "r2" do |r2|
r2.vm.box = "generic/debian9"
r2.vm.hostname = "DBR2"
r2.vm.network "private_network", ip: "192.168.12.2", virtualbox_intnet: "nix12"
r2.vm.network "private_network", ip: "10.1.23.2", virtualbox_intnet: "nix23"
r2.vm.network "private_network", ip: "10.1.24.2", virtualbox_intnet: "nix24"
r2.vm.provision "shell", path: "deb_bird_prov"
end

config.vm.define "r3" do |r3|
r3.vm.box = "generic/debian9"
r3.vm.hostname = "DBR3"
r3.vm.network "private_network", ip: "10.1.23.3", virtualbox_intnet: "nix23"
r3.vm.provision "shell", path: "deb_bird_prov"
end

config.vm.define "r4" do |r4|
r4.vm.box = "generic/debian9"
r4.vm.hostname = "DBR4"
r4.vm.network "private_network", ip: "10.1.24.4", virtualbox_intnet: "nix24"
r4.vm.provision "shell", path: "deb_bird_prov"
end
end

If you read my last post or just familiar with Vagrant you already know what’s going on here. This config will create out topology with 4 VMs, add required number of interfaces and assign IPv4 addresses to them, quite simple. The most interesting part is provisioning. I will use simple bash script to prepare all VMs as routers, listing is going next.

#! /usr/bin/env bash

sudo sysctl net.ipv4.ip_forward=1
sudo sysctl net.ipv6.conf.all.disable_ipv6=0
sudo sysctl net.ipv6.conf.all.forwarding=1
case $(hostname) in
DBR1)
addr_octet=1
first_int=1122
lo_addrs=("2001:db8:1111:1::1/64" "2001:db8:1111:2::1/64" "2001:db8:1111:3::1/64" "2001:db8:1111:4::1/64")
;;
DBR2)
addr_octet=2
first_int=1122
second_int=2233
third_int=2244
;;
DBR3)
addr_octet=3
first_int=2233
lo_addrs=("2001:db8:3333:1::3/64" "2001:db8:3333:2::3/64")
;;
DBR4)
addr_octet=4
first_int=2244
lo_addrs=("2001:db8:4444:1::4/64" "2001:db8:4444:2::4/64")
;;
*)
echo "unknown hostname"
;;
esac
sudo ip addr add 172.20.$addr_octet.$addr_octet/32 dev lo
sudo ip addr add 2001:db8:eeee:$addr_octet::$addr_octet/64 dev lo
sudo ip addr add 2001:db8:$first_int::$addr_octet/64 dev eth1
if [[ $(hostname) = DBR2 ]]; then
sudo ip addr add 2001:db8:$second_int::$addr_octet/64 dev eth2
sudo ip addr add 2001:db8:$third_int::$addr_octet/64 dev eth3
else
sudo ip link add name dummy0 type dummy
sudo ip link set dummy0 up
for addr in "${lo_addrs[@]}"; do
sudo ip addr add "$addr" dev dummy0
done
fi
sudo apt -y install vim libreadline-dev
wget ftp://bird.network.cz/pub/bird/bird-2.0.2.tar.gz
tar xf bird-2.0.2.tar.gz
cd bird-2.0.2/
./configure --sbindir=/usr/local/bin --sysconfdir=/etc/bird
make
sudo make install
cd
wget http://ftp.us.debian.org/debian/pool/main/b/bird/bird_1.6.3-2_amd64.deb
ar vx bird_1.6.3-2_amd64.deb
unxz data.tar.xz
tar xf data.tar
sudo bash -c "sed 's/usr\/sbin/usr\/local\/bin/' lib/systemd/system/bird.service > /etc/systemd/system/bird.service"
sudo cp etc/bird/envvars /etc/bird/
sudo cp -r usr/lib/bird /usr/lib/
sudo useradd -s /bin/none -U bird
sudo systemctl start bird.service

Even if you have never prior experience with bash scripting this one probably will not offend you. It looks like list of commands, which it really is! Apart of some flow logic (if/else, case) of course. So what it is all about? This script will first adjust some kernel settings (sysctl part), enabling IPv6 protocol and allowing traffic forwarding for both IP versions. Then it assigns all required IPv6 addresses depending on machine hostname and even add loopback addresses. Dummy interface is a way to add additional loopbacks on Linux machine, we will use that addresses to announce between our peers later. After that it will perform BIRD installation and start it, same thing we done manually previously. If your start all VMs now you will get our whole topology. Or you can enable them one by one, it’s up to you. While provisioning running you will see it’s output into console, there can be many of it, and some of it may be coloured red, but just ignore it for now. Finally, let us turn our attention to BIRD.
You can control BIRD connecting to it with birdc command, falling into daemon CLI where you can issue commands and use tabulation/question mark. You can start and stop protocols from here, query RIB, show filters and protocol information, but no config is done from here. As an alternative you can just provide your commands as arguments to birdc, not descending into CLI, as we done previously with birdc show proto. One useful thing is globbing (or pattern matching), so you can for example disable both “ibgp_r1” and “ebgp_r3” with birdc ‘disable “*bgp_*”‘ command. If you execute show proto now you will see output like that:

vagrant@DBR1:~$ sudo birdc show proto
BIRD 2.0.2 ready.
Name Proto Table State Since Info
device1 Device --- up 11:26:45.927
direct1 Direct --- down 11:26:45.927
kernel1 Kernel master4 up 11:26:45.927
kernel2 Kernel master6 up 11:26:45.927
static1 Static master4 up 11:26:45.927

Configuration

As you see there are some protocols already preconfigured and connected to routing tables. Static will not require introduction and I told you about kernel before. But what with that “Device” and “Direct”? Device protocol used for network interface scanning, it will not generate any routes and it is not have any channels associated, it just notify BIRD about system interfaces state. Direct protocol used to import connected routes, sometimes you need it, sometimes you don’t. But most of the time you probably don’t want to import connected routes via “Direct” and than sync them to kernel via “Kernel”. Because, obviously, kernel will better handle such routes itself. Consult BIRD documentation on where it’s appropriate to use it, but we will enable it later. It’s probably time to start configure our BIRD, so let’s edit it’s config file on DBR1 (located at /etc/bird/bird.conf). I prefer to clean it up (delete) and get a clean slate like this:

log syslog all;

filter EXPORT_KERNEL {
if source = RTS_DEVICE then
reject;
if dest = RTD_UNREACHABLE then
reject;
accept;
}

protocol device {
}

protocol direct {
ipv4;
ipv6;
interface "-eth0", "*";
}

protocol kernel {
persist;
ipv4 { export filter EXPORT_KERNEL; };
}

protocol kernel {
persist;
ipv6 { export filter EXPORT_KERNEL; };
}

Let’s go through it. First line is default log settings, we are not interested in playing with it, but it better to have some logs, so I leave that line here. Next we define our first filter! What it does is rejecting any directly connected routes (“RTS_DEVICE” value is somehow contradictory to fact that such routes will be imported with “Direct” protocol, not “Device”) and routes which destination is unreachable. Ignore that “RTS_*” and “RTD_*” constructions (and whole filter logic and operations) for now. All other routes will be accepted. We use this filter later in our config. Next we enable device protocol with default config, and direct protocol for both IP versions. But, we omit “eth0” interface from it’s control, because that interface used by Vagrant for our SSHing inside the box, remember? Than we enable kernel protocol 2 times, one for each IP version. As you can see we define few protocols already, but didn’t give them any name, in such case BIRD will resolve names automatically, enumerating them. Config for both kernel protocols are same. “persist” keyword means didn’t delete routes from Linux on BIRD shutdown. And export line allow us use our filter defined earlier, so we didn’t allow connected routes sink backwards and stop unreachable routes. By default kernel protocol will import all routes in BIRD. To check your config simply run bird -p (bird, not birdc!). And if it will not bark at you, reconfigure it with sudo birdc configure, you don’t need to restart daemon at all. Now you can query BIRD RIB for available routes with sudo birdc show route.
Quite good for start. Now let’s configure our first BGP protocol on DBR1. And since it’s internal one, let’s use link-local address for it. Assign eth1 terse and deterministic link-local address with:

sudo ip addr add fe80::eeee:1/64 dev eth1

And now add this to our BIRD config:

router id 172.20.1.1;

protocol bgp internal_r2 {
local fe80::eeee:1 as 65000;
neighbor fe80::eeee:2%eth1 as 65000;
direct;
ipv6 {
import all;
export all;
import keep filtered on;
};
ipv4 {
import all;
export all;
import keep filtered on;
};
}

Going from top to bottom. First we assign distinctive router ID to our box (using our IPv4 loopback address), then we define protocol named “internal_r2” of type BGP. Inside you can see addresses and AS declared with “%eth1” in neighbor statement, so BIRD know where to find this link-local. “direct” told BIRD that this session can be negotiated over direct connection only. Such a precaution redundant in our case because of link-local addresses but can be useful with global ones if path to neighbor somehow become indirect. Next followed 2 identical channels definition for both IP versions. We going unsafe here allowing any routes in and out, but it’s OK since we just labbing, plus it’s internal session. Third line allows BIRD to remember any routes that it filtered out, somewhat similar to Cisco’s “software reconfiguration inbound”. If you now apply same config (with some adjustments of course) to DBR2 and add appropriate link-local address to it’s eth1, you will see that our protocol is in “Established” state.

vagrant@DBR2:~$ sudo birdc show proto
BIRD 2.0.2 ready.
Name Proto Table State Since Info
device1 Device --- up 04:47:37.883
direct1 Direct --- up 04:48:39.901
kernel1 Kernel master4 up 04:47:37.883
kernel2 Kernel master6 up 04:47:37.883
internal_r1 BGP --- up 05:02:56.037 Established

It’s probably good time to discover some BIRD querying commands.

 * show protocols all [PROTOCOL] – provide you with verbose information on protocols state
 * show route PROTOCOL – will show you all routes learned by protocol
 * show route IP||PREFIX – will query RIB for information on available route to IP or prefix, adding “all” at the end will show more information
 * show route export PROTOCOL – show you all routes advertised out to neighbor by that protocol
 * show route preexport PROTOCOL – show you all routes that can be advertised out if filter wasn’t applied
 * show route noexport PROTOCOL – show you all routes that can be advertised out, but filtered
 * show route filtered – routes advertised to our router (in), but filtered (work only when “import keep filtered” activated)
 * show route filter FILTER – routes that pass predefined filter (useful for checking yourself)
 * disable||enable PROTOCOL – disables or enables protocol
 * reload [in|out] PROTOCOL – reload or re-import and re-export all routes to/from that protocol
 * configure soft – reload configuration but don’t affect already learned routes if filters change, new (changed) filters will only apply to new routes
 * configure TIMEOUT – allows BIRD to automatically rollback if you don’t do configure confirm

That is not a full list and not a full form of commands, but I found that most useful. You will find some commands translated from Cisco-speak at a project wiki [6]. Now let’s descend into more complicated and more exciting configuration topics and build other BGP sessions on our DBR2.

Filtering it out

Since we are going to build external sessions, now we need to control what routes goes in an out, so we need to acquaint how filters and functions work. But let us start with less complicated but very helpful feature – a templates. Template is a way to create many similar protocol instances and not repeat yourself. If you gonna define 10 eBGP sessions all of them can contain identical statements, so why not to ease our work and create a template. All protocols built from template will inherit statements from template, but of course you can redefine them.

template bgp external_bgp {
local as 65000;
direct;
hold time 30;
}

template bgp external_v4 from external_bgp {
ipv4 {
import limit 100;
export limit 50;
};
}

template bgp external_v6 from external_bgp {
ipv6 {
import limit 50;
export limit 25;
};
}

This is our templates on DBR2. As you can see they look similar to real protocols definitions, but last two templates are build from first! First template define BGP instance with local AS, tweaked hold time and add “direct” statement. Second template built on top of first one, but add ipv4 channel definition to it with limits for both import and export (I took numbers out of nowhere). Third one do similar stuff. Now we can use last two templates to build v4 and v6 BGP sessions respectively.

protocol bgp external_r3_v6 from external_v6 {
neighbor 2001:db8:2233::3 as 65005;
ipv6 {
import filter IMPORT_R3;
export filter EXPORT_R3;
};
}

protocol bgp external_r3_v4 from external_v4 {
neighbor 10.1.23.3 as 65005;
ipv4 {
import none;
export filter EXPORT_R3;
};
}

protocol bgp external_r4_v6 from external_v6 {
neighbor 2001:db8:2244::4 as 65010;
ipv6 {
import filter IMPORT_R4;
export filter EXPORT_R4;
};
}

protocol bgp external_r4_v4 from external_v4 {
neighbor 10.1.24.4 as 65010;
ipv4 {
import none;
export filter EXPORT_R4;
};
}

Here we configured 4 eBGP sessions, 2 for every peer, 2 for every AF. I deliberately put “import none” into ipv4 sessions, because our attention will be on ipv6 ones. But you can do that importing yourself. You can’t use this config now, because we didn’t define our filters yet. Here is it’s full config:

define 'my_v4_nets' = [ 172.20.1.1/32, 172.20.2.2/32 ];
define 'my_v6_nets' = [ 2001:db8:eeee:1::/64, 2001:db8:eeee:2::/64, 2001:db8:1122::/64, 2001:db8:1111::/48+];
define 'as65005_v6_nets' = [ 2001:db8:3333::/48+, 2001:db8:eeee:3::/64 ];
define 'as65010_v6_nets' = [ 2001:db8:4444::/48+, 2001:db8:eeee:4::/64 ];

function CHECK_MY_ROUTES() {
if source = RTS_DEVICE then
accept;
if net.type = NET_IP4 then
return net ~ 'my_v4_nets';
if net.type = NET_IP6 then
return net ~ 'my_v6_nets';
else {
print "Unexpected condition";
return false;
}
}

filter IMPORT_R3 {
if net ~ 'as65005_v6_nets' then
accept;
reject;
}

filter EXPORT_R3 {
if CHECK_MY_ROUTES() then
accept;
if net ~ 'as65010_v6_nets' then
accept;
reject;
}

filter IMPORT_R4 {
if net ~ 'as65010_v6_nets' then
accept;
reject;
}

filter EXPORT_R4 {
if CHECK_MY_ROUTES() then
accept;
if net ~ 'as65005_v6_nets' then
accept;
reject;
}

What I did not told you before that BIRD has simple programming language inside. Too simple to send you emails, but good enough to make filtering flexible and handy. That’s probably a major BIRD selling point and a reason why engineers choose it to build route servers. So let’s go through this config. First we define some constants we can use later, it is convenient way to type less. Here I defined sets of prefixes. You can define many other things, for example:

define MYAS = 65000;

If you use symbols in constant name (like I did) you need to put apostrophes around. Speaking of prefix sets, you can define exact prefix (2001:db8:eeee:4::/64), prefix that will match longer lengths (2001:db8:4444::/48+), shorter lengths (2001:db8:4444::/48-), or length within range (2001:db8:4444::/48{56,64}). You can mix IPv4 prefixes with IPv6, but it’s not recommended.
Filters and functions has quite same look and feel. Both can have variable definitions inside, both can use conditional statements (if and case), both can manipulate route attributes. So what difference? Filter purpose is to accept or reject route, so it must define what action do we take on a route. Filter implicitly gets one route a time, probably matching goes parallel undercover, but operationally think about one route than next, then next… In the end filter must decide either accept or reject for every route. Function is reusable block of code, it can be called from any filter. Filters implicitly pass route to any function it called, but unlike filters functions can have explicit parameters, that you can pass to it (neighbor AS number for example). Moreover functions can return variables, most often true or false booleans. All defined variables must be of some type, so if you want to declare it in function of filter, you must define type. Types goes from classic bool, string, int to more routing related prefix, vpnrd to some more complex pair, ec, clist. Look for all types and they definitions in documentation. For example you can define variables like that:

int number;
ip local_ip;

Then you can assign values to them. Using functions and filters you will operate with “route” object, which have some attributes. If you know programming, think of them like a big number of parameters passed to function (of filter). Every route have same set of attributes of some type and probably have some more depending on protocol from where it come. You can check that attributes with conditional statements, assign them new values (probably using common math operations). Very interesting operation is “~”, which checks if variable is an element of set. Access to undefined attributes can result in an error, so you probably what to check if it defined:

if defined ( rip_metric ) then

Most of common route attributes is:

TypeNameDefinition
prefixnetnetwork prefix
ipfromrouter from which announce came
ipgwnext-hop
stringprotoname of protocol from which route came
enumsourcetype of protocol from which route came, some possible values like RTS_STATIC, RTS_DEVICE, RTS_OSPF
enumdestwhere that route goes, some possible values RTD_DEVICE, RTD_ROUTER, RTD_UNREACHABLE, RTD_BLACKHOLE
stringifnamename of outgoing interface

That not a full list, but most interesting ones. Depending from what protocol route came from it can have additional attributes, like for example bgp_med, krt_source or ospf_tag. Some attributes read-only, some assignable, some can be accessed for more attributes (like objects in programming), for example net.type (address family), net.len (prefix length) or bgppath.first (first ASN in AS PATH sequence). Once again, I’m not going to repeat full documentation here.
So, taking all that into account, what we’ve done in DBR2 config? Import filters will just check prefix (net variable) on belonging to our defined sets, to accept only valid neighbor routes. Export filters will call function CHECK_MY_ROUTES, which instantly accept any directly connected routes and then do the same check as in import filters, but for both IPv4 and IPv6, depending on prefix type. Looks not so complicated now, when you know where that “net” come from, right? Next, let’s configure our DBR3 and DBR4. Full config for DBR3:

log syslog all;

filter EXPORT_KERNEL {
if source = RTS_DEVICE then
reject;
if dest = RTD_UNREACHABLE then
reject;
accept;
}

protocol device {
}

protocol direct {
ipv4;
ipv6;
interface "-eth0", "*";
}

protocol kernel {
persist;
ipv4 { export filter EXPORT_KERNEL; };
}

protocol kernel {
persist;
ipv6 { export filter EXPORT_KERNEL; };
}

router id 172.20.3.3;

define 'my_v4_nets' = [ 172.20.3.3/32 ];
define 'my_v6_nets' = [ 2001:db8:eeee:3::/64, 2001:db8:3333::/48+ ];
define 'as65000_v6_nets' = [ 2001:db8:eeee:1::/64, 2001:db8:eeee:2::/64, 2001:db8:1122::/64, 2001:db8:1111::/48+, 2001:db8:4444::/48+, 2001:db8:2244::/64 ];

function CHECK_MY_ROUTES() {
if net.type = NET_IP4 then
return net ~ 'my_v4_nets';
if net.type = NET_IP6 then
return net ~ 'my_v6_nets';
else {
print "Unexpected condition";
return false;
}
}

filter IMPORT_R2 {
if net ~ 'as65000_v6_nets' then
accept;
else
reject;
}

filter EXPORT_R2 {
if CHECK_MY_ROUTES() then
accept;
else
reject;
}

protocol bgp external_r2_v6 {
local 2001:db8:2233::3 as 65005;
neighbor 2001:db8:2233::2 as 65000;
direct;
ipv6 {
import filter IMPORT_R2;
export filter EXPORT_R2;
import limit 100;
export limit 50;
};
}

protocol bgp external_r2_v4 {
local 10.1.23.3 as 65005;
neighbor 10.1.23.2 as 65000;
ipv4 {
import none;
export filter EXPORT_R2;
};
}

And DBR4:

log syslog all;

filter EXPORT_KERNEL {
if source = RTS_DEVICE then
reject;
if dest = RTD_UNREACHABLE then
reject;
accept;
}

protocol device {
}

protocol direct {
ipv4;
ipv6;
interface "-eth0", "*";
}

protocol kernel {
persist;
ipv4 { export filter EXPORT_KERNEL; };
}

protocol kernel {
persist;
ipv6 { export filter EXPORT_KERNEL; };
}

router id 172.20.4.4;

define 'my_v4_nets' = [ 172.20.4.4/32 ];
define 'my_v6_nets' = [ 2001:db8:eeee:4::/64, 2001:db8:4444::/48+ ];
define 'as65000_v6_nets' = [ 2001:db8:eeee:1::/64, 2001:db8:eeee:2::/64, 2001:db8:1122::/64, 2001:db8:1111::/48+, 2001:db8:3333::/48+, 2001:db8:2233::/64 ];

function CHECK_MY_ROUTES() {
if net.type = NET_IP4 then
return net ~ 'my_v4_nets';
if net.type = NET_IP6 then
return net ~ 'my_v6_nets';
else {
print "Unexpected condition";
return false;
}
}

filter IMPORT_R2 {
if net ~ 'as65000_v6_nets' then
accept;
else
reject;
}

filter EXPORT_R2 {
if CHECK_MY_ROUTES() then
accept;
else
reject;
}

protocol bgp external_r2_v6 {
local 2001:db8:2244::4 as 65010;
neighbor 2001:db8:2244::2 as 65000;
direct;
ipv6 {
import filter IMPORT_R2;
export filter EXPORT_R2;
import limit 100;
export limit 50;
};
}

protocol bgp external_r2_v4 {
local 10.1.24.4 as 65010;
neighbor 10.1.24.2 as 65000;
ipv4 {
import none;
export filter EXPORT_R2;
};
}

Now we have connectivity between every router and IPv6 prefix in our topology. As you can see I named all routes that can come from DBR2 as ‘as6500_v6_nets’, that is not semantically correct but nevertheless do it job right. Looks good, but it wouldn’t be fun to stop there, is it? Let’s apply some more actions in our filters. Let DBR2 aggregate prefixes when it announce it to DBR3, let it filter some prefix from DBR4 and apply MED on incoming announces from DBR3, if it’s absent. And let DBR4 do some AS path prepends. I’ll start with DBR2 config adjustments, first – with prefix aggregation. There’s no way to say BIRD to do summarization (aggregation), so we will go with static route which will point to blackhole. Then we import it to DBR3. To accomplish this we need to configure protocol of type static!

protocol static {
ipv6;
route 2001:db8:1111::/48 blackhole;
}

And augment EXPORT_R3 filter to:

filter EXPORT_R3 {
if net ~ 'as65010_v6_nets' then
accept;
if source = RTS_STATIC then
accept;
reject;
}

This filter is kinda blunt, it’s better to go more granular with prefixes in real network, but it’s OK in our lab. Now DBR3 will receive only AS65000 supernet and all AS65010 routes. Doing that, I killed all v4 export, but you can try and fix it if you want. And of course that filter conceal DBR1<=>DBR2 link prefix, but it’s not a big deal too. It would be better not to use such prefix ourselves at all, so let’s update EXPORT_KERNEL filter too:

filter EXPORT_KERNEL {
    if source = RTS_DEVICE then
        reject;
    if dest = RTD_UNREACHABLE then
        reject;
    if dest = RTD_BLACKHOLE then
        reject;
    accept;
}

Next let’s filter one route out of DBR4 announcements. We can just tweak as65010_v6_nets for that task, but it’s not so elegant. Instead, let’s create set of prefixes to ban (with just one prefix, yes). And update IMPORT_R4 filter.

define 'ban_nets' = [ 2001:db8:4444:2::/64 ];

filter IMPORT_R4 {
    if net ~ 'ban_nets' then
        reject;
    if net ~ 'as65010_v6_nets' then
        accept;
    reject;
}

Now we still accepting all AS65010 routes, except for that we want to ban. We can go further and accept such a route to advertise it to DBR3, but not to import it in our FIB (Linux kernel), but I will not do it here, you probably already know how to do it, if you want to. Last thing on DBR2 – we want to set MED to some value if there’s none on AS65005 routes. So we need to update our IMPORT_R3 filter. Checking for MED = 0 wouldn’t work, because no MED doesn’t mean zero MED. That’s how we do it:

filter IMPORT_R3 {
if ! defined( bgp_med ) then
bgp_med = 1200;
if net ~ 'as65005_v6_nets' then
accept;
reject;
}

Now if you check any routes imported from DBR3 you will see 1200 MED on it, we gonna fix it on DBR3 shortly.

vagrant@DBR2:~$ sudo birdc show route all 2001:db8:3333:2::/64
BIRD 2.0.2 ready.
Table master6:
2001:db8:3333:2::/64 unicast [external_r3_v6 08:13:44.277] * (100) [AS65005i]
via 2001:db8:2233::3 on eth2
Type: BGP univ
BGP.origin: IGP
BGP.as_path: 65005
BGP.next_hop: 2001:db8:2233::3 fe80::a00:27ff:fec6:fd98
BGP.med: 1200
BGP.local_pref: 100

Now, update EXPORT_R2 on DBR3. I choose some route randomly:

filter EXPORT_R2 {
if net = 2001:db8:3333:1::/64 then
bgp_med = 30;
if CHECK_MY_ROUTES() then
accept;
else
reject;
}

And now you can see that this route has MED 30 on DBR2, while previous one is still with MED 1200:

vagrant@DBR2:~$ sudo birdc show route all 2001:db8:3333:1::/64
BIRD 2.0.2 ready.
Table master6:
2001:db8:3333:1::/64 unicast [external_r3_v6 08:32:51.276] * (100) [AS65005i]
via 2001:db8:2233::3 on eth2
Type: BGP univ
BGP.origin: IGP
BGP.as_path: 65005
BGP.next_hop: 2001:db8:2233::3 fe80::a00:27ff:fec6:fd98
BGP.med: 30
BGP.local_pref: 100

Our last task here is to do some outgoing prepends on DBR4. You probably know what we gonna update:

filter EXPORT_R2 {
bgp_path.prepend(65010);
if CHECK_MY_ROUTES() then
accept;
else
reject;
}

Now all routes exported to DBR2 from DBR4 will be prepended with another 65010. I didn’t find a way to prepend twice or more other than repeat same statement. And that’s all!

Full final configs:

DBR1

log syslog all;

filter EXPORT_KERNEL {
if source = RTS_DEVICE then
reject;
if dest = RTD_UNREACHABLE then
reject;
accept;
}

protocol device {
}

protocol direct {
ipv4;
ipv6;
interface "-eth0", "*";
}

protocol kernel {
persist;
ipv4 { export filter EXPORT_KERNEL; };
}

protocol kernel {
persist;
ipv6 { export filter EXPORT_KERNEL; };
}

router id 172.20.1.1;

protocol bgp internal_r2 {
local fe80::eeee:1 as 65000;
neighbor fe80::eeee:2%eth1 as 65000;
direct;
ipv6 {
import all;
export all;
import keep filtered on;
};
ipv4 {
import all;
export all;
import keep filtered on;
};
}

DBR2

log syslog all;

filter EXPORT_KERNEL {
if source = RTS_DEVICE then
reject;
if dest = RTD_UNREACHABLE then
reject;
if dest = RTD_BLACKHOLE then
reject;
accept;
}

protocol device {
}

protocol direct {
ipv4;
ipv6;
interface "-eth0", "*";
}

protocol kernel {
persist;
ipv4 { export filter EXPORT_KERNEL; };
}

protocol kernel {
persist;
ipv6 { export filter EXPORT_KERNEL; };
}

router id 172.20.2.2;

protocol bgp internal_r1 {
local fe80::eeee:2 as 65000;
neighbor fe80::eeee:1%eth1 as 65000;
direct;
ipv6 {
import all;
export all;
import keep filtered on;
};
ipv4 {
import all;
export all;
import keep filtered on;
};
}

template bgp external_bgp {
local as 65000;
direct;
hold time 30;
}

template bgp external_v4 from external_bgp {
ipv4 {
import limit 100;
export limit 50;
};
}

template bgp external_v6 from external_bgp {
ipv6 {
import limit 50;
export limit 25;
};
}

define 'my_v4_nets' = [ 172.20.1.1/32, 172.20.2.2/32 ];
define 'my_v6_nets' = [ 2001:db8:eeee:1::/64, 2001:db8:eeee:2::/64, 2001:db8:1122::/64, 2001:db8:1111::/48+];
define 'as65005_v6_nets' = [ 2001:db8:3333::/48+, 2001:db8:eeee:3::/64 ];
define 'as65010_v6_nets' = [ 2001:db8:4444::/48+, 2001:db8:eeee:4::/64 ];
define 'ban_nets' = [ 2001:db8:4444:2::/64 ];

function CHECK_MY_ROUTES() {
if source = RTS_DEVICE then
accept;
if net.type = NET_IP4 then
return net ~ 'my_v4_nets';
if net.type = NET_IP6 then
return net ~ 'my_v6_nets';
else {
print "Unexpected condition";
return false;
}
}

filter IMPORT_R3 {
if ! defined( bgp_med ) then
bgp_med = 1200;
if net ~ 'as65005_v6_nets' then
accept;
reject;
}

filter EXPORT_R3 {
if net ~ 'as65010_v6_nets' then
accept;
if source = RTS_STATIC then
accept;
reject;
}

filter IMPORT_R4 {
if net ~ 'ban_nets' then
reject;
if net ~ 'as65010_v6_nets' then
accept;
reject;
}

filter EXPORT_R4 {
if CHECK_MY_ROUTES() then
accept;
if net ~ 'as65005_v6_nets' then
accept;
reject;
}

protocol bgp external_r3_v6 from external_v6 {
neighbor 2001:db8:2233::3 as 65005;
ipv6 {
import filter IMPORT_R3;
export filter EXPORT_R3;
};
}

protocol bgp external_r3_v4 from external_v4 {
neighbor 10.1.23.3 as 65005;
ipv4 {
import none;
export filter EXPORT_R3;
};
}

protocol bgp external_r4_v6 from external_v6 {
neighbor 2001:db8:2244::4 as 65010;
ipv6 {
import filter IMPORT_R4;
export filter EXPORT_R4;
};
}

protocol bgp external_r4_v4 from external_v4 {
neighbor 10.1.24.4 as 65010;
ipv4 {
import none;
export filter EXPORT_R4;
};
}

protocol static {
ipv6;
route 2001:db8:1111::/48 blackhole;
}

DBR3

log syslog all;

filter EXPORT_KERNEL {
if source = RTS_DEVICE then
reject;
if dest = RTD_UNREACHABLE then
reject;
accept;
}

protocol device {
}

protocol direct {
ipv4;
ipv6;
interface "-eth0", "*";
}

protocol kernel {
persist;
ipv4 { export filter EXPORT_KERNEL; };
}

protocol kernel {
persist;
ipv6 { export filter EXPORT_KERNEL; };
}

router id 172.20.3.3;

define 'my_v4_nets' = [ 172.20.3.3/32 ];
define 'my_v6_nets' = [ 2001:db8:eeee:3::/64, 2001:db8:3333::/48+ ];
define 'as65000_v6_nets' = [ 2001:db8:eeee:1::/64, 2001:db8:eeee:2::/64, 2001:db8:1122::/64, 2001:db8:1111::/48+, 2001:db8:4444::/48+, 2001:db8:2244::/64 ];

function CHECK_MY_ROUTES() {
if net.type = NET_IP4 then
return net ~ 'my_v4_nets';
if net.type = NET_IP6 then
return net ~ 'my_v6_nets';
else {
print "Unexpected condition";
return false;
}
}

filter IMPORT_R2 {
if net ~ 'as65000_v6_nets' then
accept;
else
reject;
}

filter EXPORT_R2 {
if net = 2001:db8:3333:1::/64 then
bgp_med = 30;
if CHECK_MY_ROUTES() then
accept;
else
reject;
}

protocol bgp external_r2_v6 {
local 2001:db8:2233::3 as 65005;
neighbor 2001:db8:2233::2 as 65000;
direct;
ipv6 {
import filter IMPORT_R2;
export filter EXPORT_R2;
import limit 100;
export limit 50;
};
}

protocol bgp external_r2_v4 {
local 10.1.23.3 as 65005;
neighbor 10.1.23.2 as 65000;
ipv4 {
import none;
export filter EXPORT_R2;
};
}

DBR4

log syslog all;

filter EXPORT_KERNEL {
if source = RTS_DEVICE then
reject;
if dest = RTD_UNREACHABLE then
reject;
accept;
}

protocol device {
}

protocol direct {
ipv4;
ipv6;
interface "-eth0", "*";
}

protocol kernel {
persist;
ipv4 { export filter EXPORT_KERNEL; };
}

protocol kernel {
persist;
ipv6 { export filter EXPORT_KERNEL; };
}

router id 172.20.4.4;

define 'my_v4_nets' = [ 172.20.4.4/32 ];
define 'my_v6_nets' = [ 2001:db8:eeee:4::/64, 2001:db8:4444::/48+ ];
define 'as65000_v6_nets' = [ 2001:db8:eeee:1::/64, 2001:db8:eeee:2::/64, 2001:db8:1122::/64, 2001:db8:1111::/48+, 2001:db8:3333::/48+, 2001:db8:2233::/64 ];

function CHECK_MY_ROUTES() {
if net.type = NET_IP4 then
return net ~ 'my_v4_nets';
if net.type = NET_IP6 then
return net ~ 'my_v6_nets';
else {
print "Unexpected condition";
return false;
}
}

filter IMPORT_R2 {
if net ~ 'as65000_v6_nets' then
accept;
else
reject;
}

filter EXPORT_R2 {
bgp_path.prepend(65010);
if CHECK_MY_ROUTES() then
accept;
else
reject;
}

protocol bgp external_r2_v6 {
local 2001:db8:2244::4 as 65010;
neighbor 2001:db8:2244::2 as 65000;
direct;
ipv6 {
import filter IMPORT_R2;
export filter EXPORT_R2;
import limit 100;
export limit 50;
};
}

protocol bgp external_r2_v4 {
local 10.1.24.4 as 65010;
neighbor 10.1.24.2 as 65000;
ipv4 {
import none;
export filter EXPORT_R2;
};
}

Conclusion

As you can see, BIRD has very interesting and flexible filtering system, which allow you to accomplish many complicated tasks in programmatic way. Of course we just scratch a surface here, production configurations a way more lengthy and complicated and there’s many more options and attributes in BIRD. And of course don’t forget that it’s just a routing daemon running on Linux, which make two things very important. First, routing information in BIRD doesn’t mean any forwarding entries in kernel. To understand where your traffic will flow use standard distro tools (ip route on Linux, route on BSD). Second, automation options are unlimited. You can use any of your favourite tools to build up BIRD configuration from scratch and then just reconfigure BIRD with possibilities not to even loose any existent routing information. For example you can do templates with Jinja2 or generate filters out of RIR DB with bgpq3 utility.

Links

[1]: cz.nic projects
[2]: What is BIRD, official documentation
[3]: BIRD article on Wikipedia
[4]: BIRD wiki
[5]: BIRD architecture, official documentation
[6]: Cisco IOS to BIRD commands comparison, BIRD wiki

Comments are closed.