1{
2 config,
3 lib,
4 pkgs,
5 ...
6}:
7
8with lib;
9
10let
11 cfg = config.services.sslh;
12 user = "sslh";
13
14 configFormat = pkgs.formats.libconfig { };
15 configFile = configFormat.generate "sslh.conf" cfg.settings;
16in
17
18{
19 imports = [
20 (mkRenamedOptionModule
21 [ "services" "sslh" "listenAddress" ]
22 [ "services" "sslh" "listenAddresses" ]
23 )
24 (mkRenamedOptionModule [ "services" "sslh" "timeout" ] [ "services" "sslh" "settings" "timeout" ])
25 (mkRenamedOptionModule
26 [ "services" "sslh" "transparent" ]
27 [ "services" "sslh" "settings" "transparent" ]
28 )
29 (mkRemovedOptionModule [ "services" "sslh" "appendConfig" ] "Use services.sslh.settings instead")
30 (mkChangedOptionModule
31 [ "services" "sslh" "verbose" ]
32 [ "services" "sslh" "settings" "verbose-connections" ]
33 (config: if config.services.sslh.verbose then 1 else 0)
34 )
35 ];
36
37 meta.buildDocsInSandbox = false;
38
39 options.services.sslh = {
40 enable = mkEnableOption "sslh, protocol demultiplexer";
41
42 method = mkOption {
43 type = types.enum [
44 "fork"
45 "select"
46 "ev"
47 ];
48 default = "fork";
49 description = ''
50 The method to use for handling connections:
51
52 - `fork` forks a new process for each incoming connection. It is
53 well-tested and very reliable, but incurs the overhead of many
54 processes.
55
56 - `select` uses only one thread, which monitors all connections at once.
57 It has lower overhead per connection, but if it stops, you'll lose all
58 connections.
59
60 - `ev` is implemented using libev, it's similar to `select` but
61 scales better to a large number of connections.
62 '';
63 };
64
65 listenAddresses = mkOption {
66 type = with types; coercedTo str singleton (listOf str);
67 default = [
68 "0.0.0.0"
69 "[::]"
70 ];
71 description = "Listening addresses or hostnames.";
72 };
73
74 port = mkOption {
75 type = types.port;
76 default = 443;
77 description = "Listening port.";
78 };
79
80 settings = mkOption {
81 type = types.submodule {
82 freeformType = configFormat.type;
83
84 options.timeout = mkOption {
85 type = types.ints.unsigned;
86 default = 2;
87 description = "Timeout in seconds.";
88 };
89
90 options.transparent = mkOption {
91 type = types.bool;
92 default = false;
93 description = ''
94 Whether the services behind sslh (Apache, sshd and so on) will see the
95 external IP and ports as if the external world connected directly to
96 them.
97 '';
98 };
99
100 options.verbose-connections = mkOption {
101 type = types.ints.between 0 4;
102 default = 0;
103 description = ''
104 Where to log connections information. Possible values are:
105
106 0. don't log anything
107 1. write log to stdout
108 2. write log to syslog
109 3. write log to both stdout and syslog
110 4. write to a log file ({option}`sslh.settings.logfile`)
111 '';
112 };
113
114 options.numeric = mkOption {
115 type = types.bool;
116 default = true;
117 description = ''
118 Whether to disable reverse DNS lookups, thus keeping IP
119 address literals in the log.
120 '';
121 };
122
123 options.protocols = mkOption {
124 type = types.listOf configFormat.type;
125 default = [
126 {
127 name = "ssh";
128 host = "localhost";
129 port = "22";
130 service = "ssh";
131 }
132 {
133 name = "openvpn";
134 host = "localhost";
135 port = "1194";
136 }
137 {
138 name = "xmpp";
139 host = "localhost";
140 port = "5222";
141 }
142 {
143 name = "http";
144 host = "localhost";
145 port = "80";
146 }
147 {
148 name = "tls";
149 host = "localhost";
150 port = "443";
151 }
152 {
153 name = "anyprot";
154 host = "localhost";
155 port = "443";
156 }
157 ];
158 description = ''
159 List of protocols sslh will probe for and redirect.
160 Each protocol entry consists of:
161
162 - `name`: name of the probe.
163
164 - `service`: libwrap service name (see {manpage}`hosts_access(5)`),
165
166 - `host`, `port`: where to connect when this probe succeeds,
167
168 - `log_level`: to log incoming connections,
169
170 - `transparent`: proxy this protocol transparently,
171
172 - etc.
173
174 See the documentation for all options, including probe-specific ones.
175 '';
176 };
177 };
178 description = "sslh configuration. See {manpage}`sslh(8)` for available settings.";
179 };
180 };
181
182 config = mkMerge [
183 (mkIf cfg.enable {
184 systemd.services.sslh = {
185 description = "Applicative Protocol Multiplexer (e.g. share SSH and HTTPS on the same port)";
186 after = [ "network.target" ];
187 wantedBy = [ "multi-user.target" ];
188
189 serviceConfig = {
190 DynamicUser = true;
191 User = "sslh";
192 PermissionsStartOnly = true;
193 Restart = "always";
194 RestartSec = "1s";
195 ExecStart = "${pkgs.sslh}/bin/sslh-${cfg.method} -F${configFile}";
196 KillMode = "process";
197 AmbientCapabilities = [
198 "CAP_NET_BIND_SERVICE"
199 "CAP_NET_ADMIN"
200 "CAP_SETGID"
201 "CAP_SETUID"
202 ];
203 PrivateTmp = true;
204 PrivateDevices = true;
205 ProtectSystem = "full";
206 ProtectHome = true;
207 };
208 };
209
210 services.sslh.settings = {
211 # Settings defined here are not supposed to be changed: doing so will
212 # break the module, as such you need `lib.mkForce` to override them.
213 foreground = true;
214 inetd = false;
215 listen = map (addr: {
216 host = addr;
217 port = toString cfg.port;
218 }) cfg.listenAddresses;
219 };
220
221 })
222
223 # code from https://github.com/yrutschle/sslh#transparent-proxy-support
224 # the only difference is using iptables mark 0x2 instead of 0x1 to avoid conflicts with nixos/nat module
225 (mkIf (cfg.enable && cfg.settings.transparent) {
226 # Set route_localnet = 1 on all interfaces so that ssl can use "localhost" as destination
227 boot.kernel.sysctl."net.ipv4.conf.default.route_localnet" = 1;
228 boot.kernel.sysctl."net.ipv4.conf.all.route_localnet" = 1;
229
230 systemd.services.sslh =
231 let
232 iptablesCommands = [
233 # DROP martian packets as they would have been if route_localnet was zero
234 # Note: packets not leaving the server aren't affected by this, thus sslh will still work
235 {
236 table = "raw";
237 command = "PREROUTING ! -i lo -d 127.0.0.0/8 -j DROP";
238 }
239 {
240 table = "mangle";
241 command = "POSTROUTING ! -o lo -s 127.0.0.0/8 -j DROP";
242 }
243 # Mark all connections made by ssl for special treatment (here sslh is run as user ${user})
244 {
245 table = "nat";
246 command = "OUTPUT -m owner --uid-owner ${user} -p tcp --tcp-flags FIN,SYN,RST,ACK SYN -j CONNMARK --set-xmark 0x02/0x0f";
247 }
248 # Outgoing packets that should go to sslh instead have to be rerouted, so mark them accordingly (copying over the connection mark)
249 {
250 table = "mangle";
251 command = "OUTPUT ! -o lo -p tcp -m connmark --mark 0x02/0x0f -j CONNMARK --restore-mark --mask 0x0f";
252 }
253 ];
254 ip6tablesCommands = [
255 {
256 table = "raw";
257 command = "PREROUTING ! -i lo -d ::1/128 -j DROP";
258 }
259 {
260 table = "mangle";
261 command = "POSTROUTING ! -o lo -s ::1/128 -j DROP";
262 }
263 {
264 table = "nat";
265 command = "OUTPUT -m owner --uid-owner ${user} -p tcp --tcp-flags FIN,SYN,RST,ACK SYN -j CONNMARK --set-xmark 0x02/0x0f";
266 }
267 {
268 table = "mangle";
269 command = "OUTPUT ! -o lo -p tcp -m connmark --mark 0x02/0x0f -j CONNMARK --restore-mark --mask 0x0f";
270 }
271 ];
272 in
273 {
274 path = [
275 pkgs.iptables
276 pkgs.iproute2
277 pkgs.procps
278 ];
279
280 preStart =
281 ''
282 # Cleanup old iptables entries which might be still there
283 ${concatMapStringsSep "\n" (
284 { table, command }: "while iptables -w -t ${table} -D ${command} 2>/dev/null; do echo; done"
285 ) iptablesCommands}
286 ${concatMapStringsSep "\n" (
287 { table, command }: "iptables -w -t ${table} -A ${command}"
288 ) iptablesCommands}
289
290 # Configure routing for those marked packets
291 ip rule add fwmark 0x2 lookup 100
292 ip route add local 0.0.0.0/0 dev lo table 100
293
294 ''
295 + optionalString config.networking.enableIPv6 ''
296 ${concatMapStringsSep "\n" (
297 { table, command }: "while ip6tables -w -t ${table} -D ${command} 2>/dev/null; do echo; done"
298 ) ip6tablesCommands}
299 ${concatMapStringsSep "\n" (
300 { table, command }: "ip6tables -w -t ${table} -A ${command}"
301 ) ip6tablesCommands}
302
303 ip -6 rule add fwmark 0x2 lookup 100
304 ip -6 route add local ::/0 dev lo table 100
305 '';
306
307 postStop =
308 ''
309 ${concatMapStringsSep "\n" (
310 { table, command }: "iptables -w -t ${table} -D ${command}"
311 ) iptablesCommands}
312
313 ip rule del fwmark 0x2 lookup 100
314 ip route del local 0.0.0.0/0 dev lo table 100
315 ''
316 + optionalString config.networking.enableIPv6 ''
317 ${concatMapStringsSep "\n" (
318 { table, command }: "ip6tables -w -t ${table} -D ${command}"
319 ) ip6tablesCommands}
320
321 ip -6 rule del fwmark 0x2 lookup 100
322 ip -6 route del local ::/0 dev lo table 100
323 '';
324 };
325 })
326 ];
327}