{ config, lib, pkgs, xlib, ... }: # SNI-fronted reverse proxy (vds/nginx.nix style): port 443 is owned by an # nginx stream block that reads the ClientHello SNI and forwards the raw # TLS stream. The HTTP vhosts no longer bind 443 — they listen on an # internal HTTPS listener (127.0.0.1:8443) that the stream forwards onto, # so all existing services behave exactly as before. # # Routing: # x.zeroq.su → 127.0.0.1:2049 (3x-ui controller panel; TLS is # terminated inside the container by the mounted LE # cert, exactly like pubray1.zeroq.su on the VDS) # immich/git/... → 127.0.0.1:8443 (nginx's own http {} listener: # TLS termination + proxyPass unchanged) # default → 127.0.0.1:15380 (Xray REALITY; podman DNATs # host:15380 → container:443 so Xray sees its real # configured port 443) # # ssl_preread reads SNI from the ClientHello; every SNI matching a known # vhost goes to the internal web listener, x.zeroq.su goes to the panel, # and anything else (REALITY fronting domains, direct-IP) goes to Xray. let server = "192.168.1.20"; panelDomain = "x.zeroq.su"; # Replaces the default 443 binding for every public vhost: 443 now # belongs to the stream block, the internal https listener hosts the # real server blocks. Port 80 stays public for ACME http-01 + redirects. webListen = [ { addr = "0.0.0.0"; port = 80; } { addr = "127.0.0.1"; port = 8443; ssl = true; } ]; mkProxy = { domain, port, addSSL ? false, extraConfig ? "", }: { name = domain; value = { enableACME = true; listen = webListen; locations."/" = { proxyPass = "http://${server}:${toString port}"; proxyWebsockets = true; }; } // lib.optionalAttrs (!addSSL) { forceSSL = true; } // lib.optionalAttrs addSSL { addSSL = true; } // lib.optionalAttrs (extraConfig != "") { inherit extraConfig; }; }; bigUploads = "client_max_body_size 5G;"; sites = [ { domain = "immich.zeroq.su"; port = 2283; addSSL = true; extraConfig = bigUploads; } { domain = "kuma.zeroq.su"; port = 4001; } { domain = "health.zeroq.su"; port = 19999; } { domain = "git.zeroq.su"; port = 3000; } { domain = "homebox.zeroq.su"; port = 7745; } { domain = "flux.zeroq.su"; port = 6061; } { domain = "navidrome.zeroq.su"; port = 4533; addSSL = true; } { domain = "calibre.zeroq.su"; port = 8083; extraConfig = bigUploads; } { domain = "nix-cache.zeroq.su"; port = 5000; extraConfig = bigUploads; } { domain = "pdf.zeroq.su"; port = 8446; extraConfig = bigUploads; } ]; # Hardcoded vhosts served by the internal web listener (next to `sites`). # Every one of them must appear in the SNI map → web. extraWebDomains = [ "office.zeroq.su" "zeroq.su" "vetymae.opencodes.zeroq.su" "lamet.opencodes.zeroq.su" "nextcloud.zeroq.su" ]; # Goes inside the auto-generated `stream {}` block (services.nginx.streamConfig). # x.zeroq.su → 3x-ui panel; known vhosts → nginx's own https listener; # everything else (any SNI a REALITY client uses, e.g. # media.mediavitrina.ru, or direct-IP) → Xray. streamConfig = '' ssl_preread on; map $ssl_preread_server_name $sni_backend { default xray; ${panelDomain} panel; ${"\n" + lib.concatMapStringsSep "\n" (d: " ${d} web;") (extraWebDomains ++ (map (s: s.domain) sites))} } upstream panel { server 127.0.0.1:2049; } upstream web { server 127.0.0.1:8443; } upstream xray { server 127.0.0.1:15380; } server { listen 443; proxy_pass $sni_backend; proxy_timeout 600s; proxy_connect_timeout 5s; } ''; in { services.nginx = { enable = true; recommendedGzipSettings = true; recommendedOptimisation = true; recommendedProxySettings = true; recommendedTlsSettings = true; # Lands inside the auto-generated `stream {}` block. streamConfig = streamConfig; virtualHosts = (builtins.listToAttrs (map mkProxy sites)) // { "nextcloud.private" = { forceSSL = false; enableACME = false; listen = [ { addr = "100.64.0.0"; port = 10000; } { addr = "192.168.1.20"; port = 10000; } { addr = "127.0.0.1"; port = 10000; } ]; }; "office.zeroq.su" = { forceSSL = true; enableACME = true; listen = webListen; }; "pdf.private" = { forceSSL = false; enableACME = false; listen = [ { addr = "0.0.0.0"; port = 80; } { addr = "100.64.0.0"; port = 8446; } { addr = "192.168.1.20"; port = 8446; } { addr = "127.0.0.1"; port = 8446; } ]; extraConfig = bigUploads; }; "x.zeroq.su" = { # ACME only — no https listener here: 443 is owned by the stream # block, which routes the x.zeroq.su SNI to the panel inside the # container (127.0.0.1:2049). 3x-ui terminates TLS itself using # the cert this vhost renews (mounted at /root/cert/), and serves # /subs/, /subsjs/, /clash/ straight from the panel, like the VDS. # Don't add forceSSL: it would generate an HTTPS server block that # conflicts with the stream listener. enableACME = true; }; "zeroq.su" = { forceSSL = true; enableACME = true; listen = webListen; root = pkgs.writeTextDir "index.html" ''
What are you doing here?''; locations."/guest/" = { proxyPass = "http://${server}:80"; proxyWebsockets = true; }; }; "vetymae.opencodes.zeroq.su" = { forceSSL = true; enableACME = true; listen = webListen; locations."/" = { proxyPass = "http://100.86.62.4:4096"; proxyWebsockets = true; }; }; "lamet.opencodes.zeroq.su" = { forceSSL = true; enableACME = true; listen = webListen; locations."/" = { proxyPass = "http://100.106.21.39:6061"; proxyWebsockets = true; }; }; "nextcloud.zeroq.su" = { forceSSL = true; enableACME = true; listen = webListen; locations = { "/" = { proxyPass = "http://${server}:10000"; proxyWebsockets = true; }; "/whiteboard" = { proxyPass = "http://${server}:3002"; proxyWebsockets = true; }; }; extraConfig = bigUploads; }; }; }; networking.firewall.allowedTCPPorts = [ 80 443 ]; }