Introducing my bird2 configuration for my Lab AS

mk16.de

I have in the dn42 a Lab AS. AS4242422924. This is an IPv6-only network, which is only on one node. I use it on one side to monitor my main network and on the other side to connect prefixes to GNS3.

I use bird2 as BGP daemon. Today I want to “introduce” my bird2 configuration.

I tried to build the configuration a bit more modular than the configuration of my main network. Therefore the configuration currently consists of five files. bird.conf:

log syslog { warning, error, fatal };
log "/var/log/bird/remote.log" { remote };
log "/var/log/bird/bugs.log" { bug };
log "/var/log/bird/trace.log" { trace };
log "/var/log/bird/debug.log" { debug };
log "/var/log/bird/info.log" { info };

include "header.conf";

/* Set router details */
router id RID;
hostname HOSTNAME;

/* Set tables */
ipv6 table dn42;

/* Utility functions */

function is_self_net() {
  return net ~ OWNNETSET;
}

function is_valid_network() {
  return net ~ [
    fd00::/8{44,64}
  ];
}

/* ROA */

roa6 table dnroa;

# import roa from file
protocol static {
    roa6 {
        table dnroa;
    };
    include "/var/lib/bird/dn42-roa6.conf";
}

include "filters.conf";

/* Export own net */

protocol static ownnet {
    route OWNNET unreachable;

    ipv6 {
        table dn42;
    };
}

/* Kernel */

protocol kernel {
    metric 500;

    ipv6 {
        table dn42;
        export where kernel_export();
    };
}

/* make unknown routes unreachable */

protocol static {
    route fd00::/8 unreachable;

    ipv6 {
        table dn42;
    };
}

protocol bfd { }
protocol device { }

include "templates.conf";

include "peers/*.conf";
log syslog { warning, error, fatal };
log "/var/log/bird/remote.log" { remote };
log "/var/log/bird/bugs.log" { bug };
log "/var/log/bird/trace.log" { trace };
log "/var/log/bird/debug.log" { debug };
log "/var/log/bird/info.log" { info };

So that not everything is written into the syslog and thus becomes unclear, I let write only error messages and warnings into syslog. Other things I let write in log files.

include "header.conf";

After that I import header.conf.

define OWNAS = 4242422924;

define OWNIP = fd00:8e13:ce5d::1;

define OWNNET = fd00:8e13:ce5d::/48;

define OWNNETSET = [fd00:8e13:ce5d::/48+];

define RID = 42.0.29.24;

define HOSTNAME = "srv.dn42-lab.de";

define BANDWIDTH = 25;
define REGION_GEO = 41;
define REGION_COUNTRY = 1276;

In this only variables are defined. Among them my AS number, my IP address, my IP prefix, my router ID, my host name and parameters for the BGP communities. In REGION_GEO and REGION_COUNTRY are the region codes according to the wiki.

/* Set router details */
router id RID;
hostname HOSTNAME;

I will tell this to bird2 in these lines. The hostname does not have to be in bird. However, you can set it to announce the hostname via BGP. I thought that I would try this feature.

/* Set tables */
ipv6 table dn42;

Normally bird uses the tables master4 for IPv4 and master6 for IPv6 routes, but I wanted a cleaner separation and store all dn42 routes in the table dn42.

/* Utility functions */

function is_self_net() {
  return net ~ OWNNETSET;
}

function is_valid_network() {
  return net ~ [
    fd00::/8{44,64}
  ];
}

I took these two functions from the wiki. is_self_net checks if a prefix is in your own network and with is_valid_network you can sort out all non-IPv6 ULAs. Furthermore a maximum and a minimum prefix length is set: /44 to /64.

/* ROA */

roa6 table dnroa;

# import roa from file
protocol static {
    roa6 {
        table dnroa;
    };
    include "/var/lib/bird/dn42-roa6.conf";
}

After that I create a ROA table with the name dnroa. From the ROA file /var/lib/bird/dn42-roa6.conf bird imports everything. I use dn42-roagen to create the ROA files.

