diff --git a/modules/containers/3x-ui.nix b/modules/containers/3x-ui.nix index 2cba403..3891f9c 100644 --- a/modules/containers/3x-ui.nix +++ b/modules/containers/3x-ui.nix @@ -10,26 +10,27 @@ let certDomain = xlib.services."3x-ui".certDomain or null; certMounts = if certDomain == null then [ ] - else [ - # Let's Encrypt cert for the panel domain — mounted read-only so - # 3x-ui can serve the panel over its own TLS. webCertFile / - # webKeyFile in the x-ui settings table must point at - # /root/cert/fullchain.pem and /root/cert/key.pem respectively. - "/var/lib/acme/${certDomain}/fullchain.pem:/root/cert/fullchain.pem:ro" - "/var/lib/acme/${certDomain}/key.pem:/root/cert/key.pem:ro" - ]; + else + # LE cert mounted read-only so 3x-ui can terminate TLS itself. + # The 3x-ui settings table must point webCertFile / webKeyFile at + # /root/cert/fullchain.pem and /root/cert/key.pem. + map (f: "/var/lib/acme/${certDomain}/${f}:/root/cert/${f}:ro") [ + "fullchain.pem" + "key.pem" + ]; basePorts = [ + # 2049/tcp — 3x-ui web panel + # 2096/tcp — subscription endpoint + # 14380-15379/tcp+udp — Xray inbounds (matches firewall open range) "0.0.0.0:2049:2049/tcp" "0.0.0.0:2096:2096/tcp" "0.0.0.0:14380-15379:14380-15379/tcp" "0.0.0.0:14380-15379:14380-15379/udp" ]; - realityPorts = - # Only vds needs the 15380→443 forwarding that lets nginx stream - # pass-through Xray REALITY while Xray itself sees the connection - # arriving on 443 (matching its REALITY inbound config). - lib.optional xlib.services."3x-ui".reality443Forwarding - "0.0.0.0:15380:443/tcp"; + # VDS-only: nginx stream forwards host:443 → host:15380 → container:443, + # so Xray inside the container sees its REALITY inbound on its real + # configured port 443. + realityPorts = lib.optional xlib.services."3x-ui".reality443Forwarding "0.0.0.0:15380:443/tcp"; in { virtualisation = { @@ -55,15 +56,8 @@ in "${panel}/db/:/etc/x-ui:rw" ] ++ certMounts; log-driver = "journald"; - # Port-forwarded networking (replaces --network=host). - # Common across all nodes that import this module: - # 2049/tcp — 3x-ui web panel - # 2096/tcp — subscription endpoint - # 14380-15379/tcp+udp — Xray inbounds (matches firewall open range) - # Vds-only (xlib.services.3x-ui.reality443Forwarding = true): - # 15380→443/tcp — Xray REALITY inbound (nginx stream on 443 → 15380) # Adding a new inbound through the 3x-ui panel on a port outside - # this range will require extending this list and rebuilding. + # the 14380-15379 range requires extending basePorts and rebuilding. ports = basePorts ++ realityPorts; }; }; @@ -72,21 +66,12 @@ in systemd = { services = { "podman-3xui_app" = { - serviceConfig = { - Restart = lib.mkOverride 90 "always"; - }; - partOf = [ - "podman-compose-3x-ui-root.target" - ]; - wantedBy = [ - "podman-compose-3x-ui-root.target" - ]; + serviceConfig.Restart = lib.mkOverride 90 "always"; + partOf = [ "podman-compose-3x-ui-root.target" ]; + wantedBy = [ "podman-compose-3x-ui-root.target" ]; }; - # Update "podman-update-3xui_app" = { - path = [ - pkgs.podman - ]; + path = [ pkgs.podman ]; serviceConfig = { Type = "oneshot"; TimeoutSec = 300; @@ -96,29 +81,10 @@ in systemctl restart podman-3xui_app.service ''; }; - # Builds - # "podman-build-3xui_app" = { - # path = [ - # pkgs.podman - # pkgs.git - # ]; - # serviceConfig = { - # Type = "oneshot"; - # TimeoutSec = 300; - # }; - # script = '' - # cd /mnt/containers/3x-ui - # podman build -t compose2nix/3xui_app -f ./Dockerfile . - # ''; - # }; }; - # Root service - # When started, this will automatically create all resources and start - # the containers. When stopped, this will teardown all resources. + # Starts/stops together with all 3x-ui compose resources. targets."podman-compose-3x-ui-root" = { - unitConfig = { - Description = "Root target generated by compose2nix."; - }; + unitConfig.Description = "Root target generated by compose2nix."; wantedBy = [ "multi-user.target" ]; }; timers."podman-update-3xui_app" = { @@ -128,15 +94,15 @@ in Persistent = true; }; }; - # Folders tmpfiles.rules = [ - "d ${xlib.dirs.services-mnt-folder} 0755 root root -" - "d ${xlib.dirs.services-nodes-folder} 0755 root root -" - "d ${xlib.dirs.services-nodes-folder}/${xlib.device.hostname} 0755 root root -" - "d ${panel} 0755 root root -" - "d ${panel}/db 0755 root root -" - "d ${panel}/cert 0755 root root -" - "Z ${panel} 0755 root root -" + (xlib.helpers.mkTmpfile "d" xlib.dirs.services-mnt-folder "0755" "root" "root") + (xlib.helpers.mkTmpfile "d" xlib.dirs.services-nodes-folder "0755" "root" "root") + (xlib.helpers.mkTmpfile "d" "${xlib.dirs.services-nodes-folder}/${xlib.device.hostname}" "0755" "root" "root") + (xlib.helpers.mkTmpfile "d" panel "0755" "root" "root") + (xlib.helpers.mkTmpfile "d" "${panel}/db" "0755" "root" "root") + (xlib.helpers.mkTmpfile "d" "${panel}/cert" "0755" "root" "root") + # Relabel panel dir for SELinux so containers can access it. + (xlib.helpers.mkTmpfile "Z" panel "0755" "root" "root") ]; }; diff --git a/modules/server/nginx.nix b/modules/server/nginx.nix index 9336e9e..a4e8a2b 100644 --- a/modules/server/nginx.nix +++ b/modules/server/nginx.nix @@ -5,17 +5,18 @@ xlib, ... }: +# Standard reverse-proxy: HTTP/S termination upstream, backend on the LAN. +# x.zeroq.su is the 3x-ui controller panel — see 3x-ui.nix for the +# /subs/, /subsjs/, /clash/ routing logic. let server = "192.168.1.20"; - # Standard TLS proxy vhost: "/" -> http://server:port - # Returns { name = domain; value = vhost; } for builtins.listToAttrs mkProxy = { domain, port, addSSL ? false, - body ? false, + extraConfig ? "", }: { name = domain; @@ -28,20 +29,17 @@ let } // lib.optionalAttrs (!addSSL) { forceSSL = true; } // lib.optionalAttrs addSSL { addSSL = true; } - // lib.optionalAttrs body { - extraConfig = '' - client_max_body_size 5G; - ''; - }; + // lib.optionalAttrs (extraConfig != "") { inherit extraConfig; }; }; - # Simple proxy sites + bigUploads = "client_max_body_size 5G;"; + sites = [ { domain = "immich.zeroq.su"; port = 2283; addSSL = true; - body = true; + extraConfig = bigUploads; } { domain = "kuma.zeroq.su"; @@ -71,337 +69,141 @@ let { domain = "calibre.zeroq.su"; port = 8083; - body = true; + extraConfig = bigUploads; } - # { - # domain = "mc.zeroq.su"; - # port = 25565; - # } { domain = "nix-cache.zeroq.su"; port = 5000; - body = true; + extraConfig = bigUploads; } { domain = "pdf.zeroq.su"; port = 8446; - body = true; + extraConfig = bigUploads; } ]; in { - services = { - nginx = { - enable = true; - recommendedGzipSettings = true; - recommendedOptimisation = true; - recommendedProxySettings = true; - recommendedTlsSettings = true; - 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; - }; - "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 = '' - client_max_body_size 5G; - ''; - }; - "x.zeroq.su" = { - forceSSL = true; - enableACME = true; - locations = { - "/" = { - proxyPass = "http://${server}:2049"; - proxyWebsockets = true; - }; - "/subs/" = { - proxyPass = "http://${server}:2096"; - proxyWebsockets = true; - }; - "/subsjs/" = { - proxyPass = "http://${server}:2096"; - proxyWebsockets = true; - }; - "/clash/" = { - proxyPass = "http://${server}:2096"; - proxyWebsockets = true; - }; + services.nginx = { + enable = true; + recommendedGzipSettings = true; + recommendedOptimisation = true; + recommendedProxySettings = true; + recommendedTlsSettings = true; + 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; + }; + "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" = { + forceSSL = true; + enableACME = true; + locations = { + "/" = { + proxyPass = "http://${server}:2049"; + proxyWebsockets = true; }; - }; - # "talk.zeroq.su" = { - # forceSSL = true; - # enableACME = true; - # # locations = { - # # "/" = { - # # proxyPass = "http://127.0.0.1:7880"; - # # proxyWebsockets = true; - # # }; - # # }; - # extraConfig = '' - # client_max_body_size 5G; - # ''; - # }; - # "turn.zeroq.su" = { - # forceSSL = true; - # enableACME = true; - # locations = { - # "/" = { - # proxyPass = "http://127.0.0.1:5349"; - # proxyWebsockets = true; - # }; - # }; - # extraConfig = '' - # client_max_body_size 5G; - # ''; - # }; - # "ca.home.arpa" = { - # forceSSL = true; - # enableACME = true; - # locations."/" = { - # proxyPass = "http://127.0.0.1:9000"; - # proxyWebsockets = true; - # }; - # extraConfig = '' - # client_max_body_size 5G; - # ''; - # }; - # "n8n.zeroq.su" = { - # forceSSL = true; - # enableACME = true; - # locations."/" = { - # proxyPass = "http://${server}:5678"; - # proxyWebsockets = true; - # }; - # extraConfig = '' - # client_max_body_size 5G; - # ''; - # }; - # "kuma.home.arpa" = { - # forceSSL = true; - # enableACME = true; - # locations."/" = { - # proxyPass = "http://127.0.0.1:4001"; - # proxyWebsockets = true; - # }; - # }; - # "flux.home.arpa" = { - # addSSL = true; - # enableACME = true; - # locations."/" = { - # proxyPass = "http://127.0.0.1:6061"; - # proxyWebsockets = true; - # }; - # }; - # "navidrome.home.arpa" = { - # addSSL = true; - # enableACME = true; - # locations."/" = { - # proxyPass = "http://127.0.0.1:4533"; - # proxyWebsockets = true; - # }; - # }; - # "immich.home.arpa" = { - # addSSL = true; - # enableACME = true; - # locations."/" = { - # proxyPass = "http://127.0.0.1:2283"; - # proxyWebsockets = true; - # }; - # extraConfig = '' - # client_max_body_size 5G; - # ''; - # }; - # "agent.zeroq.su" = { - # forceSSL = true; - # enableACME = true; - # locations."/" = { - # proxyPass = "http://${server}:3000"; - # proxyWebsockets = true; - # }; - # }; - # "node-red.zeroq.su" = { - # forceSSL = true; - # enableACME = true; - # kTLS = true; - # locations."/" = { - # proxyPass = "http://${server}:1880"; - # proxyWebsockets = true; - # }; - # extraConfig = '' - # client_max_body_size 5G; - # ''; - # }; - "zeroq.su" = { - forceSSL = true; - enableACME = true; - root = pkgs.writeTextDir "index.html" '' - - - -
What are you doing here?
- - - ''; - locations = { - "/guest/" = { - proxyPass = "http://${server}:80"; - proxyWebsockets = true; - }; - # "/.well-known/discord" = { - # extraConfig = '' - # default_type text/plain; - # return 200 "dh=c2d103553a4cfdaa1b7952a87a7d8120a1e167cc"; - # ''; - # }; + "/subs/" = { + proxyPass = "http://${server}:2096"; + proxyWebsockets = true; }; - }; - "vetymae.opencodes.zeroq.su" = { - forceSSL = true; - enableACME = true; - locations."/" = { - proxyPass = "http://100.86.62.4:4096"; + "/subsjs/" = { + proxyPass = "http://${server}:2096"; + proxyWebsockets = true; + }; + "/clash/" = { + proxyPass = "http://${server}:2096"; proxyWebsockets = true; }; }; - "lamet.opencodes.zeroq.su" = { - forceSSL = true; - enableACME = true; - locations."/" = { - proxyPass = "http://100.106.21.39:6061"; + }; + "zeroq.su" = { + forceSSL = true; + enableACME = true; + 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; + locations."/" = { + proxyPass = "http://100.86.62.4:4096"; + proxyWebsockets = true; + }; + }; + "lamet.opencodes.zeroq.su" = { + forceSSL = true; + enableACME = true; + locations."/" = { + proxyPass = "http://100.106.21.39:6061"; + proxyWebsockets = true; + }; + }; + "nextcloud.zeroq.su" = { + forceSSL = true; + enableACME = true; + locations = { + "/" = { + proxyPass = "http://${server}:10000"; + proxyWebsockets = true; + }; + "/whiteboard" = { + proxyPass = "http://${server}:3002"; proxyWebsockets = true; }; }; - # "n8n.zeroq.su" = { - # forceSSL = true; - # enableACME = true; - # locations."/" = { - # proxyPass = "http://${server}:5678"; - # proxyWebsockets = true; - # }; - # }; - # "office.zeroq.su" = { - # enableACME = true; - # forceSSL = true; - # locations = { - # "/" = { - # proxyPass = "http://${server}:8090"; - # proxyWebsockets = true; - # }; - # }; - # }; - "nextcloud.zeroq.su" = { - forceSSL = true; - enableACME = true; - locations = { - "/" = { - proxyPass = "http://${server}:10000"; - proxyWebsockets = true; - }; - "/whiteboard" = { - proxyPass = "http://${server}:3002"; - proxyWebsockets = true; - }; - }; - extraConfig = '' - client_max_body_size 5G; - ''; - }; - # "calibre.home.arpa" = { - # forceSSL = true; - # enableACME = true; - # locations."/" = { - # proxyPass = "http://127.0.0.1:8083"; - # proxyWebsockets = true; - # }; - # extraConfig = '' - # client_max_body_size 5G; - # ''; - # }; - # "dns.home.arpa" = { - # forceSSL = true; - # enableACME = true; - # locations."/" = { - # proxyPass = "http://127.0.0.1:53"; - # }; - # extraConfig = '' - # client_max_body_size 5G; - # ''; - # }; - # "glances.home.arpa" = { - # forceSSL = true; - # enableACME = true; - # locations."/" = { - # proxyPass = "http://127.0.0.1:61208"; - # }; - # }; - # "syncthing.home.arpa" = { - # addSSL = true; - # enableACME = true; - # locations."/" = { - # proxyPass = "http://127.0.0.1:8384"; - # }; - # }; - # "zeroq.home.arpa" = { - # forceSSL = true; - # enableACME = true; - # root = pkgs.writeTextDir "index.html" '' - # - # - # - #
This server is running in backend.
- # - # - # ''; - # listen = [ - # { - # addr = "100.64.0.0"; - # port = 80; - # } - # { - # addr = "192.168.1.20"; - # port = 80; - # } - # ]; - # }; + extraConfig = bigUploads; }; }; }; diff --git a/modules/vds/nginx.nix b/modules/vds/nginx.nix index f44e306..9f494eb 100644 --- a/modules/vds/nginx.nix +++ b/modules/vds/nginx.nix @@ -4,92 +4,72 @@ pkgs, ... }: +# VDS nginx differs from server's in one key way: port 443 is owned +# by an nginx stream block that does SNI-based TCP routing, not by +# http { server {} } blocks. This lets a single host (pubray1.zeroq.su) +# serve both the 3x-ui panel and an Xray REALITY inbound over TLS, +# sharing the same port. +# +# Routing: +# pubray1.zeroq.su → 127.0.0.1:2049 (3x-ui panel; LE cert mounted +# into the container terminates TLS) +# pubrayx1.zeroq.su → 127.0.0.1:15380 (Xray REALITY; Xray sees its +# real configured port 443 via DNAT) +# default → 127.0.0.1:15380 (REALITY fallback for any +# other SNI / IP-direct) +# +# ssl_preread reads SNI from the ClientHello and forwards the rest of +# the TLS stream as-is, so Xray sees a real port-443 connection even +# though podman DNATs it via host:15380. let - server = "100.64.0.0"; - # TCP-level SNI routing on 443: - # pubray1.zeroq.su → 3x-ui panel (TLS passthrough to 127.0.0.1:2049, - # 3x-ui terminates TLS using the cert mounted - # from /var/lib/acme/pubray1.zeroq.su/) - # pubrayx1.zeroq.su → Xray REALITY (TLS passthrough to 127.0.0.1:15380, - # which podman DNATs to container:443) - # default → Xray (any other SNI / IP-direct hits the REALITY - # fallback, which is what we want) - # nginx stream does not inspect TLS — the SNI is read from the ClientHello - # via ssl_preread, then the entire TCP stream (including the rest of the - # TLS handshake) is forwarded as-is to the chosen upstream. So Xray sees - # the client connect on its real configured port (443) even though the - # host-side port from podman's perspective is 15380. + # Goes inside the auto-generated `stream {}` block (services.nginx.streamConfig). + # pubray1.zeroq.su → 3x-ui panel; everything else (including any SNI + # a REALITY client uses, e.g. media.mediavitrina.ru) → Xray. streamConfig = '' - stream { - ssl_preread on; + ssl_preread on; - # pubray1.zeroq.su SNI → 3x-ui panel (TLS terminated inside the - # container using the LE cert mounted from /var/lib/acme/). - # Everything else (including any sni the REALITY client uses, - # e.g. media.mediavitrina.ru) → Xray. So a single domain - # pubray1.zeroq.su serves both panel and Xray — the SNI in the - # TLS ClientHello disambiguates. - map $ssl_preread_server_name $sni_backend { - default xray; - pubray1.zeroq.su panel; - } + map $ssl_preread_server_name $sni_backend { + default xray; + pubray1.zeroq.su panel; + } - upstream panel { - server 127.0.0.1:2049; - } + upstream panel { + server 127.0.0.1:2049; + } - upstream xray { - server 127.0.0.1:15380; - } + upstream xray { + server 127.0.0.1:15380; + } - server { - listen 443; - proxy_pass $sni_backend; - proxy_timeout 600s; - proxy_connect_timeout 5s; - } + server { + listen 443; + proxy_pass $sni_backend; + proxy_timeout 600s; + proxy_connect_timeout 5s; } ''; in { users.users.nginx.extraGroups = [ "acme" ]; - services = { - nginx = { - enable = true; - # No virtualHosts..locations for /, /subs/, /subsjs/, /clash/ - # anymore — port 443 is now owned by the stream block, and the - # 3x-ui panel serves those paths itself once TLS is forwarded to it. - # Recommended HTTP options retained (still apply to the port-80 - # ACME server block). - recommendedGzipSettings = true; - recommendedOptimisation = true; - recommendedProxySettings = true; - recommendedTlsSettings = true; - virtualHosts = { - # ACME only — nginx owns no HTTP server on 443 anymore. The - # cert at /var/lib/acme/pubray1.zeroq.su/ is consumed by the - # 3x-ui container (mounted into /root/cert/) so it can do its - # own TLS termination on 2049. Don't add forceSSL here: that - # would generate an HTTPS server block on 443 that conflicts - # with the stream listener above. - "pubray1.zeroq.su" = { - enableACME = true; - }; - }; - # Top-level nginx.conf snippet — `stream {}` lives outside `http {}`, - # so it can't go through virtualHosts / appendHttpConfig. The NixOS - # nginx module exposes appendConfig for this. - appendConfig = streamConfig; - }; + services.nginx = { + enable = true; + recommendedGzipSettings = true; + recommendedOptimisation = true; + recommendedProxySettings = true; + recommendedTlsSettings = true; + # ACME only — 443 is owned by the stream block, not by http { server {} }. + # Don't add forceSSL: it would generate an HTTPS server block that + # conflicts with the stream listener. + virtualHosts."pubray1.zeroq.su".enableACME = true; + # Lands inside the auto-generated `stream {}` block. + streamConfig = streamConfig; }; security.acme = { acceptTerms = true; - defaults = { - email = "oqyude@gmail.com"; - }; + defaults.email = "oqyude@gmail.com"; }; networking.firewall.allowedTCPPorts = [ 80 443 ]; -} \ No newline at end of file +}