# SPDX-FileCopyrightText: 2020 Luke Granger-Brown <depot@lukegb.com>
#
# SPDX-License-Identifier: Apache-2.0

{ lib, config, ... }:
let
  generateSnippet = base: args: lib.concatStringsSep "\n" (lib.mapAttrsToList ( ixName: ix: generateSnippetForIX (args // { ixName = ixName; ix = ix; }) ) base );
  generateSnippetForIX = { ixName, ix, ... }@args: ''
    ipv4 table ${ixName}4;
    ipv6 table ${ixName}6;
    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"}
      ${lib.concatMapStringsSep "\n" (asn: "if (bgp_path ~ [= * ${toString asn} * =]) then reject;") ix.remote.drop_asns}
      bgp_local_pref = ${toString ix.remote.bgp_local_pref};
      accept;
    }
    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}
      bgp_local_pref = ${toString ix.remote.bgp_local_pref};
      accept;
    }
    protocol pipe ${ixName}pipe_4 {
      table ${ixName}4;
      peer table master4;
      import where ((ro, ${toString ix.local.asn}, ${toString ix.remote.export_community}) ~ bgp_ext_community);
      export filter bgp_in_${ixName}4;
    };
    protocol pipe ${ixName}pipe_6 {
      table ${ixName}6;
      peer table master6;
      import 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;
  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};";
  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, ... }: ''
    protocol bgp ${ixName}${toString routerNum}_4 {
      ${enabledSnippet router}
      ${passwordSnippet router}
      ${multihopSnippet 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 where ((ro, ${toString ix.local.asn}, 1000) ~ bgp_ext_community);
        ${prefixLimitSnippet ix.remote.prefix_limit.v4}
      };
    };
    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;
      ipv6 {
        table ${ixName}6;
        import all;
        export where ((ro, ${toString ix.local.asn}, 1000) ~ bgp_ext_community);
        ${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.<foo>.local
                  type = submodule {
                    options = {
                      asn = mkOption { # lukegbgp.config.peering.<foo>.local.asn
                        type = int;
                      };
                      v4 = mkOption { # lukegbgp.config.peering.<foo>.local.v4
                        type = str;
                      };
                      v6 = mkOption { # lukegbgp.config.peering.<foo>.local.v6
                        type = str;
                      };
                    };
                  };
                };
                remote = mkOption { # lukegbgp.config.peering.<foo>.remote
                  type = submodule {
                    options = {
                      asn = mkOption { # lukegbgp.config.peering.<foo>.remote.asn
                        type = int;
                      };
                      passive = mkOption { # lukegbgp.config.peering.<foo>.remote.passive
                        type = bool;
                        default = false;
                      };
                      export_community = mkOption { # lukegbgp.config.peering.<foo>.remote.export_community
                        type = int;
                      };
                      prefix_limit.v4 = mkOption { # lukegbgp.config.peering.<foo>.remote.prefix_limit.v4
                        type = nullOr int;
                        default = null;
                      };
                      prefix_limit.v6 = mkOption { # lukegbgp.config.peering.<foo>.remote.prefix_limit.v6
                        type = nullOr int;
                        default = null;
                      };
                      must_be_next_hop = mkOption { # lukegbgp.config.peering.<foo>.remote.must_be_next_hop
                        type = bool;
                        default = true;
                      };
                      drop_asns = mkOption { # lukegbgp.config.peering.<foo>.remote.drop_asns
                        type = listOf int;
                        default = [];
                      };
                      bgp_local_pref = mkOption { # lukegbgp.config.peering.<foo>.remote.bgp_local_pref
                        type = int;
                        default = 100;
                      };
                      routers = mkOption { # lukegbgp.config.peering.<foo>.remote.routers
                        type = listOf (submodule {
                          options = {
                            enabled = mkOption { # lukegbgp.config.peering.<foo>.remote.routers.<n>.enabled
                              type = bool;
                              default = true;
                            };
                            v4 = mkOption { # lukegbgp.config.peering.<foo>.remote.routers.<n>.v4
                              type = str;
                            };
                            v6 = mkOption { # lukegbgp.config.peering.<foo>.remote.routers.<n>.v6
                              type = str;
                            };
                            multihop = mkOption { # lukegbgp.config.peering.<foo>.remote.routers.<n>.multihop
                              type = nullOr int;
                              default = null;
                            };
                            password = mkOption { # lukegbgp.config.peering.<foo>.remote.routers.<n>.password
                              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 = "";
                };
              };
            };
          };
          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 blade-tuvok

              # 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 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 blade-tuvok/blade-paris
              bgp_ext_community.add((ro, 205479, 4002));  #
              bgp_ext_community.add((ro, 205479, 5000));  # linx route collector from blade-tuvok 
              bgp_ext_community.add((ro, 205479, 5001));  # linx route server from blade-tuvok
              bgp_ext_community.add((ro, 205479, 5002));  # facebook from blade-tuvok
              bgp_ext_community.add((ro, 205479, 5003));  # openpeering from blade-tuvok
              bgp_ext_community.add((ro, 205479, 5004));  # freetransitnet from blade-tuvok
              bgp_ext_community.add((ro, 205479, 5005));  # he from blade-tuvok
              bgp_ext_community.add((ro, 205479, 5006));  # clouvider from blade-tuvok
              bgp_ext_community.add((ro, 205479, 5007));  # google from blade-tuvok
              bgp_ext_community.add((ro, 205479, 5008));  # cloudflare from blade-tuvok
              bgp_ext_community.add((ro, 205479, 5009));  # fastly from blade-tuvok

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