{ 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 ''; }; }) ]; }