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

with lib;

let
  cfg = config.services.sslh;
  user = "sslh";

  configFormat = pkgs.formats.libconfig {};
  configFile = configFormat.generate "sslh.conf" cfg.settings;
in

{
  imports = [
    (mkRenamedOptionModule [ "services" "sslh" "listenAddress" ] [ "services" "sslh" "listenAddresses" ])
    (mkRenamedOptionModule [ "services" "sslh" "timeout" ] [ "services" "sslh" "settings" "timeout" ])
    (mkRenamedOptionModule [ "services" "sslh" "transparent" ] [ "services" "sslh" "settings" "transparent" ])
    (mkRemovedOptionModule [ "services" "sslh" "appendConfig" ] "Use services.sslh.settings instead")
    (mkChangedOptionModule [ "services" "sslh" "verbose" ] [ "services" "sslh" "settings" "verbose-connections" ]
      (config: if config.services.sslh.verbose then 1 else 0))
  ];

  meta.buildDocsInSandbox = false;

  options.services.sslh = {
    enable = mkEnableOption "sslh, protocol demultiplexer";

    method = mkOption {
      type = types.enum [ "fork" "select" "ev" ];
      default = "fork";
      description = ''
        The method to use for handling connections:

          - `fork` forks a new process for each incoming connection. It is
          well-tested and very reliable, but incurs the overhead of many
          processes.

          - `select` uses only one thread, which monitors all connections at once.
          It has lower overhead per connection, but if it stops, you'll lose all
          connections.

          - `ev` is implemented using libev, it's similar to `select` but
            scales better to a large number of connections.
      '';
    };

    listenAddresses = mkOption {
      type = with types; coercedTo str singleton (listOf str);
      default = [ "0.0.0.0" "[::]" ];
      description = "Listening addresses or hostnames.";
    };

    port = mkOption {
      type = types.port;
      default = 443;
      description = "Listening port.";
    };

    settings = mkOption {
      type = types.submodule {
        freeformType = configFormat.type;

        options.timeout = mkOption {
          type = types.ints.unsigned;
          default = 2;
          description = "Timeout in seconds.";
        };

        options.transparent = mkOption {
          type = types.bool;
          default = false;
          description = ''
            Whether the services behind sslh (Apache, sshd and so on) will see the
            external IP and ports as if the external world connected directly to
            them.
          '';
        };

        options.verbose-connections = mkOption {
          type = types.ints.between 0 4;
          default = 0;
          description = ''
            Where to log connections information. Possible values are:

             0. don't log anything
             1. write log to stdout
             2. write log to syslog
             3. write log to both stdout and syslog
             4. write to a log file ({option}`sslh.settings.logfile`)
          '';
        };

        options.numeric = mkOption {
          type = types.bool;
          default = true;
          description = ''
            Whether to disable reverse DNS lookups, thus keeping IP
            address literals in the log.
          '';
        };

        options.protocols = mkOption {
          type = types.listOf configFormat.type;
          default = [
            { name = "ssh";     host = "localhost"; port =  "22"; service= "ssh"; }
            { name = "openvpn"; host = "localhost"; port = "1194"; }
            { name = "xmpp";    host = "localhost"; port = "5222"; }
            { name = "http";    host = "localhost"; port =   "80"; }
            { name = "tls";     host = "localhost"; port =  "443"; }
            { name = "anyprot"; host = "localhost"; port =  "443"; }
          ];
          description = ''
            List of protocols sslh will probe for and redirect.
            Each protocol entry consists of:

              - `name`: name of the probe.

              - `service`: libwrap service name (see {manpage}`hosts_access(5)`),

              - `host`, `port`: where to connect when this probe succeeds,

              - `log_level`: to log incoming connections,

              - `transparent`: proxy this protocol transparently,

              - etc.

            See the documentation for all options, including probe-specific ones.
          '';
        };
      };
      description = "sslh configuration. See {manpage}`sslh(8)` for available settings.";
    };
  };

  config = mkMerge [
    (mkIf cfg.enable {
      systemd.services.sslh = {
        description = "Applicative Protocol Multiplexer (e.g. share SSH and HTTPS on the same port)";
        after = [ "network.target" ];
        wantedBy = [ "multi-user.target" ];

        serviceConfig = {
          DynamicUser          = true;
          User                 = "sslh";
          PermissionsStartOnly = true;
          Restart              = "always";
          RestartSec           = "1s";
          ExecStart            = "${pkgs.sslh}/bin/sslh-${cfg.method} -F${configFile}";
          KillMode             = "process";
          AmbientCapabilities  = ["CAP_NET_BIND_SERVICE" "CAP_NET_ADMIN" "CAP_SETGID" "CAP_SETUID"];
          PrivateTmp           = true;
          PrivateDevices       = true;
          ProtectSystem        = "full";
          ProtectHome          = true;
        };
      };

      services.sslh.settings = {
        # Settings defined here are not supposed to be changed: doing so will
        # break the module, as such you need `lib.mkForce` to override them.
        foreground = true;
        inetd = false;
        listen = map (addr: { host = addr; port = toString cfg.port; }) cfg.listenAddresses;
      };

    })

    # code from https://github.com/yrutschle/sslh#transparent-proxy-support
    # the only difference is using iptables mark 0x2 instead of 0x1 to avoid conflicts with nixos/nat module
    (mkIf (cfg.enable && cfg.settings.transparent) {
      # Set route_localnet = 1 on all interfaces so that ssl can use "localhost" as destination
      boot.kernel.sysctl."net.ipv4.conf.default.route_localnet" = 1;
      boot.kernel.sysctl."net.ipv4.conf.all.route_localnet"     = 1;

      systemd.services.sslh = let
        iptablesCommands = [
          # DROP martian packets as they would have been if route_localnet was zero
          # Note: packets not leaving the server aren't affected by this, thus sslh will still work
          { table = "raw";    command = "PREROUTING  ! -i lo -d 127.0.0.0/8 -j DROP"; }
          { table = "mangle"; command = "POSTROUTING ! -o lo -s 127.0.0.0/8 -j DROP"; }
          # Mark all connections made by ssl for special treatment (here sslh is run as user ${user})
          { table = "nat";    command = "OUTPUT -m owner --uid-owner ${user} -p tcp --tcp-flags FIN,SYN,RST,ACK SYN -j CONNMARK --set-xmark 0x02/0x0f"; }
          # Outgoing packets that should go to sslh instead have to be rerouted, so mark them accordingly (copying over the connection mark)
          { table = "mangle"; command = "OUTPUT ! -o lo -p tcp -m connmark --mark 0x02/0x0f -j CONNMARK --restore-mark --mask 0x0f"; }
        ];
        ip6tablesCommands = [
          { table = "raw";    command = "PREROUTING  ! -i lo -d ::1/128     -j DROP"; }
          { table = "mangle"; command = "POSTROUTING ! -o lo -s ::1/128     -j DROP"; }
          { table = "nat";    command = "OUTPUT -m owner --uid-owner ${user} -p tcp --tcp-flags FIN,SYN,RST,ACK SYN -j CONNMARK --set-xmark 0x02/0x0f"; }
          { table = "mangle"; command = "OUTPUT ! -o lo -p tcp -m connmark --mark 0x02/0x0f -j CONNMARK --restore-mark --mask 0x0f"; }
        ];
      in {
        path = [ pkgs.iptables pkgs.iproute2 pkgs.procps ];

        preStart = ''
          # Cleanup old iptables entries which might be still there
          ${concatMapStringsSep "\n" ({table, command}: "while iptables -w -t ${table} -D ${command} 2>/dev/null; do echo; done") iptablesCommands}
          ${concatMapStringsSep "\n" ({table, command}:       "iptables -w -t ${table} -A ${command}"                           ) iptablesCommands}

          # Configure routing for those marked packets
          ip rule  add fwmark 0x2 lookup 100
          ip route add local 0.0.0.0/0 dev lo table 100

        '' + optionalString config.networking.enableIPv6 ''
          ${concatMapStringsSep "\n" ({table, command}: "while ip6tables -w -t ${table} -D ${command} 2>/dev/null; do echo; done") ip6tablesCommands}
          ${concatMapStringsSep "\n" ({table, command}:       "ip6tables -w -t ${table} -A ${command}"                           ) ip6tablesCommands}

          ip -6 rule  add fwmark 0x2 lookup 100
          ip -6 route add local ::/0 dev lo table 100
        '';

        postStop = ''
          ${concatMapStringsSep "\n" ({table, command}: "iptables -w -t ${table} -D ${command}") iptablesCommands}

          ip rule  del fwmark 0x2 lookup 100
          ip route del local 0.0.0.0/0 dev lo table 100
        '' + optionalString config.networking.enableIPv6 ''
          ${concatMapStringsSep "\n" ({table, command}: "ip6tables -w -t ${table} -D ${command}") ip6tablesCommands}

          ip -6 rule  del fwmark 0x2 lookup 100
          ip -6 route del local ::/0 dev lo table 100
        '';
      };
    })
  ];
}
