{ config, pkgs, lib, ... }:

let
  address4 = "172.22.99.253";
  address6 = "fe80::deca:fbad";
  inherit (pkgs) neighbors;
in
{
  networking = {
    domain = "hq.c3d2.de";
    hostName = "dn42";
    useNetworkd = true;
    # No Firewalling!
    firewall.enable = false;
  };

  c3d2 = {
    hq = {
      interface = "c3d2";
      statistics.enable = true;
    };
    deployment.server = "server10";
  };

  services = {
    collectd.plugins.exec =
      let
        routecount = pkgs.writeScript "run-routecount" ''
          #!${pkgs.bash}/bin/bash

          export PATH=${lib.makeBinPath (with pkgs; [ ruby iproute ] )}
          ruby ${./routecount.rb}
        '';
      in
      ''
        Exec "collectd" "${routecount}"
      '';
    resolved.enable = false;
  };

  sops = {
    age.sshKeyPaths = [ "/etc/ssh/ssh_host_ed25519_key" ];
    defaultSopsFile = ./secrets.yaml;
    secrets = builtins.foldl'
      (result: name:
        let
          conf = neighbors.${name};
        in
        result // (
          if conf ? openvpn
          then { "neighbors/${name}/openvpn/key" = { }; }
          else if conf ? wireguard
          then { "neighbors/${name}/wireguard/privateKey" = { }; }
          else { }
        )
      )
      { }
      (builtins.attrNames neighbors);
  };

  boot.kernel.sysctl = {
    "net.ipv4.conf.all.forwarding" = true;
    "net.ipv4.conf.default.forwarding" = true;
    "net.ipv6.conf.all.forwarding" = true;
    "net.ipv6.conf.default.forwarding" = true;
  };

  boot.postBootCommands = /* bash */''
    if [ ! -c /dev/net/tun ]; then
      mkdir -p /dev/net
      mknod -m 666 /dev/net/tun c 10 200
    fi
  '';

  services.openvpn =
    let
      openvpnNeighbors = lib.filterAttrs (_: conf: conf ? openvpn) neighbors;
      mkServer = name: conf: {
        config = ''
          dev ${name}
          dev-type tun
          ifconfig ${address4} ${conf.address4}
          user nobody
          group nogroup
          persist-tun
          persist-key
          ping 30
          ping-restart 45
          verb 1
          ${conf.openvpn}
          secret ${config.sops.secrets."neighbors/${name}/openvpn/key".path}
        '';
        up = ''
          ${pkgs.iproute}/bin/ip addr flush dev $1
          ${pkgs.iproute}/bin/ip addr add ${address4} dev ${name} peer ${conf.address4}/32
          ${pkgs.iproute}/bin/ip addr add ${address6}/64 dev $1
        '';
      };
    in
    {
      servers = builtins.mapAttrs mkServer openvpnNeighbors;
    };

  networking.wireguard = {
    enable = true;
    interfaces =
      let
        wireguardNeighbors = lib.filterAttrs (_: conf: conf ? wireguard) neighbors;
      in
      builtins.mapAttrs (name: conf: {
        inherit (conf.wireguard) listenPort;
        privateKeyFile = config.sops.secrets."neighbors/${name}/wireguard/privateKey".path;
        ips = [ "${address4}/32" "${address6}/64" ];
        allowedIPsAsRoutes = false;
        postSetup = ''
          ${pkgs.iproute}/bin/ip addr del ${address4}/32 dev ${name}
          ${pkgs.iproute}/bin/ip addr add ${address4} dev ${name}${if conf ? address4 then " peer ${conf.address4}/32" else ""}
        '';
        peers = [
          ({
            inherit (conf.wireguard) publicKey;
            allowedIPs = [ "0.0.0.0/0" "::0/0" ];
            persistentKeepalive = 30;
          } // (lib.optionalAttrs (conf.wireguard ? endpoint) {
            inherit (conf.wireguard) endpoint;
          }))
        ];
      }) wireguardNeighbors;
  };

  services.bird2 = {
    enable = true;
    config =
      let
        bgpNeighbors = builtins.concatStringsSep "\n" (builtins.attrValues
          (builtins.mapAttrs
            (name: conf@{ multiprotocol ? false, ... }:
              let
                neighbor4 =
                  if conf ? address4 && multiprotocol != "ipv6" then ''
                    protocol bgp ${name}_4 from dnpeers {
                      neighbor ${conf.address4} as ${builtins.toString conf.asn};
                    }
                  '' else
                    "";
                neighbor6 =
                  if conf ? address6 && multiprotocol != "ipv4" then ''
                    protocol bgp ${name}_6 from dnpeers {
                      neighbor ${conf.address6}%${interface} as ${
                        builtins.toString conf.asn
                      };
                    }
                  '' else
                    "";
                interface = conf.interface or name;
              in
              "${neighbor4}${neighbor6}") neighbors));
      in
      ''
        protocol kernel {
          ipv4 {
            export all;
          };
        }
        protocol kernel {
          ipv6 {
            export all;
          };
        }
        protocol device {
          scan time 10;
        }

        protocol static {
          ipv4;
          route 10.0.0.0/8 unreachable;
          route 172.16.0.0/12 unreachable;
          route 192.168.0.0/16 unreachable;
        }

        protocol static {
          ipv6;
          route 2000::/3 via 2a00:8180:2c00:281::c3d2:3;
          route fd00::/8 unreachable;
        }


        protocol static hq4 {
          ipv4;
          route 172.22.99.0/24 via "c3d2";
        }

        protocol static hq6 {
          ipv6;
          route fd23:42:c3d2:500::/56 unreachable;
        }

        template bgp dnpeers {
          local as 64699;
          ipv4 {
            import filter {
              if proto = "hq4" then reject;
              accept;
            };
            export filter {
              if source = RTS_BGP then accept;
              if proto = "hq4" then accept;
              reject;
            };
          };
          ipv6 {
            import filter {
              if proto = "hq6" then reject;
              accept;
            };
            export filter {
              if source = RTS_BGP then accept;
              if proto = "hq6" then accept;
              reject;
            };
          };
        }
        ${bgpNeighbors}

        router id ${address4};
      '';
  };

  system.stateVersion = "19.09";
}
