{ config, lib, ... }: with lib; let cfg = config.modules.expose; in { options.modules.expose = { enable = mkEnableOption "expose services to network"; routes = mkOption { type = types.attrsOf (types.submodule { options = { to = mkOption { type = types.str; example = "https://localhost:3000"; description = "Destination address"; }; ts = mkOption { type = types.bool; default = true; description = "Whether to enable nginx & blocky for Tailscale routing"; }; cf = mkOption { type = types.bool; default = true; description = "Whether to enable Cloudflare Tunnel for public routing"; }; configureNginx = mkOption { type = types.bool; default = true; description = "Whether to configure nginx virtualHosts for this route"; }; }; }); }; ssl = { enable = mkEnableOption "SSL on all routes"; acmeHost = mkOption { type = types.nullOr types.str; default = null; description = "ACMEHost for the certificate"; }; }; webmasterEmail = mkOption { type = types.str; description = "Email of the webmaster to be contacted for ACME events"; }; tailscaleIp = mkOption { type = types.str; description = "Tailscale IP of this node"; }; cloudflareUUID = mkOption { type = types.str; description = "UUID of the Cloudflare Tunnel"; }; secrets = { acme-credentials = mkOption { type = types.path; description = "Path to the acme environment file"; }; cloudflare-credentials = mkOption { type = types.path; description = "Path to the cloudflare tunnel credentials"; }; }; }; config = mkIf cfg.enable { assertions = [ { assertion = cfg.ssl.enable -> cfg.ssl.acmeHost != null; message = "ssl.acmeHost must be set when enabling SSL"; } ]; services.nginx.virtualHosts = mapAttrs (_: v: { locations."/".proxyPass = v.to; } // optionalAttrs (cfg.ssl.enable) { forceSSL = true; useACMEHost = cfg.ssl.acmeHost; }) (filterAttrs (_: v: v.configureNginx) cfg.routes); # Discard non-localhost mappings, and replace destination with tailscale IP # Map localhost routes to tailscaleIp services.blocky.settings.customDNS.mapping = mapAttrs (_: v: if (hasInfix "localhost" v.to) then cfg.tailscaleIp else v.to) (filterAttrs (_: v: v.ts) cfg.routes); services.cloudflared.tunnels."${cfg.cloudflareUUID}" = { credentialsFile = cfg.secrets.cloudflare-credentials; ingress = mapAttrs (_: v: v.to) (filterAttrs (_: v: v.cf) cfg.routes); } // optionalAttrs (cfg.ssl.enable) { # TODO: This seems to have no effect. Remove? originRequest.originServerName = "*.${cfg.ssl.acmeHost}"; # originRequest.caPool = config.security.acme.certs.${cfg.ssl.acmeHost}.directory; }; }; }