{ 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";
  inherit (pkgs.c3d2-freifunk) ddmeshRegisterKey;
  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.tmpOnTmpfs = true;
  boot.postBootCommands = ''
    if [ ! -c /dev/net/tun ]; then
      mkdir -p /dev/net
      mknod -m 666 /dev/net/tun c 10 200
    fi
  '';
  c3d2 = {
    hq.statistics.enable = true;
    deployment = {
      server = "server10";
      autoNetSetup = false;
    };
  };
  services.collectd.plugins.protocols = "";

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

  networking.hostName = "freifunk";
  networking.useNetworkd = true;
  networking.nameservers = [ "172.20.73.8" "9.9.9.9" ];
  networking.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 ${meshInterface} -j SNAT --to 10.200.${ddmeshAddrPart}
      ${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
    '';
  };
  # Configure rt_table name
  networking.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
    '';
  };

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

  sops = {
    age.sshKeyPaths = [ "/etc/ssh/ssh_host_ed25519_key" ];
    defaultSopsFile = ./secrets.yaml;
    secrets."wireguard/vpn6/privateKey" = {
      group = "systemd-network";
      mode = "0440";
    };
  };

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

  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 = {
            # vpn6.freifunk-dresden.de
            Endpoint = "85.195.253.5: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
  systemd.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
  systemd.services.ddmesh-register-node = {
    script = ''
      ${pkgs.curl}/bin/curl -k \
        -o /tmp/ddmesh-registration.json \
        '${ddmeshRegisterUrl}?registerkey=${ddmeshRegisterKey}&node=${
          toString ddmeshNode
        }'
    '';
    serviceConfig = {
      User = "nobody";
      Group = "nogroup";
    };
  };
  systemd.timers.ddmesh-register-node = {
    partOf = [ "ddmesh-register-node.service" ];
    wantedBy = [ "timers.target" ];
    timerConfig.OnCalendar = "daily";
  };

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

  # Advertise Freifunk routes to ZW core
  services.bird2 = {
    enable = true;
    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;
            authentication cryptographic;
            password "${pkgs.zentralwerk-ospf-message-digest-key}";
          };
        };
      }

      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;
            authentication cryptographic;
            password "${pkgs.zentralwerk-ospf-message-digest-key}";
          };
        };
      }

      protocol ospf v3 ZW6 {
        ipv6 {
          import all;
        };
        area 0 {
          interface "core" {
            hello 10;
            wait 20;
            authentication cryptographic;
            password "${pkgs.zentralwerk-ospf-message-digest-key}";
          };
        };
      }

      ${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;
              authentication cryptographic;
              password "${pkgs.zentralwerk-ospf-message-digest-key}";
            };
          };
        };
      '') 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;
              authentication cryptographic;
              password "${pkgs.zentralwerk-ospf-message-digest-key}";
            };
          };
        };
      '') upstreams)}

      router id ${coreAddress};
    '';
  };

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

    virtualHosts = {
      "c3d2.ffdd" = {
        default = true;
        root = ./assets;
        locations = let
          sysinfo-json = {
            alias = "/run/nginx/sysinfo.json";
            extraConfig = ''
              add_header Content-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/";
    };
  };

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