{ 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;
  };
  services.resolved.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}"
    '';

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

  # This value determines the NixOS release with which your system is to be
  # compatible, in order to avoid breaking some software such as database
  # servers. You should change this only after NixOS release notes say you
  # should.
  system.stateVersion = "19.09"; # Did you read the comment?
}
