1{ config, lib, pkgs, ... }:
2
3with lib;
4
5let
6 cfg = config.services.sslh;
7 user = "sslh";
8 configFile = pkgs.writeText "sslh.conf" ''
9 verbose: ${boolToString cfg.verbose};
10 foreground: true;
11 inetd: false;
12 numeric: false;
13 transparent: ${boolToString cfg.transparent};
14 timeout: "${toString cfg.timeout}";
15
16 listen:
17 (
18 { host: "${cfg.listenAddress}"; port: "${toString cfg.port}"; }
19 );
20
21 ${cfg.appendConfig}
22 '';
23 defaultAppendConfig = ''
24 protocols:
25 (
26 { name: "ssh"; service: "ssh"; host: "localhost"; port: "22"; probe: "builtin"; },
27 { name: "openvpn"; host: "localhost"; port: "1194"; probe: "builtin"; },
28 { name: "xmpp"; host: "localhost"; port: "5222"; probe: "builtin"; },
29 { name: "http"; host: "localhost"; port: "80"; probe: "builtin"; },
30 { name: "ssl"; host: "localhost"; port: "443"; probe: "builtin"; },
31 { name: "anyprot"; host: "localhost"; port: "443"; probe: "builtin"; }
32 );
33 '';
34in
35{
36 options = {
37 services.sslh = {
38 enable = mkEnableOption "sslh";
39
40 verbose = mkOption {
41 type = types.bool;
42 default = false;
43 description = "Verbose logs.";
44 };
45
46 timeout = mkOption {
47 type = types.int;
48 default = 2;
49 description = "Timeout in seconds.";
50 };
51
52 transparent = mkOption {
53 type = types.bool;
54 default = false;
55 description = "Will the services behind sslh (Apache, sshd and so on) see the external IP and ports as if the external world connected directly to them";
56 };
57
58 listenAddress = mkOption {
59 type = types.str;
60 default = "0.0.0.0";
61 description = "Listening address or hostname.";
62 };
63
64 port = mkOption {
65 type = types.int;
66 default = 443;
67 description = "Listening port.";
68 };
69
70 appendConfig = mkOption {
71 type = types.str;
72 default = defaultAppendConfig;
73 description = "Verbatim configuration file.";
74 };
75 };
76 };
77
78 config = mkMerge [
79 (mkIf cfg.enable {
80 users.users.${user} = {
81 description = "sslh daemon user";
82 isSystemUser = true;
83 };
84
85 systemd.services.sslh = {
86 description = "Applicative Protocol Multiplexer (e.g. share SSH and HTTPS on the same port)";
87 after = [ "network.target" ];
88 wantedBy = [ "multi-user.target" ];
89
90 serviceConfig = {
91 User = user;
92 Group = "nogroup";
93 PermissionsStartOnly = true;
94 Restart = "always";
95 RestartSec = "1s";
96 ExecStart = "${pkgs.sslh}/bin/sslh -F${configFile}";
97 KillMode = "process";
98 AmbientCapabilities = "CAP_NET_BIND_SERVICE CAP_NET_ADMIN CAP_SETGID CAP_SETUID";
99 PrivateTmp = true;
100 PrivateDevices = true;
101 ProtectSystem = "full";
102 ProtectHome = true;
103 };
104 };
105 })
106
107 # code from https://github.com/yrutschle/sslh#transparent-proxy-support
108 # the only difference is using iptables mark 0x2 instead of 0x1 to avoid conflicts with nixos/nat module
109 (mkIf (cfg.enable && cfg.transparent) {
110 # Set route_localnet = 1 on all interfaces so that ssl can use "localhost" as destination
111 boot.kernel.sysctl."net.ipv4.conf.default.route_localnet" = 1;
112 boot.kernel.sysctl."net.ipv4.conf.all.route_localnet" = 1;
113
114 systemd.services.sslh = let
115 iptablesCommands = [
116 # DROP martian packets as they would have been if route_localnet was zero
117 # Note: packets not leaving the server aren't affected by this, thus sslh will still work
118 { table = "raw"; command = "PREROUTING ! -i lo -d 127.0.0.0/8 -j DROP"; }
119 { table = "mangle"; command = "POSTROUTING ! -o lo -s 127.0.0.0/8 -j DROP"; }
120 # Mark all connections made by ssl for special treatment (here sslh is run as user ${user})
121 { table = "nat"; command = "OUTPUT -m owner --uid-owner ${user} -p tcp --tcp-flags FIN,SYN,RST,ACK SYN -j CONNMARK --set-xmark 0x02/0x0f"; }
122 # Outgoing packets that should go to sslh instead have to be rerouted, so mark them accordingly (copying over the connection mark)
123 { table = "mangle"; command = "OUTPUT ! -o lo -p tcp -m connmark --mark 0x02/0x0f -j CONNMARK --restore-mark --mask 0x0f"; }
124 ];
125 ip6tablesCommands = [
126 { table = "raw"; command = "PREROUTING ! -i lo -d ::1/128 -j DROP"; }
127 { table = "mangle"; command = "POSTROUTING ! -o lo -s ::1/128 -j DROP"; }
128 { table = "nat"; command = "OUTPUT -m owner --uid-owner ${user} -p tcp --tcp-flags FIN,SYN,RST,ACK SYN -j CONNMARK --set-xmark 0x02/0x0f"; }
129 { table = "mangle"; command = "OUTPUT ! -o lo -p tcp -m connmark --mark 0x02/0x0f -j CONNMARK --restore-mark --mask 0x0f"; }
130 ];
131 in {
132 path = [ pkgs.iptables pkgs.iproute pkgs.procps ];
133
134 preStart = ''
135 # Cleanup old iptables entries which might be still there
136 ${concatMapStringsSep "\n" ({table, command}: "while iptables -w -t ${table} -D ${command} 2>/dev/null; do echo; done") iptablesCommands}
137 ${concatMapStringsSep "\n" ({table, command}: "iptables -w -t ${table} -A ${command}" ) iptablesCommands}
138
139 # Configure routing for those marked packets
140 ip rule add fwmark 0x2 lookup 100
141 ip route add local 0.0.0.0/0 dev lo table 100
142
143 '' + optionalString config.networking.enableIPv6 ''
144 ${concatMapStringsSep "\n" ({table, command}: "while ip6tables -w -t ${table} -D ${command} 2>/dev/null; do echo; done") ip6tablesCommands}
145 ${concatMapStringsSep "\n" ({table, command}: "ip6tables -w -t ${table} -A ${command}" ) ip6tablesCommands}
146
147 ip -6 rule add fwmark 0x2 lookup 100
148 ip -6 route add local ::/0 dev lo table 100
149 '';
150
151 postStop = ''
152 ${concatMapStringsSep "\n" ({table, command}: "iptables -w -t ${table} -D ${command}") iptablesCommands}
153
154 ip rule del fwmark 0x2 lookup 100
155 ip route del local 0.0.0.0/0 dev lo table 100
156 '' + optionalString config.networking.enableIPv6 ''
157 ${concatMapStringsSep "\n" ({table, command}: "ip6tables -w -t ${table} -D ${command}") ip6tablesCommands}
158
159 ip -6 rule del fwmark 0x2 lookup 100
160 ip -6 route del local ::/0 dev lo table 100
161 '';
162 };
163 })
164 ];
165}