# Test the firewall module.

{ lib, backend, ... }:
{
  name = "firewall-${backend}";
  meta = with lib.maintainers; {
    maintainers = [
      prince213
      rvfg
      garyguo
    ];
  };

  nodes = {
    walled =
      { lib, ... }:
      {
        networking.firewall = {
          enable = true;
          inherit backend;
          logRefusedPackets = true;
          # Syntax smoke test, not actually verified otherwise
          allowedTCPPorts = [
            25
            993
            8005
          ];
          allowedTCPPortRanges = [
            {
              from = 980;
              to = 1000;
            }
            {
              from = 990;
              to = 1010;
            }
            {
              from = 8000;
              to = 8010;
            }
          ];
          interfaces = lib.mkIf (backend != "firewalld") {
            eth0 = {
              allowedTCPPorts = [ 10003 ];
              allowedTCPPortRanges = [
                {
                  from = 10000;
                  to = 10005;
                }
              ];
            };
            eth3 = {
              allowedUDPPorts = [ 10003 ];
              allowedUDPPortRanges = [
                {
                  from = 10000;
                  to = 10005;
                }
              ];
            };
          };
        };
        services.firewalld.enable = backend == "firewalld";
        networking.nftables.enable = backend != "iptables";
        services.httpd.enable = true;
        services.httpd.adminAddr = "foo@example.org";

        specialisation.different-config.configuration = {
          networking.firewall.rejectPackets = true;
        };
      };

    attacker =
      { ... }:
      {
        services.httpd.enable = true;
        services.httpd.adminAddr = "foo@example.org";
        networking.firewall.enable = false;
      };
  };

  testScript =
    { nodes, ... }:
    let
      unit = if backend == "iptables" then "firewall" else backend;
      openPort =
        if backend == "firewalld" then
          "firewall-cmd --add-port=80/tcp"
        else
          "nixos-firewall-tool open tcp 80";
      reset = if backend == "firewalld" then "firewall-cmd --reload" else "nixos-firewall-tool reset";
      # https://github.com/firewalld/firewalld/issues/1571
      waitForFirewalld = lib.optionalString (backend == "firewalld") ''
        walled.wait_until_succeeds("firewall-cmd --state")
      '';
    in
    ''
      start_all()

      walled.wait_for_unit("${unit}")
      walled.wait_for_unit("httpd")
      ${waitForFirewalld}
      attacker.wait_for_unit("network.target")

      # Local connections should still work.
      walled.succeed("curl -v http://localhost/ >&2")

      # Connections to the firewalled machine should fail, but ping should succeed.
      attacker.fail("curl --fail --connect-timeout 2 http://walled/ >&2")
      attacker.succeed("ping -c 1 walled >&2")

      # Outgoing connections/pings should still work.
      walled.succeed("curl -v http://attacker/ >&2")
      walled.succeed("ping -c 1 attacker >&2")

      # Open tcp port 80 at runtime
      walled.succeed("${openPort}")
      attacker.succeed("curl -v http://walled/ >&2")

      # Reset the firewall
      walled.succeed("${reset}")
      attacker.fail("curl --fail --connect-timeout 2 http://walled/ >&2")

      # Check whether activation of a new configuration reloads the firewall.
      walled.succeed(
          "/run/booted-system/specialisation/different-config/bin/switch-to-configuration test 2>&1 | grep -F ${unit}.service"
      )

      # If we stop the firewall, then connections should succeed.
      walled.stop_job("${unit}")
      attacker.succeed("curl -v http://walled/ >&2")
    '';
}
