# Routing daemon configuration { hostName, config, lib, pkgs, ... }: let hostConf = config.site.hosts.${hostName}; upstreamInterfaces = lib.filterAttrs (_: { upstream, ... }: upstream != null ) hostConf.interfaces; isUpstream = upstreamInterfaces != {}; # Configuring a gateway? If so, this is the associated net. gatewayNet = let m = builtins.match "(.+)-gw" hostName; in if hostName == "c3d2-gw3" then "c3d2" else if m == [ "cls" ] then "cluster" else if m == null then null else builtins.head m; enumerate = n: list: if list == [] then [] else [ { n = n; x = builtins.head list; } ] ++ (enumerate (n + 1) (builtins.tail list)); in { services.bird2 = { enable = true; config = '' router id ${config.site.net.core.hosts4.${hostName}}; protocol kernel K4 { learn; ipv4 { ${if isUpstream then '' # Install all routes but the default route on upstreams export where net != 0.0.0.0/0; # Learn the upstream default route import where net = 0.0.0.0/0; '' else '' export all; ''} }; } protocol kernel K6 { learn; ipv6 { ${if isUpstream then '' # Install all routes but the default route on upstreams export where net != ::/0; # Learn the upstream default route import where net = ::/0; '' else '' export all; ''} }; } protocol device { scan time 10; } ${lib.optionalString isUpstream '' # 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 { # No RFC6598 import where net !~ 100.64.0.0/10 # No RFC1918 && net !~ 10.0.0.0/8 && net !~ 172.16.0.0/12 && net !~ 192.168.0.0/16; }; ipv6; interface ${lib.concatMapStringsSep ", " (iface: ''"${iface}"'' )(builtins.attrNames upstreamInterfaces)}; check link yes; } ''} ${lib.optionalString (builtins.match "anon.*" hostName != null) '' # BIRD routing table for Wireguard transport ipv4 table vpn4_table; # Kernel routing table for Wireguard transport protocol kernel VPN4 { # "vpn4_table" configured on anon routers kernel table 100; ipv4 { export all; table vpn4_table; }; } ''} ${lib.optionalString (gatewayNet != null) '' # Router advertisements protocol radv { rdnss ${config.site.net.serv.hosts6.dn42.dnscache}; interface "${gatewayNet}" { min ra interval 10; max ra interval 60; ${builtins.concatStringsSep "\n" ( map (subnet6: '' prefix ${subnet6} { preferred lifetime 600; valid lifetime 1800; }; '') (builtins.attrValues config.site.net.${gatewayNet}.subnets6) )} dnssl "${config.site.net.${gatewayNet}.domainName}"; }; } ''} # OSPFv2 for site-local IPv4 protocol ospf v2 ZW4 { ipv4 { export where net != 0.0.0.0/0 && source != RTS_BGP; }; area 0 { ${builtins.concatStringsSep "\n" ( builtins.attrValues ( builtins.mapAttrs (net: _: # Enable OSPF only on networks with a secret. Others # are treated as a stubnet whose routes to # advertise. if config.site.net ? "${net}" && config.site.net.${net}.ospf.secret != null then '' interface "${net}" { hello 10; wait 20; authentication cryptographic; password "${config.site.net.${net}.ospf.secret}"; }; '' else if config.site.net ? "${net}" && config.site.net.${net}.subnet4 != null then '' # Advertise route of network ${net} stubnet ${config.site.net.${net}.subnet4} {}; '' else "" ) hostConf.interfaces ) )} ${builtins.concatStringsSep "\n" ( map (stubnet4: '' # Advertise additional route stubnet ${stubnet4} {}; '') hostConf.ospf.stubNets4 )} }; } ${lib.optionalString isUpstream '' # OSPFv2 to advertise my default route protocol ospf v2 ZW4_${hostName} { ipv4 { export where net = 0.0.0.0/0; }; area 0 { ${builtins.concatStringsSep "\n" ( builtins.attrValues ( builtins.mapAttrs (net: _: # Enable OSPF only on interfaces with a secret. lib.optionalString (config.site.net.${net}.ospf.secret != null) '' interface "${net}" instance ${toString hostConf.ospf.upstreamInstance} { # Become the designated router priority 10; hello 10; wait 20; authentication cryptographic; password "${config.site.net.${net}.ospf.secret}"; }; '' ) hostConf.physicalInterfaces ) )} }; } ''} ${( builtins.foldl' ({ text, n }: upstream: { text = '' ${text} # OSPFv2 to receive a default route from ${upstream} protocol ospf v2 ZW4_${upstream} { ipv4 { import filter { preference = preference + ${toString (100 - n)}; accept; }; ${lib.optionalString (builtins.match "anon.*" hostName != null) '' table vpn4_table; ''} }; area 0 { ${builtins.concatStringsSep "\n" ( builtins.attrValues ( builtins.mapAttrs (net: _: # Enable OSPF only on interfaces with a secret. lib.optionalString (config.site.net.${net}.ospf.secret != null) '' interface "${net}" instance ${toString config.site.hosts.${upstream}.ospf.upstreamInstance} { hello 10; wait 20; authentication cryptographic; password "${config.site.net.${net}.ospf.secret}"; }; '' ) hostConf.physicalInterfaces ) )} }; } ''; n = n + 1; }) { text = ""; n = 0; } hostConf.ospf.allowedUpstreams ).text} # OSPFv3 for site-local IPv6 protocol ospf v3 ZW6 { ipv6 { export where net != ::/0 && source != RTS_BGP; }; area 0 { ${builtins.concatStringsSep "\n" ( builtins.attrValues ( builtins.mapAttrs (net: _: # Enable OSPF only on networks with a secret. Others # are treated as a stubnet whose routes to # advertise. if config.site.net.${net}.ospf.secret != null then '' interface "${net}" { hello 10; wait 20; authentication cryptographic; password "${config.site.net.${net}.ospf.secret}"; }; '' else builtins.concatStringsSep "\n" ( map (subnet6: '' # Advertise route of network ${net} stubnet ${subnet6} {}; '') (builtins.attrValues config.site.net.${net}.subnets6) ) ) hostConf.physicalInterfaces ) )} ${builtins.concatStringsSep "\n" ( map (stubnet6: '' # Advertise additional route stubnet ${stubnet6} {}; '') hostConf.ospf.stubNets6 )} }; } ${lib.optionalString isUpstream '' # OSPFv3 to advertise my default route protocol ospf v3 ZW6_${hostName} { ipv6 { export where net = ::/0; }; area 0 { ${builtins.concatStringsSep "\n" ( builtins.attrValues ( builtins.mapAttrs (net: _: # Enable OSPF only on interfaces with a secret. lib.optionalString (config.site.net.${net}.ospf.secret != null) '' interface "${net}" instance ${toString hostConf.ospf.upstreamInstance} { # Become the designated router priority 10; hello 10; wait 20; authentication cryptographic; password "${config.site.net.${net}.ospf.secret}"; }; '' ) hostConf.physicalInterfaces ) )} }; } ''} ${lib.optionalString (builtins.match "anon.*" hostName == null) ( builtins.foldl' ({ text, n }: upstream: { text = '' ${text} # OSPFv3 to receive a default route from ${upstream} protocol ospf v3 ZW6_${upstream} { ipv6 { import filter { preference = preference + ${toString (100 - n)}; accept; }; }; area 0 { ${builtins.concatStringsSep "\n" ( builtins.attrValues ( builtins.mapAttrs (net: _: # Enable OSPF only on interfaces with a secret. lib.optionalString (config.site.net.${net}.ospf.secret != null) '' interface "${net}" instance ${toString config.site.hosts.${upstream}.ospf.upstreamInstance} { hello 10; wait 20; authentication cryptographic; password "${config.site.net.${net}.ospf.secret}"; }; '' ) hostConf.physicalInterfaces ) )} }; } ''; n = n + 1; }) { text = ""; n = 0; } hostConf.ospf.allowedUpstreams ).text} # 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; } ${lib.optionalString (hostConf.bgp != null) '' template bgp bgppeer { local as ${toString hostConf.bgp.asn}; ipv4 { import all; export where source=RTS_STATIC; }; ipv6 { import all; export where source=RTS_STATIC; }; } ${builtins.concatStringsSep "\n" ( map ({ n, x }: let peer = x; peerConf = hostConf.bgp.peers.${peer}; in '' protocol bgp bgp_${toString n} from bgppeer { neighbor ${peer} as ${toString peerConf.asn}; } '' ) (enumerate 1 (builtins.attrNames hostConf.bgp.peers)) )} ''} ''; }; # 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 = "ZW4_${hostName}"; ipv6 = "ZW6_${hostName}"; }; 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"; }; }