refactoring

This commit is contained in:
2026-08-29 04:05:38 +03:00
parent 7f5ea81f37
commit e1d276097d
3 changed files with 203 additions and 455 deletions
+30 -64
View File
@@ -10,26 +10,27 @@ let
certDomain = xlib.services."3x-ui".certDomain or null; certDomain = xlib.services."3x-ui".certDomain or null;
certMounts = certMounts =
if certDomain == null then [ ] if certDomain == null then [ ]
else [ else
# Let's Encrypt cert for the panel domain — mounted read-only so # LE cert mounted read-only so 3x-ui can terminate TLS itself.
# 3x-ui can serve the panel over its own TLS. webCertFile / # The 3x-ui settings table must point webCertFile / webKeyFile at
# webKeyFile in the x-ui settings table must point at # /root/cert/fullchain.pem and /root/cert/key.pem.
# /root/cert/fullchain.pem and /root/cert/key.pem respectively. map (f: "/var/lib/acme/${certDomain}/${f}:/root/cert/${f}:ro") [
"/var/lib/acme/${certDomain}/fullchain.pem:/root/cert/fullchain.pem:ro" "fullchain.pem"
"/var/lib/acme/${certDomain}/key.pem:/root/cert/key.pem:ro" "key.pem"
]; ];
basePorts = [ 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:2049:2049/tcp"
"0.0.0.0:2096:2096/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/tcp"
"0.0.0.0:14380-15379:14380-15379/udp" "0.0.0.0:14380-15379:14380-15379/udp"
]; ];
realityPorts = # VDS-only: nginx stream forwards host:443 → host:15380 → container:443,
# Only vds needs the 15380→443 forwarding that lets nginx stream # so Xray inside the container sees its REALITY inbound on its real
# pass-through Xray REALITY while Xray itself sees the connection # configured port 443.
# arriving on 443 (matching its REALITY inbound config). realityPorts = lib.optional xlib.services."3x-ui".reality443Forwarding "0.0.0.0:15380:443/tcp";
lib.optional xlib.services."3x-ui".reality443Forwarding
"0.0.0.0:15380:443/tcp";
in in
{ {
virtualisation = { virtualisation = {
@@ -55,15 +56,8 @@ in
"${panel}/db/:/etc/x-ui:rw" "${panel}/db/:/etc/x-ui:rw"
] ++ certMounts; ] ++ certMounts;
log-driver = "journald"; 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 # 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; ports = basePorts ++ realityPorts;
}; };
}; };
@@ -72,21 +66,12 @@ in
systemd = { systemd = {
services = { services = {
"podman-3xui_app" = { "podman-3xui_app" = {
serviceConfig = { serviceConfig.Restart = lib.mkOverride 90 "always";
Restart = lib.mkOverride 90 "always"; partOf = [ "podman-compose-3x-ui-root.target" ];
}; wantedBy = [ "podman-compose-3x-ui-root.target" ];
partOf = [
"podman-compose-3x-ui-root.target"
];
wantedBy = [
"podman-compose-3x-ui-root.target"
];
}; };
# Update
"podman-update-3xui_app" = { "podman-update-3xui_app" = {
path = [ path = [ pkgs.podman ];
pkgs.podman
];
serviceConfig = { serviceConfig = {
Type = "oneshot"; Type = "oneshot";
TimeoutSec = 300; TimeoutSec = 300;
@@ -96,29 +81,10 @@ in
systemctl restart podman-3xui_app.service 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 # Starts/stops together with all 3x-ui compose resources.
# When started, this will automatically create all resources and start
# the containers. When stopped, this will teardown all resources.
targets."podman-compose-3x-ui-root" = { targets."podman-compose-3x-ui-root" = {
unitConfig = { unitConfig.Description = "Root target generated by compose2nix.";
Description = "Root target generated by compose2nix.";
};
wantedBy = [ "multi-user.target" ]; wantedBy = [ "multi-user.target" ];
}; };
timers."podman-update-3xui_app" = { timers."podman-update-3xui_app" = {
@@ -128,15 +94,15 @@ in
Persistent = true; Persistent = true;
}; };
}; };
# Folders
tmpfiles.rules = [ tmpfiles.rules = [
"d ${xlib.dirs.services-mnt-folder} 0755 root root -" (xlib.helpers.mkTmpfile "d" xlib.dirs.services-mnt-folder "0755" "root" "root")
"d ${xlib.dirs.services-nodes-folder} 0755 root root -" (xlib.helpers.mkTmpfile "d" xlib.dirs.services-nodes-folder "0755" "root" "root")
"d ${xlib.dirs.services-nodes-folder}/${xlib.device.hostname} 0755 root root -" (xlib.helpers.mkTmpfile "d" "${xlib.dirs.services-nodes-folder}/${xlib.device.hostname}" "0755" "root" "root")
"d ${panel} 0755 root root -" (xlib.helpers.mkTmpfile "d" panel "0755" "root" "root")
"d ${panel}/db 0755 root root -" (xlib.helpers.mkTmpfile "d" "${panel}/db" "0755" "root" "root")
"d ${panel}/cert 0755 root root -" (xlib.helpers.mkTmpfile "d" "${panel}/cert" "0755" "root" "root")
"Z ${panel} 0755 root root -" # Relabel panel dir for SELinux so containers can access it.
(xlib.helpers.mkTmpfile "Z" panel "0755" "root" "root")
]; ];
}; };
+123 -321
View File
@@ -5,17 +5,18 @@
xlib, 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 let
server = "192.168.1.20"; server = "192.168.1.20";
# Standard TLS proxy vhost: "/" -> http://server:port
# Returns { name = domain; value = vhost; } for builtins.listToAttrs
mkProxy = mkProxy =
{ {
domain, domain,
port, port,
addSSL ? false, addSSL ? false,
body ? false, extraConfig ? "",
}: }:
{ {
name = domain; name = domain;
@@ -28,20 +29,17 @@ let
} }
// lib.optionalAttrs (!addSSL) { forceSSL = true; } // lib.optionalAttrs (!addSSL) { forceSSL = true; }
// lib.optionalAttrs addSSL { addSSL = true; } // lib.optionalAttrs addSSL { addSSL = true; }
// lib.optionalAttrs body { // lib.optionalAttrs (extraConfig != "") { inherit extraConfig; };
extraConfig = ''
client_max_body_size 5G;
'';
};
}; };
# Simple proxy sites bigUploads = "client_max_body_size 5G;";
sites = [ sites = [
{ {
domain = "immich.zeroq.su"; domain = "immich.zeroq.su";
port = 2283; port = 2283;
addSSL = true; addSSL = true;
body = true; extraConfig = bigUploads;
} }
{ {
domain = "kuma.zeroq.su"; domain = "kuma.zeroq.su";
@@ -71,337 +69,141 @@ let
{ {
domain = "calibre.zeroq.su"; domain = "calibre.zeroq.su";
port = 8083; port = 8083;
body = true; extraConfig = bigUploads;
} }
# {
# domain = "mc.zeroq.su";
# port = 25565;
# }
{ {
domain = "nix-cache.zeroq.su"; domain = "nix-cache.zeroq.su";
port = 5000; port = 5000;
body = true; extraConfig = bigUploads;
} }
{ {
domain = "pdf.zeroq.su"; domain = "pdf.zeroq.su";
port = 8446; port = 8446;
body = true; extraConfig = bigUploads;
} }
]; ];
in in
{ {
services = { services.nginx = {
nginx = { enable = true;
enable = true; recommendedGzipSettings = true;
recommendedGzipSettings = true; recommendedOptimisation = true;
recommendedOptimisation = true; recommendedProxySettings = true;
recommendedProxySettings = true; recommendedTlsSettings = true;
recommendedTlsSettings = true; virtualHosts = (builtins.listToAttrs (map mkProxy sites)) // {
virtualHosts = (builtins.listToAttrs (map mkProxy sites)) // { "nextcloud.private" = {
"nextcloud.private" = { forceSSL = false;
forceSSL = false; enableACME = false;
enableACME = false; listen = [
listen = [ {
{ addr = "100.64.0.0";
addr = "100.64.0.0"; port = 10000;
port = 10000; }
} {
{ addr = "192.168.1.20";
addr = "192.168.1.20"; port = 10000;
port = 10000; }
} {
{ addr = "127.0.0.1";
addr = "127.0.0.1"; port = 10000;
port = 10000; }
} ];
]; };
}; "office.zeroq.su" = {
"office.zeroq.su" = { forceSSL = true;
forceSSL = true; enableACME = true;
enableACME = true; };
}; "pdf.private" = {
"pdf.private" = { forceSSL = false;
forceSSL = false; enableACME = false;
enableACME = false; listen = [
listen = [ {
{ addr = "0.0.0.0";
addr = "0.0.0.0"; port = 80;
port = 80; }
} {
{ addr = "100.64.0.0";
addr = "100.64.0.0"; port = 8446;
port = 8446; }
} {
{ addr = "192.168.1.20";
addr = "192.168.1.20"; port = 8446;
port = 8446; }
} {
{ addr = "127.0.0.1";
addr = "127.0.0.1"; port = 8446;
port = 8446; }
} ];
]; extraConfig = bigUploads;
extraConfig = '' };
client_max_body_size 5G; "x.zeroq.su" = {
''; forceSSL = true;
}; enableACME = true;
"x.zeroq.su" = { locations = {
forceSSL = true; "/" = {
enableACME = true; proxyPass = "http://${server}:2049";
locations = { proxyWebsockets = true;
"/" = {
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;
};
}; };
}; "/subs/" = {
# "talk.zeroq.su" = { proxyPass = "http://${server}:2096";
# forceSSL = true; proxyWebsockets = 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" ''
<!doctype html>
<html>
<body>
<pre>What are you doing here?</pre>
</body>
</html>
'';
locations = {
"/guest/" = {
proxyPass = "http://${server}:80";
proxyWebsockets = true;
};
# "/.well-known/discord" = {
# extraConfig = ''
# default_type text/plain;
# return 200 "dh=c2d103553a4cfdaa1b7952a87a7d8120a1e167cc";
# '';
# };
}; };
}; "/subsjs/" = {
"vetymae.opencodes.zeroq.su" = { proxyPass = "http://${server}:2096";
forceSSL = true; proxyWebsockets = true;
enableACME = true; };
locations."/" = { "/clash/" = {
proxyPass = "http://100.86.62.4:4096"; proxyPass = "http://${server}:2096";
proxyWebsockets = true; proxyWebsockets = true;
}; };
}; };
"lamet.opencodes.zeroq.su" = { };
forceSSL = true; "zeroq.su" = {
enableACME = true; forceSSL = true;
locations."/" = { enableACME = true;
proxyPass = "http://100.106.21.39:6061"; root = pkgs.writeTextDir "index.html" ''
<!doctype html>
<html>
<body>
<pre>What are you doing here?</pre>
</body>
</html>
'';
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; proxyWebsockets = true;
}; };
}; };
# "n8n.zeroq.su" = { extraConfig = bigUploads;
# 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" ''
# <!doctype html>
# <html>
# <body>
# <pre>This server is running in backend.</pre>
# </body>
# </html>
# '';
# listen = [
# {
# addr = "100.64.0.0";
# port = 80;
# }
# {
# addr = "192.168.1.20";
# port = 80;
# }
# ];
# };
}; };
}; };
}; };
+50 -70
View File
@@ -4,92 +4,72 @@
pkgs, 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 let
server = "100.64.0.0"; # Goes inside the auto-generated `stream {}` block (services.nginx.streamConfig).
# TCP-level SNI routing on 443: # pubray1.zeroq.su → 3x-ui panel; everything else (including any SNI
# pubray1.zeroq.su → 3x-ui panel (TLS passthrough to 127.0.0.1:2049, # a REALITY client uses, e.g. media.mediavitrina.ru) → Xray.
# 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.
streamConfig = '' streamConfig = ''
stream { ssl_preread on;
ssl_preread on;
# pubray1.zeroq.su SNI → 3x-ui panel (TLS terminated inside the map $ssl_preread_server_name $sni_backend {
# container using the LE cert mounted from /var/lib/acme/). default xray;
# Everything else (including any sni the REALITY client uses, pubray1.zeroq.su panel;
# 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;
}
upstream panel { upstream panel {
server 127.0.0.1:2049; server 127.0.0.1:2049;
} }
upstream xray { upstream xray {
server 127.0.0.1:15380; server 127.0.0.1:15380;
} }
server { server {
listen 443; listen 443;
proxy_pass $sni_backend; proxy_pass $sni_backend;
proxy_timeout 600s; proxy_timeout 600s;
proxy_connect_timeout 5s; proxy_connect_timeout 5s;
}
} }
''; '';
in in
{ {
users.users.nginx.extraGroups = [ "acme" ]; users.users.nginx.extraGroups = [ "acme" ];
services = { services.nginx = {
nginx = { enable = true;
enable = true; recommendedGzipSettings = true;
# No virtualHosts.<name>.locations for /, /subs/, /subsjs/, /clash/ recommendedOptimisation = true;
# anymore — port 443 is now owned by the stream block, and the recommendedProxySettings = true;
# 3x-ui panel serves those paths itself once TLS is forwarded to it. recommendedTlsSettings = true;
# Recommended HTTP options retained (still apply to the port-80 # ACME only — 443 is owned by the stream block, not by http { server {} }.
# ACME server block). # Don't add forceSSL: it would generate an HTTPS server block that
recommendedGzipSettings = true; # conflicts with the stream listener.
recommendedOptimisation = true; virtualHosts."pubray1.zeroq.su".enableACME = true;
recommendedProxySettings = true; # Lands inside the auto-generated `stream {}` block.
recommendedTlsSettings = true; streamConfig = streamConfig;
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;
};
}; };
security.acme = { security.acme = {
acceptTerms = true; acceptTerms = true;
defaults = { defaults.email = "oqyude@gmail.com";
email = "oqyude@gmail.com";
};
}; };
networking.firewall.allowedTCPPorts = [ networking.firewall.allowedTCPPorts = [
80 80
443 443
]; ];
} }