include "filters.conf";

Then I include the filters.conf. This contains my import/export filters.

include "community_filter.conf";

define ASN_BLACKLIST = [
0
];

function kernel_export() {
    krt_prefsrc = OWNIP;
    accept;
}

function reject_invalid_roa() {
    if (roa_check(dnroa, net, bgp_path.last) != ROA_VALID) then {
        print "Reject: ROA failed|", net, "|", bgp_path;
        reject;
    }
}

function reject_default_route() {
    if (net = fd00::/8 || net = ::/0) then
        reject;
}

function reject_blacklisted()
int set blacklist;
{
    blacklist = ASN_BLACKLIST;

    if ( bgp_path ~ blacklist ) then {
        print "Reject: blacklisted ASN|", bgp_path;
        reject;
    }
}

function honor_graceful_shutdown() {
    if (65535, 0) ~ bgp_community then {
        bgp_local_pref = 0;
    }
}

function add_own_communities(int link_type) {
    bgp_large_community.add((OWNAS, 5, link_type));
}

function dn_import_filter(int link_latency; int link_bandwidth; int link_crypto; int link_type) {
    if ( net.type != NET_IP6 ) then {
        print "Reject: non-IPv6 on IPv6 Channel|", net, "|", bgp_path;
        reject;
    }

    if ( ! is_valid_network() ) then {
        print "Reject: invalid network|", net, "|", bgp_path;
        reject;
    }

    if ( is_self_net() ) then {
        print "Reject: export our network|", net, "|", bgp_path.first;
        reject;
    }

    if ( bgp_path.len > 25 ) then {
        print "Reject: AS path too long|", net, "|", bgp_path;
        reject;
    }

    reject_blacklisted();
    reject_invalid_roa();
    reject_default_route();

    if (bgp_path.len = 1) then
        bgp_local_pref = bgp_local_pref + 500;

    honor_graceful_shutdown();

    update_flags(link_latency, link_bandwidth, link_crypto);
    add_own_communities(link_type);

    accept;
}

function dn_export_filter(int link_latency; int link_bandwidth; int link_crypto; int link_type) {
    if (source !~ [RTS_STATIC, RTS_BGP]) then
        reject;

    if (bgp_path.last != bgp_path.first) then
        reject;

    reject_default_route();

    update_flags(link_latency, link_bandwidth, link_crypto);
    update_region();

    bgp_med = 0;
    bgp_med = bgp_med + ( ( 4 - ( link_crypto - 30 ) ) * 600 );
    bgp_med = bgp_med + ( ( 9 - ( link_bandwidth - 20 ) ) * 100);
    bgp_med = bgp_med + ( ( link_latency - 1) * 300);
    bgp_med = bgp_med + ( link_type - 1 ) * 10;

    accept;
}

function dn_export_collector() {
    if (source !~ [RTS_STATIC, RTS_BGP]) then
        reject;

    update_region();
    accept;
}

The filter file on the other side includes community_filter.conf. This is a slightly modified copy from the wiki.

function update_latency(int link_latency) {
  bgp_community.add((64511, link_latency));
       if (64511, 9) ~ bgp_community then { bgp_community.delete([(64511, 1..8)]); }
  else if (64511, 8) ~ bgp_community then { bgp_community.delete([(64511, 1..7)]); }
  else if (64511, 7) ~ bgp_community then { bgp_community.delete([(64511, 1..6)]); }
  else if (64511, 6) ~ bgp_community then { bgp_community.delete([(64511, 1..5)]); }
  else if (64511, 5) ~ bgp_community then { bgp_community.delete([(64511, 1..4)]); }
  else if (64511, 4) ~ bgp_community then { bgp_community.delete([(64511, 1..3)]); }
  else if (64511, 3) ~ bgp_community then { bgp_community.delete([(64511, 1..2)]); }
  else if (64511, 2) ~ bgp_community then { bgp_community.delete([(64511, 1..1)]); }
}

