1 { config, lib, pkgs, ... }:
6 cfg = config.services.sslh;
9 configFormat = pkgs.formats.libconfig {};
10 configFile = configFormat.generate "sslh.conf" cfg.settings;
15 (mkRenamedOptionModule [ "services" "sslh" "listenAddress" ] [ "services" "sslh" "listenAddresses" ])
16 (mkRenamedOptionModule [ "services" "sslh" "timeout" ] [ "services" "sslh" "settings" "timeout" ])
17 (mkRenamedOptionModule [ "services" "sslh" "transparent" ] [ "services" "sslh" "settings" "transparent" ])
18 (mkRemovedOptionModule [ "services" "sslh" "appendConfig" ] "Use services.sslh.settings instead")
19 (mkChangedOptionModule [ "services" "sslh" "verbose" ] [ "services" "sslh" "settings" "verbose-connections" ]
20 (config: if config.services.sslh.verbose then 1 else 0))
23 meta.buildDocsInSandbox = false;
25 options.services.sslh = {
26 enable = mkEnableOption "sslh, protocol demultiplexer";
29 type = types.enum [ "fork" "select" "ev" ];
32 The method to use for handling connections:
34 - `fork` forks a new process for each incoming connection. It is
35 well-tested and very reliable, but incurs the overhead of many
38 - `select` uses only one thread, which monitors all connections at once.
39 It has lower overhead per connection, but if it stops, you'll lose all
42 - `ev` is implemented using libev, it's similar to `select` but
43 scales better to a large number of connections.
47 listenAddresses = mkOption {
48 type = with types; coercedTo str singleton (listOf str);
49 default = [ "0.0.0.0" "[::]" ];
50 description = "Listening addresses or hostnames.";
56 description = "Listening port.";
60 type = types.submodule {
61 freeformType = configFormat.type;
63 options.timeout = mkOption {
64 type = types.ints.unsigned;
66 description = "Timeout in seconds.";
69 options.transparent = mkOption {
73 Whether the services behind sslh (Apache, sshd and so on) will see the
74 external IP and ports as if the external world connected directly to
79 options.verbose-connections = mkOption {
80 type = types.ints.between 0 4;
83 Where to log connections information. Possible values are:
86 1. write log to stdout
87 2. write log to syslog
88 3. write log to both stdout and syslog
89 4. write to a log file ({option}`sslh.settings.logfile`)
93 options.numeric = mkOption {
97 Whether to disable reverse DNS lookups, thus keeping IP
98 address literals in the log.
102 options.protocols = mkOption {
103 type = types.listOf configFormat.type;
105 { name = "ssh"; host = "localhost"; port = "22"; service= "ssh"; }
106 { name = "openvpn"; host = "localhost"; port = "1194"; }
107 { name = "xmpp"; host = "localhost"; port = "5222"; }
108 { name = "http"; host = "localhost"; port = "80"; }
109 { name = "tls"; host = "localhost"; port = "443"; }
110 { name = "anyprot"; host = "localhost"; port = "443"; }
113 List of protocols sslh will probe for and redirect.
114 Each protocol entry consists of:
116 - `name`: name of the probe.
118 - `service`: libwrap service name (see {manpage}`hosts_access(5)`),
120 - `host`, `port`: where to connect when this probe succeeds,
122 - `log_level`: to log incoming connections,
124 - `transparent`: proxy this protocol transparently,
128 See the documentation for all options, including probe-specific ones.
132 description = "sslh configuration. See {manpage}`sslh(8)` for available settings.";
138 systemd.services.sslh = {
139 description = "Applicative Protocol Multiplexer (e.g. share SSH and HTTPS on the same port)";
140 after = [ "network.target" ];
141 wantedBy = [ "multi-user.target" ];
146 PermissionsStartOnly = true;
149 ExecStart = "${pkgs.sslh}/bin/sslh-${cfg.method} -F${configFile}";
150 KillMode = "process";
151 AmbientCapabilities = ["CAP_NET_BIND_SERVICE" "CAP_NET_ADMIN" "CAP_SETGID" "CAP_SETUID"];
153 PrivateDevices = true;
154 ProtectSystem = "full";
159 services.sslh.settings = {
160 # Settings defined here are not supposed to be changed: doing so will
161 # break the module, as such you need `lib.mkForce` to override them.
164 listen = map (addr: { host = addr; port = toString cfg.port; }) cfg.listenAddresses;
169 # code from https://github.com/yrutschle/sslh#transparent-proxy-support
170 # the only difference is using iptables mark 0x2 instead of 0x1 to avoid conflicts with nixos/nat module
171 (mkIf (cfg.enable && cfg.settings.transparent) {
172 # Set route_localnet = 1 on all interfaces so that ssl can use "localhost" as destination
173 boot.kernel.sysctl."net.ipv4.conf.default.route_localnet" = 1;
174 boot.kernel.sysctl."net.ipv4.conf.all.route_localnet" = 1;
176 systemd.services.sslh = let
178 # DROP martian packets as they would have been if route_localnet was zero
179 # Note: packets not leaving the server aren't affected by this, thus sslh will still work
180 { table = "raw"; command = "PREROUTING ! -i lo -d 127.0.0.0/8 -j DROP"; }
181 { table = "mangle"; command = "POSTROUTING ! -o lo -s 127.0.0.0/8 -j DROP"; }
182 # Mark all connections made by ssl for special treatment (here sslh is run as user ${user})
183 { table = "nat"; command = "OUTPUT -m owner --uid-owner ${user} -p tcp --tcp-flags FIN,SYN,RST,ACK SYN -j CONNMARK --set-xmark 0x02/0x0f"; }
184 # Outgoing packets that should go to sslh instead have to be rerouted, so mark them accordingly (copying over the connection mark)
185 { table = "mangle"; command = "OUTPUT ! -o lo -p tcp -m connmark --mark 0x02/0x0f -j CONNMARK --restore-mark --mask 0x0f"; }
187 ip6tablesCommands = [
188 { table = "raw"; command = "PREROUTING ! -i lo -d ::1/128 -j DROP"; }
189 { table = "mangle"; command = "POSTROUTING ! -o lo -s ::1/128 -j DROP"; }
190 { table = "nat"; command = "OUTPUT -m owner --uid-owner ${user} -p tcp --tcp-flags FIN,SYN,RST,ACK SYN -j CONNMARK --set-xmark 0x02/0x0f"; }
191 { table = "mangle"; command = "OUTPUT ! -o lo -p tcp -m connmark --mark 0x02/0x0f -j CONNMARK --restore-mark --mask 0x0f"; }
194 path = [ pkgs.iptables pkgs.iproute2 pkgs.procps ];
197 # Cleanup old iptables entries which might be still there
198 ${concatMapStringsSep "\n" ({table, command}: "while iptables -w -t ${table} -D ${command} 2>/dev/null; do echo; done") iptablesCommands}
199 ${concatMapStringsSep "\n" ({table, command}: "iptables -w -t ${table} -A ${command}" ) iptablesCommands}
201 # Configure routing for those marked packets
202 ip rule add fwmark 0x2 lookup 100
203 ip route add local 0.0.0.0/0 dev lo table 100
205 '' + optionalString config.networking.enableIPv6 ''
206 ${concatMapStringsSep "\n" ({table, command}: "while ip6tables -w -t ${table} -D ${command} 2>/dev/null; do echo; done") ip6tablesCommands}
207 ${concatMapStringsSep "\n" ({table, command}: "ip6tables -w -t ${table} -A ${command}" ) ip6tablesCommands}
209 ip -6 rule add fwmark 0x2 lookup 100
210 ip -6 route add local ::/0 dev lo table 100
214 ${concatMapStringsSep "\n" ({table, command}: "iptables -w -t ${table} -D ${command}") iptablesCommands}
216 ip rule del fwmark 0x2 lookup 100
217 ip route del local 0.0.0.0/0 dev lo table 100
218 '' + optionalString config.networking.enableIPv6 ''
219 ${concatMapStringsSep "\n" ({table, command}: "ip6tables -w -t ${table} -D ${command}") ip6tablesCommands}
221 ip -6 rule del fwmark 0x2 lookup 100
222 ip -6 route del local ::/0 dev lo table 100