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

let
  inherit (zentralwerk.lib.config.site.net) core;
  inherit (config.networking) hostName;
  coreAddress = core.hosts4.${hostName};
  meshInterface = "bmx";
  meshLoopback = "bmx_prime";
  ddmeshRegisterUrl = "https://register.freifunk-dresden.de/bot.php";
  ddmeshBroadcast = "10.255.255.255";
  ddmeshNode = 51073;
  ddmeshAddrPart = "200.74";
  rt_table_hosts = 7;
  rt_table_nets = rt_table_hosts + 1;
  rt_table_tuns = rt_table_hosts + 2;
  rt_table_ffgw = 244;
  sysinfo-json = import ./sysinfo-json.nix { inherit pkgs ddmeshNode; };

  upstreams = [ "upstream4" "upstream3" ];
  upstreamMark = 3;
  rt_table_upstream = 100;

  node51001AddrPart = "200.2";

  mac = {
    core = "00:de:13:cb:9a:7b";
    bmx = "00:de:13:cb:9a:7c";
  };
  bmxdGatewayScript = with pkgs; writeScript "bmxd-gateway.sh" ''
    #! ${runtimeShell} -e

    echo "bmxd-gateway.sh: $@" >&2

    PATH=${lib.makeBinPath [ iproute2 ]}
    case "$1" in
      gateway)
        ;;
      del|init)
        ;;
      *)
        ip tunnel del ffgw || true

        ip tunnel add ffgw mode ipip local 10.200.${ddmeshAddrPart} remote "$1"
        ip link set ffgw up
        ip addr add 10.200.${ddmeshAddrPart}/32 dev ffgw
        ip route add 0/0 dev ffgw table ${toString rt_table_ffgw}
        ip rule add pref 33500 table ${toString rt_table_ffgw} || true
        ;;
    esac
  '';

