1{ config, lib, pkgs, ... }:
2let
3 inherit (lib)
4 attrValues
5 concatMap
6 concatStringsSep
7 escapeShellArg
8 literalExpression
9 mapAttrs'
10 mkDefault
11 mkEnableOption
12 mkIf
13 mkOption
14 nameValuePair
15 optional
16 types
17 ;
18
19 mainCfg = config.services.ghostunnel;
20
21 module = { config, name, ... }:
22 {
23 options = {
24
25 listen = mkOption {
26 description = lib.mdDoc ''
27 Address and port to listen on (can be HOST:PORT, unix:PATH).
28 '';
29 type = types.str;
30 };
31
32 target = mkOption {
33 description = lib.mdDoc ''
34 Address to forward connections to (can be HOST:PORT or unix:PATH).
35 '';
36 type = types.str;
37 };
38
39 keystore = mkOption {
40 description = lib.mdDoc ''
41 Path to keystore (combined PEM with cert/key, or PKCS12 keystore).
42
43 NB: storepass is not supported because it would expose credentials via `/proc/*/cmdline`.
44
45 Specify this or `cert` and `key`.
46 '';
47 type = types.nullOr types.str;
48 default = null;
49 };
50
51 cert = mkOption {
52 description = lib.mdDoc ''
53 Path to certificate (PEM with certificate chain).
54
55 Not required if `keystore` is set.
56 '';
57 type = types.nullOr types.str;
58 default = null;
59 };
60
61 key = mkOption {
62 description = lib.mdDoc ''
63 Path to certificate private key (PEM with private key).
64
65 Not required if `keystore` is set.
66 '';
67 type = types.nullOr types.str;
68 default = null;
69 };
70
71 cacert = mkOption {
72 description = lib.mdDoc ''
73 Path to CA bundle file (PEM/X509). Uses system trust store if `null`.
74 '';
75 type = types.nullOr types.str;
76 };
77
78 disableAuthentication = mkOption {
79 description = lib.mdDoc ''
80 Disable client authentication, no client certificate will be required.
81 '';
82 type = types.bool;
83 default = false;
84 };
85
86 allowAll = mkOption {
87 description = lib.mdDoc ''
88 If true, allow all clients, do not check client cert subject.
89 '';
90 type = types.bool;
91 default = false;
92 };
93
94 allowCN = mkOption {
95 description = lib.mdDoc ''
96 Allow client if common name appears in the list.
97 '';
98 type = types.listOf types.str;
99 default = [];
100 };
101
102 allowOU = mkOption {
103 description = lib.mdDoc ''
104 Allow client if organizational unit name appears in the list.
105 '';
106 type = types.listOf types.str;
107 default = [];
108 };
109
110 allowDNS = mkOption {
111 description = lib.mdDoc ''
112 Allow client if DNS subject alternative name appears in the list.
113 '';
114 type = types.listOf types.str;
115 default = [];
116 };
117
118 allowURI = mkOption {
119 description = lib.mdDoc ''
120 Allow client if URI subject alternative name appears in the list.
121 '';
122 type = types.listOf types.str;
123 default = [];
124 };
125
126 extraArguments = mkOption {
127 description = lib.mdDoc "Extra arguments to pass to `ghostunnel server`";
128 type = types.separatedString " ";
129 default = "";
130 };
131
132 unsafeTarget = mkOption {
133 description = lib.mdDoc ''
134 If set, does not limit target to localhost, 127.0.0.1, [::1], or UNIX sockets.
135
136 This is meant to protect against accidental unencrypted traffic on
137 untrusted networks.
138 '';
139 type = types.bool;
140 default = false;
141 };
142
143 # Definitions to apply at the root of the NixOS configuration.
144 atRoot = mkOption {
145 internal = true;
146 };
147 };
148
149 # Clients should not be authenticated with the public root certificates
150 # (afaict, it doesn't make sense), so we only provide that default when
151 # client cert auth is disabled.
152 config.cacert = mkIf config.disableAuthentication (mkDefault null);
153
154 config.atRoot = {
155 assertions = [
156 { message = ''
157 services.ghostunnel.servers.${name}: At least one access control flag is required.
158 Set at least one of:
159 - services.ghostunnel.servers.${name}.disableAuthentication
160 - services.ghostunnel.servers.${name}.allowAll
161 - services.ghostunnel.servers.${name}.allowCN
162 - services.ghostunnel.servers.${name}.allowOU
163 - services.ghostunnel.servers.${name}.allowDNS
164 - services.ghostunnel.servers.${name}.allowURI
165 '';
166 assertion = config.disableAuthentication
167 || config.allowAll
168 || config.allowCN != []
169 || config.allowOU != []
170 || config.allowDNS != []
171 || config.allowURI != []
172 ;
173 }
174 ];
175
176 systemd.services."ghostunnel-server-${name}" = {
177 after = [ "network.target" ];
178 wants = [ "network.target" ];
179 wantedBy = [ "multi-user.target" ];
180 serviceConfig = {
181 Restart = "always";
182 AmbientCapabilities = ["CAP_NET_BIND_SERVICE"];
183 DynamicUser = true;
184 LoadCredential = optional (config.keystore != null) "keystore:${config.keystore}"
185 ++ optional (config.cert != null) "cert:${config.cert}"
186 ++ optional (config.key != null) "key:${config.key}"
187 ++ optional (config.cacert != null) "cacert:${config.cacert}";
188 };
189 script = concatStringsSep " " (
190 [ "${mainCfg.package}/bin/ghostunnel" ]
191 ++ optional (config.keystore != null) "--keystore=$CREDENTIALS_DIRECTORY/keystore"
192 ++ optional (config.cert != null) "--cert=$CREDENTIALS_DIRECTORY/cert"
193 ++ optional (config.key != null) "--key=$CREDENTIALS_DIRECTORY/key"
194 ++ optional (config.cacert != null) "--cacert=$CREDENTIALS_DIRECTORY/cacert"
195 ++ [
196 "server"
197 "--listen ${config.listen}"
198 "--target ${config.target}"
199 ] ++ optional config.allowAll "--allow-all"
200 ++ map (v: "--allow-cn=${escapeShellArg v}") config.allowCN
201 ++ map (v: "--allow-ou=${escapeShellArg v}") config.allowOU
202 ++ map (v: "--allow-dns=${escapeShellArg v}") config.allowDNS
203 ++ map (v: "--allow-uri=${escapeShellArg v}") config.allowURI
204 ++ optional config.disableAuthentication "--disable-authentication"
205 ++ optional config.unsafeTarget "--unsafe-target"
206 ++ [ config.extraArguments ]
207 );
208 };
209 };
210 };
211
212in
213{
214
215 options = {
216 services.ghostunnel.enable = mkEnableOption (lib.mdDoc "ghostunnel");
217
218 services.ghostunnel.package = mkOption {
219 description = lib.mdDoc "The ghostunnel package to use.";
220 type = types.package;
221 default = pkgs.ghostunnel;
222 defaultText = literalExpression "pkgs.ghostunnel";
223 };
224
225 services.ghostunnel.servers = mkOption {
226 description = lib.mdDoc ''
227 Server mode ghostunnels (TLS listener -> plain TCP/UNIX target)
228 '';
229 type = types.attrsOf (types.submodule module);
230 default = {};
231 };
232 };
233
234 config = mkIf mainCfg.enable {
235 assertions = lib.mkMerge (map (v: v.atRoot.assertions) (attrValues mainCfg.servers));
236 systemd = lib.mkMerge (map (v: v.atRoot.systemd) (attrValues mainCfg.servers));
237 };
238
239 meta.maintainers = with lib.maintainers; [
240 roberth
241 ];
242}