network/nix/nixos-module/container/bird.nix

398 lines
11 KiB
Nix

# Routing daemon configuration
{ hostName, config, lib, pkgs, ... }:
let
hostNameEscaped = builtins.replaceStrings [ "-" ] [ "_" ] hostName;
hostConf = config.site.hosts.${hostName};
upstreamInterfaces = lib.filterAttrs (_: { upstream, ... }:
upstream != null
) hostConf.interfaces;
isUpstream = upstreamInterfaces != {};
ipv6RouterNets = builtins.attrNames (
lib.filterAttrs (net: { ipv6Router, ... }:
ipv6Router == hostName
) config.site.net
);
enumerate = n: list:
if list == []
then []
else [ {
n = n;
x = builtins.head list;
} ] ++ (enumerate (n + 1) (builtins.tail list));
nets4 =
hostConf.bgp.nets4
++
builtins.concatMap (net:
if net != "core"
then
let
subnet4 = config.site.net.${net}.subnet4 or null;
in lib.optional (subnet4 != null) subnet4
else
[]
) (builtins.attrNames hostConf.interfaces);
nets6 =
hostConf.bgp.nets6
++
builtins.concatMap (net:
if net != "core"
then
builtins.attrValues config.site.net.${net}.subnets6 or {}
else
[]
) (builtins.attrNames hostConf.interfaces);
upstreamsToOrder = upstreams:
builtins.foldl' (order: { n, x }:
order // {
${x} = n;
}
) {} (enumerate 1 upstreams);
upstream4Order = upstreamsToOrder hostConf.bgp.allowedUpstreams;
upstream6Order = upstreamsToOrder hostConf.bgp.allowedUpstreams6;
allowedUpstreams = lib.unique (
hostConf.bgp.allowedUpstreams ++ hostConf.bgp.allowedUpstreams6
);
in
{
services.bird2 = {
enable = true;
config = ''
router id ${config.site.net.core.hosts4.${hostName}};
protocol kernel K4 {
learn;
ipv4 {
export all;
};
}
protocol kernel K6 {
learn;
ipv6 {
export all;
};
}
protocol device {
scan time 10;
}
# Import address ranges of upstream interfaces so that
# internal traffic to local public services take no detours
# if the default router takes another upstream gateway.
protocol direct {
ipv4 {
${if isUpstream
then ''
# No RFC1918, RFC6598
import where net !~ [ 100.64.0.0/10 ] && net !~ [ 10.0.0.0/8 ] && net !~ [ 172.16.0.0/12 ] && net !~ [ 192.168.0.0/16 ];
''
else ""}
};
ipv6;
interface ${lib.concatMapStringsSep ", " (iface:
''"${iface}"''
)(builtins.attrNames hostConf.interfaces)};
check link yes;
}
${lib.optionalString (hostConf.bgp.upstreamTable != null) ''
# BIRD routing table for Wireguard transport
ipv4 table vpn_table;
# Kernel routing table for Wireguard transport
protocol kernel VPN {
# "vpn_table" configured on anon routers
kernel table 100;
ipv4 {
export all;
table vpn_table;
};
}
''}
${lib.optionalString (ipv6RouterNets != []) ''
# Router advertisements
protocol radv {
rdnss ${config.site.net.serv.hosts6.dn42.dnscache};
${lib.concatMapStrings (net: ''
interface "${net}" {
min ra interval 10;
max ra interval 60;
solicited ra unicast yes;
${builtins.concatStringsSep "\n" (
map (subnet6: ''
prefix ${subnet6} {
preferred lifetime 600;
valid lifetime 1800;
};
'') (builtins.attrValues config.site.net.${net}.subnets6)
)}
dnssl "${config.site.net.${net}.domainName}";
};
'') ipv6RouterNets}
}
''}
# Zentralwerk DN42
protocol static {
ipv4;
route 172.20.72.0/21 unreachable;
}
protocol static {
ipv6;
route fd23:42:c3d2:580::/57 unreachable;
route 2a00:8180:2c00:200::/56 unreachable;
route 2a0f:5382:acab:1400::/56 unreachable;
}
${lib.optionalString (hostConf.bgp != null) ''
# zentralwerk-network
template bgp bgp_rr_server {
local as ${toString hostConf.bgp.asn};
direct;
ipv4 {
import filter {
preference = preference + 200;
accept;
};
export filter {
if net ~ [ ${config.site.net.core.subnet4} ] then {
reject;
}
${lib.optionalString (nets4 != []) ''
if net ~ [ ${lib.concatMapStringsSep ", " (n: "${n}+") nets4} ] then {
accept;
}
''}
reject;
};
};
ipv6 {
import filter {
preference = preference + 200;
accept;
};
export filter {
if net ~ [ ${lib.concatStringsSep ", " (builtins.attrValues config.site.net.core.subnets6)} ] then {
reject;
}
${lib.optionalString (nets6 != []) ''
if net ~ [ ${lib.concatMapStringsSep ", " (n: "${n}+") nets6} ] then {
accept;
}
''}
reject;
};
};
}
template bgp bgp_rr_client {
local as ${toString hostConf.bgp.asn};
direct;
connect delay time 1;
connect retry time 3;
error wait time 1 5;
error forget time 5;
ipv4 {
next hop self on;
import filter {
preference = preference + 200;
accept;
};
${lib.optionalString (nets4 != []) ''
export where net ~ [ ${lib.concatMapStringsSep ", " (n: "${n}") nets4} ];
''}
};
ipv6 {
next hop self on;
import filter {
preference = preference + 200;
accept;
};
${lib.optionalString (nets6 != []) ''
export where net ~ [ ${lib.concatMapStringsSep ", " (n: "${n}") nets6} ];
''}
};
}
# dn42
template bgp bgp_external {
local as ${toString hostConf.bgp.asn};
direct;
ipv4 {
next hop self on;
import all;
export where source = RTS_STATIC;
};
ipv6 {
next hop self on;
import all;
export where source = RTS_STATIC;
};
}
# emitting default routes
template bgp bgp_upstream {
local as ${toString hostConf.bgp.asn};
direct;
ipv4 {
next hop self on;
import all;
export where net = 0.0.0.0/0;
};
ipv6 {
next hop self on;
import all;
export where net = ::/0;
};
}
${lib.concatMapStrings (peer:
let
peerConf = hostConf.bgp.peers.${peer};
isRange = lib.hasInfix "/" peer;
in ''
protocol bgp bgp_${peerConf.name} from bgp_${peerConf.type} {
neighbor ${lib.optionalString isRange "range"} ${peer} as ${toString peerConf.asn};
${lib.optionalString isRange ''
dynamic name "bgp_${peerConf.name}";
''}
${lib.optionalString (peerConf.type == "rr") ''
rr client;
''}
}
'') (builtins.attrNames hostConf.bgp.peers)}
${lib.concatMapStrings ({ n, x }: let upstream = x; in ''
# upstream client instance #${toString n}
protocol bgp bgp_up_${builtins.replaceStrings ["-"] ["_"] upstream} {
local as ${toString hostConf.bgp.asn};
neighbor ${config.site.net.core.hosts6.dn42.${upstream}} as ${toString hostConf.bgp.asn};
direct;
connect delay time 1;
connect retry time 3;
error wait time 1 5;
error forget time 5;
ipv4 {
${if (upstream4Order ? ${upstream})
then ''
import filter {
preference = preference + ${toString (100 - upstream4Order.${upstream})};
accept;
};
''
else ''
import none;
''}
${lib.optionalString (nets4 != []) ''
export where net ~ [ ${lib.concatMapStringsSep ", " (n: "${n}") nets4} ];
''}
${lib.optionalString (hostConf.bgp.upstreamTable != null) ''
table ${hostConf.bgp.upstreamTable};
''}
};
ipv6 {
${if (upstream4Order ? ${upstream})
then ''
import filter {
preference = preference + ${toString (100 - upstream4Order.${upstream})};
accept;
};
''
else ''
import none;
''}
${lib.optionalString (nets6 != []) ''
export where net ~ [ ${lib.concatMapStringsSep ", " (n: "${n}") nets6} ];
''}
};
}
'') (enumerate 1 allowedUpstreams)}
''}
'';
};
# Script that pings internet hosts every few minutes to determine if
# the upstream actually works. The associated OSPF instance will be
# enabled/disabled on state change.
systemd.services =
let
interval = 5;
targets = {
ipv4 = [
# inbert.c3d2.de
"217.197.83.184"
# ccc.de
"195.54.164.39"
# Cloud DNS services
"9.9.9.9"
"8.8.8.8"
"1.1.1.1"
];
ipv6 = [
# inbert.c3d2.de
"2001:67c:1400:2240::1"
# ccc.de
"2001:67c:20a0:2:0:164:0:39"
# Cloud DNS services
"2620:fe::9"
"2606:4700:4700::1111"
"2001:4860:4860::8888"
];
};
instance = {
ipv4 = "bgp_up";
};
checkService = addressFamily: {
description = "Check connectivity for ${addressFamily}";
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
User = "bird2";
Group = "bird2";
};
path = [ pkgs.bird2 "/run/wrappers" ];
script = ''
STATE=unknown
while true; do
NEW_STATE=unknown
false \
${lib.concatMapStrings (target:
" || ping -n -s 0 -c 1 -w 1 ${target} 2>/dev/null >/dev/null \\\n"
) targets.${addressFamily}} \
&& NEW_STATE=up \
|| NEW_STATE=down
if [ $STATE != $NEW_STATE ]; then
echo "Connectivity change from $STATE to $NEW_STATE"
if [ $NEW_STATE = up ]; then
birdc enable ${instance.${addressFamily}}
else
birdc disable ${instance.${addressFamily}}
fi
fi
STATE=$NEW_STATE
sleep ${toString interval}
done
'';
};
in lib.mkIf isUpstream {
check-upstream-ipv4 = checkService "ipv4";
#check-upstream-ipv6 = checkService "ipv6";
};
}