From 7f5ea81f37f7ddd2f75ff9f2166d894a82411a5e Mon Sep 17 00:00:00 2001 From: oqyude Date: Fri, 28 Aug 2026 01:16:00 +0300 Subject: [PATCH] 3x-ui: make module generic via xlib.services.3x-ui options MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 3x-ui container config was hardcoded for vds: it mounted the LE cert for pubray1.zeroq.su and published host:15380→container:443 for Xray REALITY. The server imports the same module but for x.zeroq.su (no REALITY inbound, no cert needed by 3x-ui itself yet). Add two options so each device picks what it needs: - xlib.services.3x-ui.certDomain: domain whose LE cert is mounted at /root/cert/{fullchain,key}.pem. null means no cert mount. - xlib.services.3x-ui.reality443Forwarding: when true, also publish host:15380→container:443 for nginx stream SNI-routed REALITY. vds sets both. Server sets only certDomain (kept harmless; nginx still terminates TLS for x.zeroq.su, so the mounted cert is unused until/unless 3x-ui is reconfigured to terminate TLS itself). --- modules/containers/3x-ui.nix | 49 +++++++++++++++++++++--------------- modules/options.nix | 29 +++++++++++++++++++++ modules/server/default.nix | 6 +++++ modules/vds/default.nix | 6 +++++ 4 files changed, 70 insertions(+), 20 deletions(-) diff --git a/modules/containers/3x-ui.nix b/modules/containers/3x-ui.nix index 9fb4335..2cba403 100644 --- a/modules/containers/3x-ui.nix +++ b/modules/containers/3x-ui.nix @@ -7,6 +7,29 @@ }: let panel = "${xlib.dirs.services-nodes-folder}/${xlib.device.hostname}/3x-ui"; + 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" + ]; + basePorts = [ + "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"; in { virtualisation = { @@ -30,32 +53,18 @@ in volumes = [ "${panel}/cert/:/root/cert:rw" "${panel}/db/:/etc/x-ui:rw" - # Let's Encrypt cert for pubray1.zeroq.su — mounted read-only so - # 3x-ui can serve the panel over its own TLS (required for the - # nginx stream SNI-route on 443 → panel:2049 to work without - # HTTP termination at nginx). webCertFile / webKeyFile in the - # x-ui settings table must point at /root/cert/fullchain.pem - # and /root/cert/key.pem respectively. - "/var/lib/acme/pubray1.zeroq.su/fullchain.pem:/root/cert/fullchain.pem:ro" - "/var/lib/acme/pubray1.zeroq.su/key.pem:/root/cert/key.pem:ro" - ]; + ] ++ certMounts; log-driver = "journald"; # Port-forwarded networking (replaces --network=host). - # Bridges the same ports that were previously reachable while - # --network=host was set: - # 2049/tcp — 3x-ui web panel (TLS, SNI-routed from 443) - # 2096/tcp — subscription endpoint (reverse-proxied by nginx) + # 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. - ports = [ - "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" - "0.0.0.0:15380:443/tcp" - ]; + ports = basePorts ++ realityPorts; }; }; }; diff --git a/modules/options.nix b/modules/options.nix index 100735f..2d7a3c5 100644 --- a/modules/options.nix +++ b/modules/options.nix @@ -75,6 +75,35 @@ in default = helpers; description = "Shared helper functions (see lib/xlib.nix)."; }; + services."3x-ui" = { + # Domain whose Let's Encrypt cert (at /var/lib/acme//) + # gets mounted read-only into the 3x-ui container so the panel + # can terminate TLS itself. Set null if 3x-ui serves plain HTTP + # and TLS is terminated by an upstream nginx. + certDomain = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + example = "pubray1.zeroq.su"; + description = '' + Domain whose LE cert should be mounted into the 3x-ui + container at /root/cert/fullchain.pem and key.pem. + ''; + }; + # Publish host:15380 → container:443. Only nodes that host an + # Xray REALITY inbound on container:443 need this (so nginx + # stream can forward TLS to Xray via 127.0.0.1:15380 while Xray + # itself sees incoming connections on its configured port 443). + # Set false on nodes that only run the 3x-ui panel. + reality443Forwarding = lib.mkOption { + type = lib.types.bool; + default = false; + description = '' + When true, publish host:15380 → container:443 so Xray + inside the container can serve REALITY on its real + configured port 443 (nginx stream forwards 443 → 15380). + ''; + }; + }; }; }; } diff --git a/modules/server/default.nix b/modules/server/default.nix index 208e9bd..7fbe7e9 100644 --- a/modules/server/default.nix +++ b/modules/server/default.nix @@ -44,6 +44,12 @@ # ./trilium.nix # ./zerotier.nix ]; + # Server's 3x-ui is the controller panel at x.zeroq.su (nginx HTTP + # terminates TLS upstream, no SNI-routing on 443 needed here because + # there are other vhosts on the same port). Cert is still mounted in + # case 3x-ui is later reconfigured to terminate TLS itself (e.g. for + # direct node-API access); nginx doesn't have to use it. + xlib.services."3x-ui".certDomain = "x.zeroq.su"; systemd.tmpfiles.rules = [ (xlib.helpers.mkTmpfile "d" "/mnt" "0755" "root" "root") (xlib.helpers.mkTmpfile "d" xlib.dirs.services-mnt-folder "0755" "root" "root") diff --git a/modules/vds/default.nix b/modules/vds/default.nix index b3f5c3b..aa40996 100644 --- a/modules/vds/default.nix +++ b/modules/vds/default.nix @@ -13,6 +13,12 @@ # ./netbird.nix # ./xray.nix ]; + # VDS hosts the public-facing Xray REALITY inbound on container:443, + # fronted by nginx stream on host:443 → host:15380 → container:443. + xlib.services."3x-ui" = { + certDomain = "pubray1.zeroq.su"; + reality443Forwarding = true; + }; systemd.tmpfiles.rules = [ (xlib.helpers.mkTmpfile "d" "/mnt" "0755" "root" "root") (xlib.helpers.mkTmpfile "d" xlib.dirs.services-mnt-folder "0755" "root" "root")