function update_bandwidth(int link_bandwidth) {
  bgp_community.add((64511, link_bandwidth));
       if (64511, 21) ~ bgp_community then { bgp_community.delete([(64511, 22..29)]); }
  else if (64511, 22) ~ bgp_community then { bgp_community.delete([(64511, 23..29)]); }
  else if (64511, 23) ~ bgp_community then { bgp_community.delete([(64511, 24..29)]); }
  else if (64511, 24) ~ bgp_community then { bgp_community.delete([(64511, 25..29)]); }
  else if (64511, 25) ~ bgp_community then { bgp_community.delete([(64511, 26..29)]); }
  else if (64511, 26) ~ bgp_community then { bgp_community.delete([(64511, 27..29)]); }
  else if (64511, 27) ~ bgp_community then { bgp_community.delete([(64511, 28..29)]); }
  else if (64511, 28) ~ bgp_community then { bgp_community.delete([(64511, 29..29)]); }
}

function update_crypto(int link_crypto) {
  bgp_community.add((64511, link_crypto));
       if (64511, 31) ~ bgp_community then { bgp_community.delete([(64511, 32..34)]); }
  else if (64511, 32) ~ bgp_community then { bgp_community.delete([(64511, 33..34)]); }
  else if (64511, 33) ~ bgp_community then { bgp_community.delete([(64511, 34..34)]); }
}

function update_flags(int link_latency; int link_bandwidth; int link_crypto) {
    if link_bandwidth > BANDWIDTH then link_bandwidth = BANDWIDTH;

    update_latency(link_latency);
    update_bandwidth(link_bandwidth);
    update_crypto(link_crypto);
}

function update_region() {
    if is_self_net() then {
        bgp_community.add((64511, REGION_GEO));
        bgp_community.add((64511, REGION_COUNTRY));
    }
}

I have removed the unnecessary variables from the function update_flags and added the function update_region. If I export my own routes, I tag them with a region community.

define ASN_BLACKLIST = [
0
];

After that I define an ASN blacklist just in case. Since bird requires at least one entry, I have entered the AS0.

function kernel_export() {
    krt_prefsrc = OWNIP;
    accept;
}

The function kernel_export is the export filter, what should be exported to the kernel. In this case everything should be exported to the kernel and my IP address should be used as source IP.

function reject_invalid_roa() {
    if (roa_check(dnroa, net, bgp_path.last) != ROA_VALID) then {
        print "Reject: ROA failed|", net, "|", bgp_path;
        reject;
    }
}

The function reject_invalid_roa rejects invalid and unknown ROAs and writes a log in the format Reject: ROA failed|IPv6 address|BGP path. The standard function from the wiki only writes down the origin of the non-valid ROA. However, the AS path is also very interesting, because you can see who exported the non-valid ROA to you.

function reject_default_route() {
    if (net = fd00::/8 || net = ::/0) then
        reject;
}

Since I also have a Clearnet AS, I know from experience that some peers like to export a “default route”. This is a route that says: “Through me you can get into the whole dn42”. The problem with this is that you then can’t do any ROA checking. I therefore reject these routes.

function reject_blacklisted()
int set blacklist;
{
    blacklist = ASN_BLACKLIST;

    if ( bgp_path ~ blacklist ) then {
        print "Reject: blacklisted ASN|", bgp_path;
        reject;
    }
}

The function reject_blacklisted checks whether a blacklisted ASN occurs in the AS path. If so, the route is rejected and a log is written in the format Reject: blacklisted ASN|AS path.

function honor_graceful_shutdown() {
    if (65535, 0) ~ bgp_community then {
        bgp_local_pref = 0;
    }
}

This is a copy from the BGP Filter Guide. If a peer sends a Graceful Shutdown, all routes will have the community (65535, 0) appended. bird should now quickly look to find other routes. With bgp_local_pref = 0 we tell bird to avoid the route. The local_pref is a decision criterion in route selection, which is evaluated before the AS path length.

function add_own_communities(int link_type) {
    bgp_large_community.add((OWNAS, 5, link_type));
}

