# 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"; }; }