in
{
  imports = [
    "${modulesPath}/profiles/minimal.nix"
  ];

  boot = {
    postBootCommands = ''
      if [ ! -c /dev/net/tun ]; then
        mkdir -p /dev/net
        mknod -m 666 /dev/net/tun c 10 200
      fi
    '';
    tmp.useTmpfs = true;
  };
  c3d2 = {
    hq.statistics.enable = true;
    deployment = {
      server = "server10";
      autoNetSetup = false;
    };
  };

  microvm.interfaces = [{
    type = "tap";
    id = "core-freifunk";
    mac = mac.core;
  }
    {
      type = "tap";
      id = "bmx-freifunk";
      mac = mac.bmx;
    }];

  networking = {
    firewall.enable = false;
    hostName = "freifunk";
    # Configure rt_table name
    iproute2 = {
      enable = true;
      rttablesExtraConfig = ''
        ${toString rt_table_upstream} upstream
        ${toString rt_table_hosts} bmx_hosts
        ${toString rt_table_nets} bmx_nets
        ${toString rt_table_tuns} bmx_tuns
      '';
    };
    nameservers = [ "172.20.73.8" "9.9.9.9" ];
    nat = {
      enable = true;
      # This doesn't really work, hence the `extraCommands`
      externalInterface = meshInterface;
      #internalInterfaces = [ "core" ];

      # Setup routing into Freifunk,
      # masquerading anything that isn't already their IP range
      extraCommands = ''
        ${pkgs.iptables}/bin/iptables -t nat -F POSTROUTING
        ${pkgs.iptables}/bin/iptables -t nat -A POSTROUTING \
          \! --source 10.200.0.0/15 -o ipip-node51001 -j SNAT --to 10.200.${ddmeshAddrPart}
        ${pkgs.iptables}/bin/iptables -t nat -o bat0 -A POSTROUTING -j MASQUERADE
        set -e
      '';
    };
    useNetworkd = true;
  };

  environment.systemPackages = with pkgs; [ tcpdump bmon wireguard-tools iperf bmxd ];

  sops = {
    age.sshKeyPaths = [ "/etc/ssh/ssh_host_ed25519_key" ];
    defaultSopsFile = ./secrets.yaml;
    secrets = {
      "bird/ospf/auth".owner = "bird2";
      "freifunk/ddmeshRegisterKey".owner = "nobody";
      "wireguard/vpn6/privateKey" = {
        group = "systemd-network";
        mode = "0440";
      };
    };
  };

  # unbreak wg-vpn6 ingress path
  boot.kernel.sysctl."net.ipv4.conf.core.rp_filter" = 0;

  services = {
    # Advertise Freifunk routes to ZW core
    bird2 = {
      enable = true;
      # nix-build cannot access /run/secrets/
      checkConfig = false;
      config = ''
        protocol kernel K4 {
          ipv4 {
            export all;
          };
        }
        # BIRD routing table for Wireguard transport
        ipv4 table upstream4_table;

        # Kernel routing table for Wireguard transport
        protocol kernel upstream4 {
          kernel table ${toString rt_table_upstream};
          ipv4 {
            export all;
            table upstream4_table;
          };
        }
        protocol kernel K6 {
          ipv6 {
            export all;
          };
        }
        protocol device {
          scan time 10;
        }

        ipv4 table bmx_gw;
        protocol kernel BMX_GW {
          learn;
          kernel table ${toString rt_table_tuns};
          ipv4 {
            table bmx_gw;
            import filter {
              if net ~ [ 0.0.0.0/0 ] then {
                # Learn Freifunk default route
                accept;
              }
              reject;
            };
          };
        }
        protocol pipe import_bmx_gw {
          table master4;
          peer table bmx_gw;
          import all;
        }

        protocol ospf v2 ZW4 {
          ipv4 {
            export where net != 0.0.0.0/0;
            import where net != 0.0.0.0/0;
          };
          area 0 {
            stubnet 10.200.0.0/15;
            interface "core" {
              hello 10;
              wait 20;
              include "${config.sops.secrets."bird/ospf/auth".path}";
            };
          };
        }

        protocol ospf v2 ZW4_freifunk {
          ipv4 {
            export where net = 0.0.0.0/0;
          };
          area 0 {
            interface "core" instance ${toString zentralwerk.lib.config.site.hosts.freifunk.ospf.upstreamInstance} {
              hello 10;
              wait 20;
              include "${config.sops.secrets."bird/ospf/auth".path}";
            };
          };
        }

        protocol ospf v3 ZW6 {
          ipv6 {
            import all;
          };
          area 0 {
            interface "core" {
              hello 10;
              wait 20;
              include "${config.sops.secrets."bird/ospf/auth".path}";
            };
          };
        }

        ${lib.concatStrings (lib.imap0 (i: upstream: ''
          # OSPFv2 to receive a default route from ${upstream}
          protocol ospf v2 ZW4_${upstream} {
            ipv4 {
              import filter {
                preference = preference + ${toString (200 - i)};
                accept;
              };
              table upstream4_table;
            };
            area 0 {
              interface "core" instance ${toString zentralwerk.lib.config.site.hosts.${upstream}.ospf.upstreamInstance} {
                hello 10;
                wait 20;
                include "${config.sops.secrets."bird/ospf/auth".path}";
              };
            };
          };
        '') upstreams)}

        ${lib.concatStrings (lib.imap0 (i: upstream: ''
          # OSPFv3 to receive a default route from ${upstream}
          protocol ospf v3 ZW6_${upstream} {
            ipv6 {
              import filter {
                preference = preference + ${toString (200 - i)};
                accept;
              };
            };
            area 0 {
              interface "core" instance ${toString zentralwerk.lib.config.site.hosts.${upstream}.ospf.upstreamInstance} {
                hello 10;
                wait 20;
                include "${config.sops.secrets."bird/ospf/auth".path}";
              };
            };
          };
        '') upstreams)}

        router id ${coreAddress};
      '';
    };
    collectd.plugins.protocols = "";

    # HTTP Reverse Proxy to provide services into Freifunk
    nginx = {
      enable = true;
      appendHttpConfig = ''
        proxy_buffering off;
      '';

      virtualHosts = {
        "c3d2.ffdd" = {
          default = true;
          root = ./assets;
          locations =
            let
              sysinfo-json = {
                alias = "/run/nginx/sysinfo.json";
                extraConfig = ''
                  default_type "application/json;charset=UTF-8";
                '';
              };
            in
            {
              "/" = {
                index = "index.html";
                extraConfig = ''
                  etag off;
                  add_header etag "\"${builtins.substring 11 32 (./assets)}\"";
                '';
              };
              "=/sysinfo-json.cgi" = sysinfo-json;
              "=/sysinfo.json" = sysinfo-json;
            };
        };
        "storage.hq.c3d2.ffdd".locations."/".proxyPass =
          "http://storage.hq.c3d2.de/";
        "grafana.hq.c3d2.ffdd".locations."/" = {
          proxyPass = "https://grafana.hq.c3d2.de/";
          extraConfig = ''
            proxy_ssl_server_name on;
          '';
        };
        "influxdb.hq.c3d2.ffdd".locations."/".proxyPass =
          "http://grafana.hq.c3d2.de:8086/";
      };
    };
  };

  systemd = {
    network = {
      netdevs = {
        # Dummy interface for primary (10.200) address
        "10-bmx-prime" = {
          enable = true;
          netdevConfig = {
            Kind = "dummy";
            Name = meshLoopback;
          };
        };
        # Freifunk Dresden Backbone
        "31-wg-vpn6" = {
          enable = true;
          netdevConfig = {
            Name = "wg-vpn6";
            Kind = "wireguard";
            MTUBytes = "1320";
          };
          wireguardConfig = {
            PrivateKeyFile = config.sops.secrets."wireguard/vpn6/privateKey".path;
            ListenPort = 5006;
            # Mark for routing with the upstream routing table
            FirewallMark = upstreamMark;
          };
          wireguardPeers = [{
            wireguardPeerConfig = {
              Endpoint = "vpn-it4r.freifunk-dresden.de:5006";
              PublicKey = "CIJa7xiRRIrLtEB7uyzwoyaQcpe0b8F2d16+3hk8KjU=";
              AllowedIPs = "10.203.0.0/16";
            };
          }];
        };
        "32-ipip-node51001" = {
          enable = true;
          netdevConfig = {
            Name = "ipip-node51001";
            Kind = "ipip";
          };
          tunnelConfig = {
            Local = "10.203.${ddmeshAddrPart}";
            Remote = "10.203.${node51001AddrPart}";
          };
        };
      };
      links = {
        # Wired mesh interface
        "10-bmx" = {
          enable = true;
          matchConfig = { MACAddress = mac.bmx; };
          linkConfig.Name = meshInterface;
        };
        # Wired core interface
        "10-core" = {
          enable = true;
          matchConfig = { MACAddress = mac.core; };
          linkConfig.Name = "core";
        };
      };
      networks = {
        # Wired mesh interface
        "10-bmx" = {
          enable = true;
          matchConfig = { MACAddress = mac.bmx; };
          addresses = [{
            addressConfig = {
              Address = "10.201.${ddmeshAddrPart}/16";
              Broadcast = ddmeshBroadcast;
            };
          }];
          routingPolicyRules = [{
            routingPolicyRuleConfig = {
              Priority = 300;
              To = "10.200.0.0/16";
              Table = rt_table_hosts;
            };
          }];
        };
        # Dummy interface for primary (10.200) address
        "11-bmx-prime" = {
          enable = true;
          matchConfig = { Name = meshLoopback; };
          addresses = [{
            addressConfig.Address = "10.200.${ddmeshAddrPart}/32";
          }];
          routingPolicyRules = [{
            routingPolicyRuleConfig = {
              Priority = 33000;
              Table = rt_table_tuns;
            };
          }];
        };
        "31-wg-vpn6" = {
          enable = true;
          matchConfig.Name = "wg-vpn6";
          addresses = [{
            addressConfig.Address = "10.203.${ddmeshAddrPart}/16";
          }];
          # reverse dependency
          networkConfig.Tunnel = [ "ipip-node51001" ];
        };
        "32-ipip-node51001" = {
          enable = true;
          matchConfig.Name = "ipip-node51001";
          addresses = [{
            addressConfig = {
              Address = "10.201.${ddmeshAddrPart}/16";
              Broadcast = ddmeshBroadcast;
            };
          }];
        };
        # ZW
        "20-core" = {
          enable = true;
          matchConfig = { MACAddress = mac.core; };
          addresses = map (Address: { addressConfig = { inherit Address; }; }) (
            [
              "${coreAddress}/${toString core.subnet4Len}"
            ] ++
            map (hosts6: "${hosts6.${hostName}}/64") (
              builtins.attrValues core.hosts6
            )
          );
          routingPolicyRules = [{
            # Marked wireguard packets take the upstream routing table
            routingPolicyRuleConfig = {
              Table = rt_table_upstream;
              FirewallMark = upstreamMark;
            };
          }];
        };
      };
    };

    # Freifunk Dresden routing daemon
    services.bmxd = {
      after = [ "systemd-networkd.service" ];
      wantedBy = [ "network.target" ];
      serviceConfig = {
        ExecStart = ''
          ${pkgs.bmxd}/sbin/bmxd \
            --rt_table_offset=${toString rt_table_hosts} \
            --no_fork 1 \
            --throw-rules 0 \
            --prio-rules 0 \
            --network 10.200.0.0/16 \
            --netid 0 \
            --only_community_gw 1 \
            --script ${bmxdGatewayScript} \
            --hop_penalty 1 \
            --lateness_penalty 10 \
            --ogm_broadcasts 100 \
            --udp_data_size 512 \
            --ogm_interval 5000 \
            --purge_timeout 35 \
            -r 3 --gateway_hysteresis 20 \
            --dev ${meshLoopback} /linklayer 0 \
            --dev ${meshInterface} /linklayer 1 \
            --dev ipip-node51001 /linklayer 1
        '';
        Restart = "always";
        RestartSec = "60";
      };
    };

    # Re-register periodically
    services.ddmesh-register-node = {
      script = ''
        ${lib.getExe pkgs.curl} -k -o /tmp/ddmesh-registration.json \
          "${ddmeshRegisterUrl}?registerkey=$(cat ${config.sops.secrets."freifunk/ddmeshRegisterKey".path})&node=${toString ddmeshNode}"
      '';
      serviceConfig = {
        User = "nobody";
        Group = "nogroup";
      };
    };

    timers.ddmesh-register-node = {
      partOf = [ "ddmesh-register-node.service" ];
      wantedBy = [ "timers.target" ];
      timerConfig.OnCalendar = "daily";
    };

    # Refresh sysinfo.json
    services.sysinfo-json = {
      script = ''
        ${sysinfo-json}/bin/bmxddump.sh
        mkdir -p /run/nginx
        ${sysinfo-json}/bin/sysinfo-json.cgi > /run/nginx/sysinfo.json
      '';
    };
    timers.sysinfo-json = {
      partOf = [ "sysinfo-json.service" ];
      wantedBy = [ "timers.target" ];
      timerConfig.OnCalendar = "minutely";
    };
  };

  # 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 = "20.03"; # Did you read the comment?
}
