{ config, lib, pkgs, ... }: let cfg = config.services.gebos.caddy; tcpProxySites = lib.filterAttrs (_: s: s ? tcpProxy) cfg.sites; httpSites = lib.filterAttrs (_: s: !(s ? tcpProxy)) cfg.sites; needsLayer4 = tcpProxySites != { }; # External MQTTS port the senders connect to (mqtt_port = 8883, SSL). tlsListenPort = 8883; # Caddy built with the layer4 module (caddy-l4) — needed to TLS-terminate raw # MQTT and proxy it to HiveMQ, which Caddy's HTTP-only core can't do. Only # built when a host actually has a tcpProxy site; app-host's static/upstream # sites run on stock Caddy so they don't pay for the custom build. caddyWithL4 = pkgs.caddy.withPlugins { plugins = [ "github.com/mholt/caddy-l4@v0.1.1" ]; hash = "sha256-O6GuC2q1mA/Fa0utb2Yg7ZE73iq13oVYhJI1IVyOvog="; }; # HTTP-style sites: static file server or reverse proxy. httpSiteBlock = host: site: if site ? staticRoot then '' ${host} { root * ${site.staticRoot} file_server try_files {path} /index.html encode zstd gzip } '' else if site ? upstream then '' ${host} { reverse_proxy ${site.upstream} encode zstd gzip } '' else throw "site `${host}` needs one of: staticRoot, upstream, tcpProxy"; # A tcpProxy site needs TWO Caddyfile pieces: # 1. a layer4 listener (in the global options block) that terminates TLS on # :8883 and proxies cleartext to the upstream (HiveMQ on 127.0.0.1:1883); # 2. a normal HTTPS site block for the host, purely so Caddy's automatic # HTTPS OBTAINS the ACME cert that the layer4 `tls` handler then serves by # SNI — caddy-l4's `tls` handler only terminates, it never provisions # certs. This is why ports 80/443 must stay open for the ACME challenge. layer4Listener = _host: site: '' :${toString tlsListenPort} { route { tls proxy ${site.tcpProxy} } } ''; certSiteBlock = host: _site: '' ${host} { respond "gebos MQTT ingest — connect MQTTS on :${toString tlsListenPort}" 200 } ''; # Caddyfile global options block must come first and exist once; we fold every # layer4 listener into a single `layer4 { ... }` inside it. globalOptions = lib.optionalString needsLayer4 '' { layer4 { ${lib.concatStrings (lib.mapAttrsToList layer4Listener tcpProxySites)} } } ''; in { options.services.gebos.caddy = { enable = lib.mkEnableOption "Gebos Caddy reverse proxy / TLS terminator"; sites = lib.mkOption { type = lib.types.attrsOf lib.types.attrs; default = { }; }; }; config = lib.mkIf cfg.enable { services.caddy = { enable = true; package = if needsLayer4 then caddyWithL4 else pkgs.caddy; email = "ops@gebos.online"; # TODO: confirm ACME contact extraConfig = lib.concatStringsSep "\n" ( [ globalOptions ] ++ lib.mapAttrsToList httpSiteBlock httpSites ++ lib.mapAttrsToList certSiteBlock tcpProxySites ); }; }; }