# SPDX-FileCopyrightText: 2020 Luke Granger-Brown # # SPDX-License-Identifier: Apache-2.0 { depot, lib, config, ... }: let generateSnippet = base: args: lib.concatStringsSep "\n" (lib.mapAttrsToList ( ixName: ix: generateSnippetForIX (args // { ixName = ixName; ix = ix; }) ) base ); generateSnippetForIX = { ixName, ix, ... }@args: '' ${lib.optionalString (doesIPv4 ix) '' ipv4 table ${ixName}4; filter bgp_in_${ixName}4 prefix set allnet; int set allas; { if ! (avoid_martians4()) then reject; ${if ix.remote.must_be_next_hop then "if (bgp_path.first != ${toString ix.remote.asn}) then reject;" else "# no next-hop requirement"} ${if ix.remote.set_imported_next_hop_to != null then "bgp_next_hop = ${ix.remote.set_imported_next_hop_to};" else "# no imported bgp_next_hop override"} ${lib.concatMapStringsSep "\n" (asn: "if (bgp_path ~ [= * ${toString asn} * =]) then reject;") ix.remote.drop_asns} ${lib.optionalString (ixName == "quadv") '' bgp_ext_community.add((ro, 205479, 1000)); bgp_ext_community.add((ro, 205479, 4000)); # etheroute bgp_ext_community.add((ro, 205479, 6000)); # cofractal-ams01 #bgp_ext_community.add((ro, 205479, 4002)); # gsl # Etheroute communities bgp_community.add((3170, 63257)); # GTT Corero bgp_community.add((64600, 3356)); # no Lumen bgp_community.add((64600, 5459)); # no LINX peers (inc. RS) ''} ${if ix.remote.pref_src.v4 != null then "krt_prefsrc = ${ix.remote.pref_src.v4};" else "# no krt_prefsrc"} bgp_local_pref = ${toString ix.remote.bgp_local_pref}; accept; } filter bgp_export_${ixName}4 { if ! ((ro, ${toString ix.local.asn}, 1000) ~ bgp_ext_community) then reject; bgp_ext_community.delete([(ro, ${toString ix.local.asn}, *)]); accept; } protocol pipe ${ixName}pipe_4 { table ${ixName}4; peer table master4; import ${if ix.remote.is_route_collector then "all" else "where ((ro, ${toString ix.local.asn}, ${toString ix.remote.export_community}) ~ bgp_ext_community)"}; export filter bgp_in_${ixName}4; }; ''} ${lib.optionalString (doesIPv6 ix) '' ipv6 table ${ixName}6; filter bgp_in_${ixName}6 prefix set allnet; int set allas; { if ! (avoid_martians6()) then reject; ${if ix.remote.must_be_next_hop then "if (bgp_path.first != ${toString ix.remote.asn}) then reject;" else "# no next-hop requirement"} ${lib.concatMapStringsSep "\n" (asn: "if (bgp_path ~ [= * ${toString asn} * =]) then reject;") ix.remote.drop_asns} ${if ix.remote.pref_src.v6 != null then "krt_prefsrc = ${ix.remote.pref_src.v6};" else "# no krt_prefsrc"} bgp_local_pref = ${toString ix.remote.bgp_local_pref}; accept; } filter bgp_export_${ixName}6 { if ! ((ro, ${toString ix.local.asn}, 1000) ~ bgp_ext_community) then reject; bgp_ext_community.delete([(ro, ${toString ix.local.asn}, *)]); accept; } protocol pipe ${ixName}pipe_6 { table ${ixName}6; peer table master6; import ${if ix.remote.is_route_collector then "all" else "where ((ro, ${toString ix.local.asn}, ${toString ix.remote.export_community}) ~ bgp_ext_community)"}; export filter bgp_in_${ixName}6; }; ''} '' + lib.concatImapStringsSep "\n" ( i: v: generateSnippetForRouter (args // { routerNum = i; router = v; }) ) ix.remote.routers; doesIPv4 = ix: (ix.local.v4 != null) || ix.v4onv6; doesIPv6 = ix: (ix.local.v6 != null); enabledSnippet = { enabled ? true, ... }: "disabled ${if enabled then "off" else "on"};"; passwordSnippet = { password ? null, ... }: if password == null then "# no password" else "password \"${password}\";"; multihopSnippet = { multihop ? null, ... }: if multihop == null then "# not multihop" else "multihop ${toString multihop};"; nexthopSnippet = { next_hop ? null, ... }: if next_hop == null then "# no next hop override" else "next hop ${toString next_hop};"; passiveSnippet = { passive, ... }: "passive ${if passive then "on" else "off"};"; prefixLimitSnippet = limit: if limit == null then "# no import limit" else "import limit ${toString limit} action restart;"; generateSnippetForRouter = { ixName, ix, routerNum, router, ... }: '' ${lib.optionalString (ix.local.v4 != null) '' protocol bgp ${ixName}${toString routerNum}_4 { ${enabledSnippet router} ${passwordSnippet router} ${multihopSnippet router} ${nexthopSnippet router} ${passiveSnippet ix.remote} local ${ix.local.v4} as ${toString ix.local.asn}; neighbor ${router.v4} as ${toString ix.remote.asn}; graceful restart on; long lived graceful restart on; ipv4 { table ${ixName}4; import all; export ${if ix.remote.is_route_collector then "all" else "filter bgp_export_${ixName}4"}; ${prefixLimitSnippet ix.remote.prefix_limit.v4} }; }; ''} ${lib.optionalString (ix.local.v6 != null) '' protocol bgp ${ixName}${toString routerNum}_6 { ${enabledSnippet router} ${passwordSnippet router} ${multihopSnippet router} ${passiveSnippet ix.remote} local ${ix.local.v6} as ${toString ix.local.asn}; neighbor ${router.v6} as ${toString ix.remote.asn}; graceful restart on; long lived graceful restart on; ${lib.optionalString (ix.v4onv6) '' ipv4 { table ${ixName}4; import all; export ${if ix.remote.is_route_collector then "all" else "filter bgp_export_${ixName}4"}; ${prefixLimitSnippet ix.remote.prefix_limit.v4} extended next hop; }; ''} ipv6 { table ${ixName}6; import all; export ${if ix.remote.is_route_collector then "all" else "filter bgp_export_${ixName}6"}; ${prefixLimitSnippet ix.remote.prefix_limit.v6} }; }; ''} ''; inherit (lib) mkOption mkAfter types; in { options.services.lukegbgp = { enable = mkOption { type = types.bool; default = false; }; config = mkOption { # lukegbgp.config type = with types; submodule { options = { local = mkOption { # lukegbgp.config.local type = submodule { options = { routerID = mkOption { # lukegbgp.config.local.routerID type = str; }; }; }; }; peering = mkOption { # lukegbgp.config.peering type = attrsOf (submodule { options = { local = mkOption { # lukegbgp.config.peering..local type = submodule { options = { asn = mkOption { # lukegbgp.config.peering..local.asn type = int; }; v4 = mkOption { # lukegbgp.config.peering..local.v4 type = nullOr str; default = null; }; v6 = mkOption { # lukegbgp.config.peering..local.v6 type = nullOr str; default = null; }; }; }; }; v4onv6 = mkOption { type = bool; default = false; }; remote = mkOption { # lukegbgp.config.peering..remote type = submodule { options = { asn = mkOption { # lukegbgp.config.peering..remote.asn type = int; }; passive = mkOption { # lukegbgp.config.peering..remote.passive type = bool; default = false; }; export_community = mkOption { # lukegbgp.config.peering..remote.export_community type = int; }; prefix_limit.v4 = mkOption { # lukegbgp.config.peering..remote.prefix_limit.v4 type = nullOr int; default = null; }; prefix_limit.v6 = mkOption { # lukegbgp.config.peering..remote.prefix_limit.v6 type = nullOr int; default = null; }; pref_src.v4 = mkOption { # lukegbgp.config.peering..pref_src.v4 type = nullOr str; default = null; }; pref_src.v6 = mkOption { # lukegbgp.config.peering..pref_src.v6 type = nullOr str; default = null; }; must_be_next_hop = mkOption { # lukegbgp.config.peering..remote.must_be_next_hop type = bool; default = true; }; set_imported_next_hop_to = mkOption { # lukegbgp.config.peering..remote.set_imported_next_hop_to type = nullOr str; default = null; }; drop_asns = mkOption { # lukegbgp.config.peering..remote.drop_asns type = listOf int; default = []; }; bgp_local_pref = mkOption { # lukegbgp.config.peering..remote.bgp_local_pref type = int; default = 100; }; is_route_collector = mkOption { # lukegbgp.config.peering..remote.is_route_collector type = bool; default = false; }; routers = mkOption { # lukegbgp.config.peering..remote.routers type = listOf (submodule { options = { enabled = mkOption { # lukegbgp.config.peering..remote.routers..enabled type = bool; default = true; }; v4 = mkOption { # lukegbgp.config.peering..remote.routers..v4 type = str; }; v6 = mkOption { # lukegbgp.config.peering..remote.routers..v6 type = str; }; multihop = mkOption { # lukegbgp.config.peering..remote.routers..multihop type = nullOr int; default = null; }; password = mkOption { # lukegbgp.config.peering..remote.routers..password type = nullOr str; default = null; }; next_hop = mkOption { # lukegbgp.config.peering..remote.routers..next_hop type = nullOr str; default = null; }; }; }); }; }; }; }; }; }); }; export = mkOption { # lukegbgp.config.export default = { v4 = []; v6 = ["2a09:a440::/48"]; }; type = submodule { options = { v4 = mkOption { # lukegbgp.config.export.v4 type = listOf str; default = []; }; v4Extra = mkOption { #lukegbgp.config.export.v4Extra type = lines; default = ""; }; v6 = mkOption { # lukegbgp.config.export.v6 type = listOf str; default = ["2a09:a440::/48"]; }; v6Extra = mkOption { #lukegbgp.config.export.v6Extra type = lines; default = ""; }; }; }; }; internal.export = mkOption { default = { v4 = []; v6 = []; }; type = submodule { options = { v4 = mkOption { # lukegbgp.config.internal.export.v4 type = listOf str; default = []; }; v4Extra = mkOption { #lukegbgp.config.internal.export.v4Extra type = lines; default = ""; }; v6 = mkOption { # lukegbgp.config.internal.export.v6 type = listOf str; default = []; }; v6Extra = mkOption { #lukegbgp.config.internal.export.v6Extra type = lines; default = ""; }; }; }; }; bfd = mkOption { # lukegbgp.config.bfd type = lines; default = ""; }; }; }; }; }; config = { services.bird2 = lib.mkIf config.services.lukegbgp.enable { enable = true; config = '' router id ${config.services.lukegbgp.config.local.routerID}; function avoid_martians4() prefix set martians; { martians = [ 169.254.0.0/16+, 172.16.0.0/12+, 192.168.0.0/16+, 192.0.0.0/24+, 192.0.2.0/24+, 192.88.99.0/24+, 198.18.0.0/15+, 198.51.100.0/24+, 203.0.113.0/24+, 10.0.0.0/8+, 100.64.0.0/10+, 224.0.0.0/4+, 240.0.0.0/4+, 0.0.0.0/32-, 0.0.0.0/0{25,32}, 0.0.0.0/0{0,7} ]; if net ~ martians then return false; return true; } function avoid_martians6() prefix set martians; { martians = [ ::/128-, ::1/128-, ::ffff:0:0/96+, ::ffff:0:0:0/96+, 64:ff9b::/96+, 100::/64+, ::/0{64,128}, ::/0{0,15}, 2001:db8::/32+, fc00::/7+, fe80::/10+, ff00::/8+ ]; if net ~ martians then return false; return true; } ${generateSnippet config.services.lukegbgp.config.peering {}} protocol kernel { persist; ipv4 { import none; export all; }; }; protocol kernel { persist; ipv6 { import none; export all; }; }; protocol device { }; protocol static export4 { ipv4 { import filter { bgp_ext_community.add((ro, 205479, 1000)); bgp_ext_community.add((ro, 205479, 2000)); bgp_ext_community.add((ro, 205479, 2001)); bgp_ext_community.add((ro, 205479, 2002)); bgp_ext_community.add((ro, 205479, 2003)); bgp_ext_community.add((ro, 205479, 3000)); bgp_ext_community.add((ro, 205479, 4000)); bgp_ext_community.add((ro, 205479, 4001)); bgp_ext_community.add((ro, 205479, 4002)); bgp_ext_community.add((ro, 205479, 5000)); bgp_ext_community.add((ro, 205479, 5001)); bgp_ext_community.add((ro, 205479, 5002)); bgp_ext_community.add((ro, 205479, 5003)); bgp_ext_community.add((ro, 205479, 5004)); bgp_ext_community.add((ro, 205479, 5005)); bgp_ext_community.add((ro, 205479, 5006)); bgp_ext_community.add((ro, 205479, 5007)); bgp_ext_community.add((ro, 205479, 5008)); bgp_ext_community.add((ro, 205479, 5009)); # fastly from rexxar bgp_ext_community.add((ro, 205479, 5010)); # ovh from rexxar bgp_ext_community.add((ro, 205479, 6000)); # EMF: EMF-IX Route Server # do not export to clouvider; they do... strange things. bgp_ext_community.add((rt, 0, 62240)); accept; }; }; ${lib.concatMapStrings (ip: "route ${ip} blackhole;") config.services.lukegbgp.config.export.v4} ${config.services.lukegbgp.config.export.v4Extra} }; protocol static exportinternal4 { ipv4 { import filter { bgp_ext_community.add((ro, 205479, 10)); # internal only accept; }; }; ${lib.concatMapStrings (ip: "route ${ip} blackhole;") config.services.lukegbgp.config.internal.export.v4} ${config.services.lukegbgp.config.internal.export.v4Extra} }; protocol static export6 { ipv6 { import filter { bgp_ext_community.add((ro, 205479, 1000)); # export bgp_ext_community.add((ro, 205479, 2000)); # bgp_ext_community.add((ro, 205479, 2001)); # bgp_ext_community.add((ro, 205479, 2002)); # bgp_ext_community.add((ro, 205479, 2003)); # bgp_ext_community.add((ro, 205479, 3000)); # clouvider from clouvider-lon01 bgp_ext_community.add((ro, 205479, 4000)); # frantech from frantech-nyc01/veloxserv from etheroute-lon01 bgp_ext_community.add((ro, 205479, 4001)); # veloxserv from rexxar bgp_ext_community.add((ro, 205479, 4002)); # mercury from etheroute-lon01 bgp_ext_community.add((ro, 205479, 5000)); # linx route collector from rexxar bgp_ext_community.add((ro, 205479, 5001)); # linx route server from rexxar bgp_ext_community.add((ro, 205479, 5002)); # facebook from rexxar bgp_ext_community.add((ro, 205479, 5003)); # openpeering from rexxar bgp_ext_community.add((ro, 205479, 5004)); # freetransitnet from rexxar bgp_ext_community.add((ro, 205479, 5005)); # he from rexxar bgp_ext_community.add((ro, 205479, 5006)); # clouvider from rexxar bgp_ext_community.add((ro, 205479, 5007)); # google from rexxar bgp_ext_community.add((ro, 205479, 5008)); # cloudflare from rexxar bgp_ext_community.add((ro, 205479, 5009)); # fastly from rexxar bgp_ext_community.add((ro, 205479, 5010)); # ovh from rexxar bgp_ext_community.add((ro, 205479, 6000)); # EMF: EMF-IX Route Server # do not export to clouvider; they do... strange things. bgp_ext_community.add((rt, 0, 62240)); accept; }; }; ${lib.concatMapStrings (ip: "route ${ip} blackhole;") config.services.lukegbgp.config.export.v6} ${config.services.lukegbgp.config.export.v6Extra} }; protocol static exportinternal6 { ipv6 { import filter { bgp_ext_community.add((ro, 205479, 10)); # internal only accept; }; }; ${lib.concatMapStrings (ip: "route ${ip} blackhole;") config.services.lukegbgp.config.internal.export.v6} ${config.services.lukegbgp.config.internal.export.v6Extra} }; protocol bfd { ${config.services.lukegbgp.config.bfd} }; ''; }; services.prometheus.exporters.bird.enable = config.services.bird2.enable; networking.firewall.allowedTCPPorts = lib.mkIf config.services.lukegbgp.enable (lib.mkAfter [ 179 ]); networking.firewall.checkReversePath = false; boot.kernel.sysctl = { "net.ipv6.conf.default.accept_ra" = 0; "net.ipv6.conf.all.accept_ra" = 0; "net.ipv6.conf.default.autoconf" = 0; "net.ipv6.conf.all.autoconf" = 0; }; services.tailscale.package = depot.nix.pkgs.tailscale; systemd.network.config.networkConfig.ManageForeignRoutes = "no"; systemd.services."systemd-resolved".environment.SYSTEMD_RESOLVED_SYNTHESIZE_HOSTNAME = "false"; }; }