{ 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:8443 (nginx's own https listener: # TLS terminated by nginx, then path-routed: # "/" → panel web server 2049, "/subs/…" etc → # panel subscription server 2096, so subscription # links can be plain https://x.zeroq.su/subs/) # immich/git/... → 127.0.0.1:8443 (nginx's own http {} listener) # 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 # (via the web listener so nginx can split /subs/ off the panel paths), # 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 → nginx's own https listener (8443), where nginx path-routes # /subs/... → panel subscription server 2096 and / → panel web 2049; # known vhosts → web 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} web; ${"\n" + lib.concatMapStringsSep "\n" (d: " ${d} web;") (extraWebDomains ++ (map (s: s.domain) sites))} } 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" = { # Panel domain. TLS is terminated HERE by nginx (stream routes this # SNI to the web listener), and paths are split: # /subs/, /subsjs/, /clash/, /sub/ → panel subscription server # (127.0.0.1:2096, TLS inside the container) so links like # https://x.zeroq.su/subs/ work without :2096; # everything else → panel web server # (127.0.0.1:2049, TLS inside the container). # The panel serves both on TLS with the mounted LE cert. enableACME = true; forceSSL = true; listen = webListen; locations = { "/subs/" = { proxyPass = "https://127.0.0.1:2096"; proxyWebsockets = true; extraConfig = "proxy_ssl_verify off;"; }; "/subsjs/" = { proxyPass = "https://127.0.0.1:2096"; extraConfig = "proxy_ssl_verify off;"; }; "/clash/" = { proxyPass = "https://127.0.0.1:2096"; extraConfig = "proxy_ssl_verify off;"; }; "/sub/" = { proxyPass = "https://127.0.0.1:2096"; extraConfig = "proxy_ssl_verify off;"; }; "/" = { proxyPass = "https://127.0.0.1:2049"; proxyWebsockets = true; extraConfig = "proxy_ssl_verify off;"; }; }; }; "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 ]; }