Merge #361424: refactor lib.packagesFromDirectoryRecursive (v2)
[NixPkgs.git] / nixos / modules / services / networking / cloudflared.nix
blob12eaf35e5fdff2b506aee3a2f2e27041b4273fa6
2   config,
3   lib,
4   pkgs,
5   ...
6 }:
7 let
8   cfg = config.services.cloudflared;
10   originRequest = {
11     connectTimeout = lib.mkOption {
12       type = with lib.types; nullOr str;
13       default = null;
14       example = "30s";
15       description = ''
16         Timeout for establishing a new TCP connection to your origin server. This excludes the time taken to establish TLS, which is controlled by [tlsTimeout](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/configuration/local-management/ingress/#tlstimeout).
17       '';
18     };
20     tlsTimeout = lib.mkOption {
21       type = with lib.types; nullOr str;
22       default = null;
23       example = "10s";
24       description = ''
25         Timeout for completing a TLS handshake to your origin server, if you have chosen to connect Tunnel to an HTTPS server.
26       '';
27     };
29     tcpKeepAlive = lib.mkOption {
30       type = with lib.types; nullOr str;
31       default = null;
32       example = "30s";
33       description = ''
34         The timeout after which a TCP keepalive packet is sent on a connection between Tunnel and the origin server.
35       '';
36     };
38     noHappyEyeballs = lib.mkOption {
39       type = with lib.types; nullOr bool;
40       default = null;
41       example = false;
42       description = ''
43         Disable the “happy eyeballs” algorithm for IPv4/IPv6 fallback if your local network has misconfigured one of the protocols.
44       '';
45     };
47     keepAliveConnections = lib.mkOption {
48       type = with lib.types; nullOr int;
49       default = null;
50       example = 100;
51       description = ''
52         Maximum number of idle keepalive connections between Tunnel and your origin. This does not restrict the total number of concurrent connections.
53       '';
54     };
56     keepAliveTimeout = lib.mkOption {
57       type = with lib.types; nullOr str;
58       default = null;
59       example = "1m30s";
60       description = ''
61         Timeout after which an idle keepalive connection can be discarded.
62       '';
63     };
65     httpHostHeader = lib.mkOption {
66       type = with lib.types; nullOr str;
67       default = null;
68       example = "";
69       description = ''
70         Sets the HTTP `Host` header on requests sent to the local service.
71       '';
72     };
74     originServerName = lib.mkOption {
75       type = with lib.types; nullOr str;
76       default = null;
77       example = "";
78       description = ''
79         Hostname that `cloudflared` should expect from your origin server certificate.
80       '';
81     };
83     caPool = lib.mkOption {
84       type = with lib.types; nullOr (either str path);
85       default = null;
86       example = "";
87       description = ''
88         Path to the certificate authority (CA) for the certificate of your origin. This option should be used only if your certificate is not signed by Cloudflare.
89       '';
90     };
92     noTLSVerify = lib.mkOption {
93       type = with lib.types; nullOr bool;
94       default = null;
95       example = false;
96       description = ''
97         Disables TLS verification of the certificate presented by your origin. Will allow any certificate from the origin to be accepted.
98       '';
99     };
101     disableChunkedEncoding = lib.mkOption {
102       type = with lib.types; nullOr bool;
103       default = null;
104       example = false;
105       description = ''
106         Disables chunked transfer encoding. Useful if you are running a WSGI server.
107       '';
108     };
110     proxyAddress = lib.mkOption {
111       type = with lib.types; nullOr str;
112       default = null;
113       example = "127.0.0.1";
114       description = ''
115         `cloudflared` starts a proxy server to translate HTTP traffic into TCP when proxying, for example, SSH or RDP. This configures the listen address for that proxy.
116       '';
117     };
119     proxyPort = lib.mkOption {
120       type = with lib.types; nullOr int;
121       default = null;
122       example = 0;
123       description = ''
124         `cloudflared` starts a proxy server to translate HTTP traffic into TCP when proxying, for example, SSH or RDP. This configures the listen port for that proxy. If set to zero, an unused port will randomly be chosen.
125       '';
126     };
128     proxyType = lib.mkOption {
129       type =
130         with lib.types;
131         nullOr (enum [
132           ""
133           "socks"
134         ]);
135       default = null;
136       example = "";
137       description = ''
138         `cloudflared` starts a proxy server to translate HTTP traffic into TCP when proxying, for example, SSH or RDP. This configures what type of proxy will be started. Valid options are:
140         - `""` for the regular proxy
141         - `"socks"` for a SOCKS5 proxy. Refer to the [tutorial on connecting through Cloudflare Access using kubectl](https://developers.cloudflare.com/cloudflare-one/tutorials/kubectl/) for more information.
142       '';
143     };
144   };
147   options.services.cloudflared = {
148     enable = lib.mkEnableOption "Cloudflare Tunnel client daemon (formerly Argo Tunnel)";
150     user = lib.mkOption {
151       type = lib.types.str;
152       default = "cloudflared";
153       description = "User account under which Cloudflared runs.";
154     };
156     group = lib.mkOption {
157       type = lib.types.str;
158       default = "cloudflared";
159       description = "Group under which cloudflared runs.";
160     };
162     package = lib.mkPackageOption pkgs "cloudflared" { };
164     tunnels = lib.mkOption {
165       description = ''
166         Cloudflare tunnels.
167       '';
168       type = lib.types.attrsOf (
169         lib.types.submodule (
170           { name, ... }:
171           {
172             options = {
173               inherit originRequest;
175               credentialsFile = lib.mkOption {
176                 type = lib.types.str;
177                 description = ''
178                   Credential file.
180                   See [Credentials file](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/install-and-setup/tunnel-useful-terms/#credentials-file).
181                 '';
182               };
184               warp-routing = {
185                 enabled = lib.mkOption {
186                   type = with lib.types; nullOr bool;
187                   default = null;
188                   description = ''
189                     Enable warp routing.
191                     See [Connect from WARP to a private network on Cloudflare using Cloudflare Tunnel](https://developers.cloudflare.com/cloudflare-one/tutorials/warp-to-tunnel/).
192                   '';
193                 };
194               };
196               default = lib.mkOption {
197                 type = lib.types.str;
198                 description = ''
199                   Catch-all service if no ingress matches.
201                   See `service`.
202                 '';
203                 example = "http_status:404";
204               };
206               ingress = lib.mkOption {
207                 type =
208                   with lib.types;
209                   attrsOf (
210                     either str (
211                       submodule (
212                         { hostname, ... }:
213                         {
214                           options = {
215                             inherit originRequest;
217                             service = lib.mkOption {
218                               type = with lib.types; nullOr str;
219                               default = null;
220                               description = ''
221                                 Service to pass the traffic.
223                                 See [Supported protocols](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/configuration/local-management/ingress/#supported-protocols).
224                               '';
225                               example = "http://localhost:80, tcp://localhost:8000, unix:/home/production/echo.sock, hello_world or http_status:404";
226                             };
228                             path = lib.mkOption {
229                               type = with lib.types; nullOr str;
230                               default = null;
231                               description = ''
232                                 Path filter.
234                                 If not specified, all paths will be matched.
235                               '';
236                               example = "/*.(jpg|png|css|js)";
237                             };
239                           };
240                         }
241                       )
242                     )
243                   );
244                 default = { };
245                 description = ''
246                   Ingress rules.
248                   See [Ingress rules](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/configuration/local-management/ingress/).
249                 '';
250                 example = {
251                   "*.domain.com" = "http://localhost:80";
252                   "*.anotherone.com" = "http://localhost:80";
253                 };
254               };
255             };
256           }
257         )
258       );
260       default = { };
261       example = {
262         "00000000-0000-0000-0000-000000000000" = {
263           credentialsFile = "/tmp/test";
264           ingress = {
265             "*.domain1.com" = {
266               service = "http://localhost:80";
267             };
268           };
269           default = "http_status:404";
270         };
271       };
272     };
273   };
275   config = lib.mkIf cfg.enable {
276     systemd.targets = lib.mapAttrs' (
277       name: tunnel:
278       lib.nameValuePair "cloudflared-tunnel-${name}" {
279         description = "Cloudflare tunnel '${name}' target";
280         requires = [ "cloudflared-tunnel-${name}.service" ];
281         after = [ "cloudflared-tunnel-${name}.service" ];
282         unitConfig.StopWhenUnneeded = true;
283       }
284     ) config.services.cloudflared.tunnels;
286     systemd.services = lib.mapAttrs' (
287       name: tunnel:
288       let
289         filterConfig = lib.attrsets.filterAttrsRecursive (
290           _: v:
291           !builtins.elem v [
292             null
293             [ ]
294             { }
295           ]
296         );
298         filterIngressSet = lib.filterAttrs (_: v: builtins.typeOf v == "set");
299         filterIngressStr = lib.filterAttrs (_: v: builtins.typeOf v == "string");
301         ingressesSet = filterIngressSet tunnel.ingress;
302         ingressesStr = filterIngressStr tunnel.ingress;
304         fullConfig = filterConfig {
305           tunnel = name;
306           "credentials-file" = tunnel.credentialsFile;
307           warp-routing = filterConfig tunnel.warp-routing;
308           originRequest = filterConfig tunnel.originRequest;
309           ingress =
310             (map (
311               key:
312               {
313                 hostname = key;
314               }
315               // lib.getAttr key (filterConfig (filterConfig ingressesSet))
316             ) (lib.attrNames ingressesSet))
317             ++ (map (key: {
318               hostname = key;
319               service = lib.getAttr key ingressesStr;
320             }) (lib.attrNames ingressesStr))
321             ++ [ { service = tunnel.default; } ];
322         };
324         mkConfigFile = pkgs.writeText "cloudflared.yml" (builtins.toJSON fullConfig);
325       in
326       lib.nameValuePair "cloudflared-tunnel-${name}" ({
327         after = [
328           "network.target"
329           "network-online.target"
330         ];
331         wants = [
332           "network.target"
333           "network-online.target"
334         ];
335         wantedBy = [ "multi-user.target" ];
336         serviceConfig = {
337           User = cfg.user;
338           Group = cfg.group;
339           ExecStart = "${cfg.package}/bin/cloudflared tunnel --config=${mkConfigFile} --no-autoupdate run";
340           Restart = "on-failure";
341         };
342       })
343     ) config.services.cloudflared.tunnels;
345     users.users = lib.mkIf (cfg.user == "cloudflared") {
346       cloudflared = {
347         group = cfg.group;
348         isSystemUser = true;
349       };
350     };
352     users.groups = lib.mkIf (cfg.group == "cloudflared") {
353       cloudflared = { };
354     };
355   };
357   meta.maintainers = with lib.maintainers; [
358     bbigras
359     anpin
360   ];