Furthermore I have my own community, which I attach to routes. Since I have a 32-bit ASN, I have to use Large Communities. Depending on how the connection to the other peer is established (physical connection, IXP, tunnel, …), a different community is attached.

function dn_import_filter(int link_latency; int link_bandwidth; int link_crypto; int link_type) {

The import filter is a bit more complex.

if ( net.type != NET_IP6 ) then {
    print "Reject: non-IPv6 on IPv6 Channel|", net, "|", bgp_path;
    reject;
}

The first thing to be rejected is anything that is not IPv6.

if ( ! is_valid_network() ) then {
    print "Reject: invalid network|", net, "|", bgp_path;
    reject;
}

After that, everything that does not belong in the dn42 network or where the prefix is too large or too small is rejected.

if ( is_self_net() ) then {
    print "Reject: export our network|", net, "|", bgp_path.first;
    reject;
}

I take care of my network. Therefore, I do not need to import this from other peers.

if ( bgp_path.len > 25 ) then {
    print "Reject: AS path too long|", net, "|", bgp_path;
    reject;
}

After that I reject too long AS paths.

reject_blacklisted();
reject_invalid_roa();
reject_default_route();

Then I call all functions from above and reject routes with blacklisted ASNs, non-valid ROAs and default routes.

if (bgp_path.len = 1) then
    bgp_local_pref = bgp_local_pref + 500;

Should I be directly connected to a peer, I always want to prefer the route. Therefore I enforce this with local_pref.

honor_graceful_shutdown();

update_flags(link_latency, link_bandwidth, link_crypto);
add_own_communities(link_type);

accept;

After that I check for Graceful Shutdown and mark the routes with a community. Finally, I accept them.

function dn_export_filter(int link_latency; int link_bandwidth; int link_crypto; int link_type) {
if (source !~ [RTS_STATIC, RTS_BGP]) then
    reject;

All routes, which I did not get myself via BGP or which are statically defined, should not be exported.

if (bgp_path.last != bgp_path.first) then
    reject;

Since it is a Lab AS, I want to be a transit only to a limited extent, so I only export transit routes to my direct peers.

There is a difference between bgp_path.last != bgp_path.first and bgp_path.len = 1. There is a technique to make your own routes more “unpopular”. This is ASN prepending. Thereby you append your own ASN several times in the AS path. This makes it longer and therefore less popular. bgp_path.len = 1 detects direct peers without ASN prepending and bgp_path.last != bgp_path.first detects also direct peers with ASN prepending. If someone artificially lengthens a route and thus makes it less popular, this certainly already has a purpose. For prepended paths I don’t set a higher local_pref, but I want to act as transit for direct peers - independent of prepending. I also send the prepending to my peers. This makes the path just as unpopular for them as it is for me.

reject_default_route();

For some reasons I have a static default route. However, I do not want to export this to my peers. Therefore I also reject it during the export.

update_flags(link_latency, link_bandwidth, link_crypto);
update_region();

Also here, I tag the routes according to the communities in the wiki.

bgp_med = 0;
bgp_med = bgp_med + ( ( 4 - ( link_crypto - 30 ) ) * 600 );
bgp_med = bgp_med + ( ( 9 - ( link_bandwidth - 20 ) ) * 100);
bgp_med = bgp_med + ( ( link_latency - 1) * 300);
bgp_med = bgp_med + ( link_type - 1 ) * 10;

Here I let calculate semiautomatically the bgp_med. If two Autonomous Systems are connected at two locations, the question is of course, where to send the traffic to. For this there is the bgp_med. If there are two routes from the same AS, the bgp_med decides which route to use. The smaller the bgp_med, the better the route. Here is the order:

    accept;

After that I export the route.

function dn_export_collector() {
    if (source !~ [RTS_STATIC, RTS_BGP]) then
        reject;

    update_region();
    accept;
}

I want to send all routes to a route collector unchanged, which I have. Therefore I export all routes, if I got them static or via BGP. I also add the region community. I also export the default route to the collector.

/* Export own net */

protocol static ownnet {
    route OWNNET unreachable;

    ipv6 {
        table dn42;
    };
}

After bird has loaded the filters, I set a static route for my own network. This causes bird to now know a route to my network and export it to others.

/* Kernel */

protocol kernel {
    metric 500;

    ipv6 {
        table dn42;
        export where kernel_export();
    };
}

Now I export the routes into the kernel and thus into the forwarding table. I use the filter I defined before. The routes all get the metric 500 in the forwarding table. I do this because there were problems with direct routes. If you don’t set a high metric, the routes over BGP are preferred, although you know a direct route for example because of a VPN connection.

/* make unknown routes unreachable */

protocol static {
    route fd00::/8 unreachable;

    ipv6 {
        table dn42;
    };
}

Here I define the dn42 default route. This causes me to return “unreachable” via ICMP if I don’t know a route, but it still tries to reach it through me.

protocol bfd { }
protocol device { }

After that I enable two “special” protocols. bfd is used to detect connection problems in the milisecond range. The device protocol is used so that bird knows the IP addresses of my local interfaces.

include "templates.conf";

After that I import the templates which I use for peering.

template bgp dnpeer {
    local as OWNAS;

    enforce first as on;
    graceful restart on;
    long lived graceful restart on;
    enable extended messages on;
    advertise hostname on;
    prefer older on;

    # defaults
    enable route refresh on;
    interpret communities on;
    default bgp_local_pref 100;

    ipv6 {
        mandatory on;

        table dn42;

        import table;
        import limit 1000 action restart;

        import none;
        export none;
    };

}

template bgp routecollector from dnpeer {
    multihop;

    ipv6 {
        add paths tx;
    };
}
local as OWNAS;

With this I inform bird about my ASN number which should be used for peering.

enforce first as on;

This option causes that in the AS path of a peer, the ASN of peer must be first. This is actually always the case except for route servers. However, to protect against misconfiguration of your peer, you can activate this option.

graceful restart on;
long lived graceful restart on;
enable extended messages on;

...
    
prefer older on;

After that activate some functions. I have tried to explain these in Beginner Tips.

advertise hostname on;

Above I had defined my hostname. This option must be activated so that this is communicated to the peers via BGP.

# defaults
enable route refresh on;
interpret communities on;
default bgp_local_pref 100;

Here I set some parameters to their default value for safety. This is actually unnecessary.

In the channel are some functions, which are not in the wiki.

import table;

I had already explained this in Beginner Tips.

import limit 1000 action restart;

If the peer exports over 1000 prefixes, the session is restarted. An alternative is the block action. This blocks all further imports instead of restarting the BGP session.

mandatory on;

With this I tell bird that the peer must support IPv6. If it does not support it, no BGP session will be established.

table dn42;

This line causes all routes to be imported or exported from table dn42 and not from the default table master6.

import none;
export none;

If no parameters have been adjusted yet, nothing should be imported or exported. For each peer I set the corresponding parameters like latency or bandwidth.

template bgp routecollector from dnpeer {
    multihop;

    ipv6 {
        add paths tx;
    };
}

The template for the Route Collector “inherits” from the dnpeer template. Here the option multihop; is activated, which causes that you do not have to create a tunnel to the route collector, but can reach it “normally”. The option add paths tx; causes that all routes (not only the best ones) are exported to the collector.

include "peers/*.conf";

After that, I include the configuration files of my peers.

The following is a configuration for the BGP session with my main network:

protocol bgp bandura from dnpeer {
    neighbor fe80::2923%bandura as 4242422923;

    bfd graceful;

    ipv6 {
        import where dn_import_filter(1, 25, 34, 1);
        export where dn_export_filter(1, 25, 34, 1);
    };
}

For example, with dn_import_filter(1, 25, 34, 1) and dn_export_filter(1, 25, 34, 1) I set parameters for the BGP communities and enable import and export. With bfd graceful; I enable BfD, which is disabled by default for BGP sessions.