3x-ui server mask

This commit is contained in:
2026-09-17 00:56:41 +03:00
parent b57ca3eedf
commit 5f1496e980
2 changed files with 102 additions and 29 deletions
+8 -6
View File
@@ -44,12 +44,14 @@
# ./trilium.nix # ./trilium.nix
# ./zerotier.nix # ./zerotier.nix
]; ];
# Server's 3x-ui is the controller panel at x.zeroq.su (nginx HTTP # Server's 3x-ui is the controller panel at x.zeroq.su, wired the same
# terminates TLS upstream, no SNI-routing on 443 needed here because # way as the VDS node: nginx stream SNI-routes x.zeroq.su → container:2049
# there are other vhosts on the same port). Cert is still mounted in # (3x-ui terminates TLS itself with the mounted LE cert), and the default
# case 3x-ui is later reconfigured to terminate TLS itself (e.g. for # SNI → Xray REALITY via host:15380 → container:443 (see nginx.nix).
# direct node-API access); nginx doesn't have to use it. xlib.services."3x-ui" = {
xlib.services."3x-ui".certDomain = "x.zeroq.su"; certDomain = "x.zeroq.su";
reality443Forwarding = true;
};
systemd.tmpfiles.rules = [ systemd.tmpfiles.rules = [
(xlib.helpers.mkTmpfile "d" "/mnt" "0755" "root" "root") (xlib.helpers.mkTmpfile "d" "/mnt" "0755" "root" "root")
(xlib.helpers.mkTmpfile "d" xlib.dirs.services-mnt-folder "0755" "root" "root") (xlib.helpers.mkTmpfile "d" xlib.dirs.services-mnt-folder "0755" "root" "root")
+93 -22
View File
@@ -5,12 +5,44 @@
xlib, xlib,
... ...
}: }:
# Standard reverse-proxy: HTTP/S termination upstream, backend on the LAN. # SNI-fronted reverse proxy (vds/nginx.nix style): port 443 is owned by an
# x.zeroq.su is the 3x-ui controller panel — see 3x-ui.nix for the # nginx stream block that reads the ClientHello SNI and forwards the raw
# /subs/, /subsjs/, /clash/ routing logic. # 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 let
server = "192.168.1.20"; 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 = mkProxy =
{ {
domain, domain,
@@ -22,6 +54,7 @@ let
name = domain; name = domain;
value = { value = {
enableACME = true; enableACME = true;
listen = webListen;
locations."/" = { locations."/" = {
proxyPass = "http://${server}:${toString port}"; proxyPass = "http://${server}:${toString port}";
proxyWebsockets = true; proxyWebsockets = true;
@@ -82,6 +115,49 @@ let
extraConfig = bigUploads; 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 in
{ {
services.nginx = { services.nginx = {
@@ -90,6 +166,8 @@ in
recommendedOptimisation = true; recommendedOptimisation = true;
recommendedProxySettings = true; recommendedProxySettings = true;
recommendedTlsSettings = true; recommendedTlsSettings = true;
# Lands inside the auto-generated `stream {}` block.
streamConfig = streamConfig;
virtualHosts = (builtins.listToAttrs (map mkProxy sites)) // { virtualHosts = (builtins.listToAttrs (map mkProxy sites)) // {
"nextcloud.private" = { "nextcloud.private" = {
forceSSL = false; forceSSL = false;
@@ -112,6 +190,7 @@ in
"office.zeroq.su" = { "office.zeroq.su" = {
forceSSL = true; forceSSL = true;
enableACME = true; enableACME = true;
listen = webListen;
}; };
"pdf.private" = { "pdf.private" = {
forceSSL = false; forceSSL = false;
@@ -137,30 +216,19 @@ in
extraConfig = bigUploads; extraConfig = bigUploads;
}; };
"x.zeroq.su" = { "x.zeroq.su" = {
forceSSL = true; # 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; 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;
};
};
}; };
"zeroq.su" = { "zeroq.su" = {
forceSSL = true; forceSSL = true;
enableACME = true; enableACME = true;
listen = webListen;
root = pkgs.writeTextDir "index.html" '' root = pkgs.writeTextDir "index.html" ''
<!doctype html> <!doctype html>
<html> <html>
@@ -177,6 +245,7 @@ in
"vetymae.opencodes.zeroq.su" = { "vetymae.opencodes.zeroq.su" = {
forceSSL = true; forceSSL = true;
enableACME = true; enableACME = true;
listen = webListen;
locations."/" = { locations."/" = {
proxyPass = "http://100.86.62.4:4096"; proxyPass = "http://100.86.62.4:4096";
proxyWebsockets = true; proxyWebsockets = true;
@@ -185,6 +254,7 @@ in
"lamet.opencodes.zeroq.su" = { "lamet.opencodes.zeroq.su" = {
forceSSL = true; forceSSL = true;
enableACME = true; enableACME = true;
listen = webListen;
locations."/" = { locations."/" = {
proxyPass = "http://100.106.21.39:6061"; proxyPass = "http://100.106.21.39:6061";
proxyWebsockets = true; proxyWebsockets = true;
@@ -193,6 +263,7 @@ in
"nextcloud.zeroq.su" = { "nextcloud.zeroq.su" = {
forceSSL = true; forceSSL = true;
enableACME = true; enableACME = true;
listen = webListen;
locations = { locations = {
"/" = { "/" = {
proxyPass = "http://${server}:10000"; proxyPass = "http://${server}:10000";