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