# 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;
    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 all;
    };
    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 all;
    };
  '' + lib.concatImapStringsSep "\n" ( i: v: generateSnippetForRouter (args // { routerNum = i; router = v; }) ) ix.remote.routers;
  enabledSnippet = { enabled ? true, ... }: "disabled ${if enabled then "off" else "on"}";
  generateSnippetForRouter = { ixName, ix, routerNum, router, ... }: ''
    protocol bgp ${ixName}${toString routerNum}_4 {
      ${enabledSnippet router};
      local ${ix.local.v4} as ${toString ix.local.asn};
      neighbor ${router.v4} as ${toString ix.remote.asn};
      ipv4 {
        table ${ixName}4;
        import all;
        export where ((ro, ${toString ix.local.asn}, 1000) ~ bgp_ext_community);
      };
    };
    protocol bgp ${ixName}${toString routerNum}_6 {
      ${enabledSnippet router};
      local ${ix.local.v6} as ${toString ix.local.asn};
      neighbor ${router.v6} as ${toString ix.remote.asn};
      ipv6 {
        table ${ixName}6;
        import all;
        export where ((ro, ${toString ix.local.asn}, 1000) ~ bgp_ext_community);
      };
    };
  '';

  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;
                      };
                      export_community = mkOption { # lukegbgp.config.peering.<foo>.remote.export_community
                        type = int;
                      };
                      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;
                            };
                          };
                        });
                      };
                    };
                  };
                };
              };
            });
          };
          export = mkOption { # lukegbgp.config.export
            default = { v4 = ["92.118.31.0/24"]; v6 = ["2a09:a440::/48"]; };
            type = submodule {
              options = {
                v4 = mkOption { # lukegbgp.config.export.v4
                  type = listOf str;
                  default = ["92.118.31.0/24"];
                };
                v6 = mkOption { # lukegbgp.config.export.v6
                  type = listOf str;
                  default = ["2a09:a440::/48"];
                };
              };
            };
          };
        };
      };
    };
  };

  config = {
    services.bird2 = lib.mkIf config.services.lukegbgp.enable {
      enable = true;
      config = ''
        router id ${config.services.lukegbgp.config.local.routerID};

        ${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));
              accept;
            };
          };
          ${lib.concatMapStrings (ip: "route ${ip} blackhole;") config.services.lukegbgp.config.export.v4}
        };
        protocol static export6 {
          ipv6 {
            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));
              accept;
            };
          };
          ${lib.concatMapStrings (ip: "route ${ip} blackhole;") config.services.lukegbgp.config.export.v6}
        };
      '';
    };

    networking.firewall.allowedTCPPorts = lib.mkIf config.services.lukegbgp.enable (lib.mkAfter [ 179 ]);

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