Всё TCP через порт 443: nginx разводит по SNI и путям
- nginx stream на 443: по SNI — REALITY, XHTTP REALITY (свой сайт маскировки), MTProto (свой FakeTLS-домен); остальное — HTTPS-сервер nginx - HTTPS-сервер: VLESS WS, VMess WS, Trojan gRPC, подписка (kit-sub) и панель по секретным путям, сайт-заглушка на всё прочее - Подключения слушают только localhost, ссылки ведут на 443 через External Proxy 3X-UI; настоящие IP клиентов доходят через proxy protocol (лимит устройств работает) - Снаружи открыты только 443/tcp, 80, SSH, Shadowsocks и UDP-порты - Режим для новых установок с доверенным сертификатом; --multi-port — прежняя схема; старые многопортовые установки не переделываются автоматически Проверено: Mihomo по подписке 10/10 у двух пользователей, официальный AmneziaWG, матрица Xray/Mihomo/sing-box, панель и заглушка, пробы REALITY и MTProto без ключа.
This commit is contained in:
+12
-5
@@ -25,15 +25,21 @@
|
||||
|---|---|---|
|
||||
| VLESS REALITY | 443/tcp | всех приложениях |
|
||||
| Hysteria2 | 443/udp | всех приложениях |
|
||||
| VLESS XHTTP + REALITY | 8443/tcp | v2rayN, Happ, Clash Verge, FlClash |
|
||||
| VLESS WebSocket + TLS | 2053/tcp | всех приложениях |
|
||||
| Trojan gRPC + TLS | 2083/tcp | всех приложениях |
|
||||
| VMess WebSocket + TLS | 2087/tcp | всех приложениях |
|
||||
| VLESS XHTTP + REALITY | 443/tcp | v2rayN, Happ, Clash Verge, FlClash |
|
||||
| VLESS WebSocket + TLS | 443/tcp | всех приложениях |
|
||||
| Trojan gRPC + TLS | 443/tcp | всех приложениях |
|
||||
| VMess WebSocket + TLS | 443/tcp | всех приложениях |
|
||||
| Shadowsocks 2022 | 8388/tcp+udp | всех приложениях (мобильный МТС его блокирует, по Wi-Fi работает) |
|
||||
| TUIC v5 | 8444/udp | Hiddify, Clash Verge, FlClash, NekoBox |
|
||||
| AmneziaWG | 51821/udp | AmneziaVPN, AmneziaWG, Clash Verge, FlClash |
|
||||
| AmneziaWG 3.1 | 51822/udp | AmneziaVPN, AmneziaWG, Clash Verge, FlClash |
|
||||
| MTProto | 8445/tcp | прокси прямо в Telegram (если сервер сам достаёт до Telegram) |
|
||||
| MTProto | 443/tcp | прокси прямо в Telegram (если сервер сам достаёт до Telegram) |
|
||||
|
||||
**Все TCP-протоколы, панель и подписка работают через один порт 443.** Его слушает nginx:
|
||||
по имени сайта в запросе (SNI) он узнаёт REALITY, XHTTP и MTProto, остальное расшифровывает
|
||||
и разводит по секретным путям, а на всё прочее показывает сайт-заглушку. Снаружи у сервера
|
||||
открыты только 443, 80 (для продления сертификата), SSH и UDP-порты. Настоящие IP клиентов
|
||||
доходят до панели, поэтому лимит устройств работает.
|
||||
|
||||
Каждый протокол проверен настоящим подключением на ядрах Xray, Mihomo и sing-box,
|
||||
в том числе через интернет к серверу в России — стенд лежит в [tests/matrix](../tests/matrix/).
|
||||
@@ -109,6 +115,7 @@ kit user add sasha --gb 50 --days 30
|
||||
| `--cert fullchain.pem --key privkey.pem --host домен` | свой сертификат, например для домена |
|
||||
| `--sni www.samsung.com` | свой сайт для маскировки REALITY |
|
||||
| `--no-ufw` | не трогать файрвол |
|
||||
| `--multi-port` | прежняя схема: у каждого протокола свой порт, без nginx |
|
||||
|
||||
## Как устроена подписка
|
||||
|
||||
|
||||
+229
-21
@@ -30,6 +30,10 @@ ALL_PROTOS=(reality hy2 xhttp ws trojan vmess ss tuic wg awg awg3 mtproto)
|
||||
DEFAULT_PROTOS=(reality hy2 xhttp ws trojan vmess ss tuic awg awg3 mtproto)
|
||||
declare -A PORTS=([xhttp]=8443 [ws]=2053 [trojan]=2083 [vmess]=2087 [ss]=8388 [tuic]=8444 [wg]=51820 [awg]=51821 [awg3]=51822 [mtproto]=8445)
|
||||
PROTOS=(); CREATED=(); OPEN=()
|
||||
# Режим «всё TCP на 443»: nginx разводит по SNI и путям, подключения слушают только localhost.
|
||||
SINGLE=no
|
||||
declare -A INNER=([reality]=10443 [xhttp]=10444 [mtproto]=10445 [web]=10446 [ws]=10451 [vmess]=10452 [trojan]=10453 [sub]=10460)
|
||||
SNI2=""; SNI3=""
|
||||
|
||||
if [[ -t 1 ]]; then
|
||||
G=$'\e[32m'; Y=$'\e[33m'; R=$'\e[31m'; B=$'\e[1m'; D=$'\e[2m'; N=$'\e[0m'
|
||||
@@ -100,7 +104,7 @@ main() {
|
||||
die "3X-UI уже установлена другим способом — не трогаю её. Удалите её (x-ui uninstall) или добавьте REALITY в панели вручную."
|
||||
fi
|
||||
|
||||
local PORT=443 SNI="" PANEL_SSL=auto HOST="" UFW=yes NAME="admin" yes=no protos=all ucert="" ukey=""
|
||||
local PORT=443 SNI="" PANEL_SSL=auto HOST="" UFW=yes NAME="admin" yes=no protos=all ucert="" ukey="" multi=no
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--port) PORT=$2; shift 2 ;;
|
||||
@@ -110,6 +114,7 @@ main() {
|
||||
--user) NAME=$2; shift 2 ;;
|
||||
--protocols) protos=$2; shift 2 ;;
|
||||
--cert) ucert=$2; shift 2 ;;
|
||||
--multi-port) multi=yes; shift ;;
|
||||
--key) ukey=$2; shift 2 ;;
|
||||
--no-ufw) UFW=no; shift ;;
|
||||
-y|--yes) yes=yes; shift ;;
|
||||
@@ -132,7 +137,7 @@ main() {
|
||||
local x
|
||||
for x in "${PROTOS[@]}"; do [[ " ${ALL_PROTOS[*]} " == *" $x "* ]] || die "Неизвестный протокол: $x. Доступны: ${ALL_PROTOS[*]}"; done ;;
|
||||
esac
|
||||
if port_busy "$PORT" tcp && ! { [[ -f $XUI_ENV ]] && ss -H -ltnp "sport = :$PORT" | grep -q xray; }; then
|
||||
if port_busy "$PORT" tcp && ! { [[ -f $XUI_ENV ]] && ss -H -ltnp "sport = :$PORT" | grep -q -E 'xray|nginx'; }; then
|
||||
die "Порт $PORT/tcp уже занят. REALITY нужен свободный порт — укажите другой: --port 8443"
|
||||
fi
|
||||
|
||||
@@ -172,6 +177,13 @@ main() {
|
||||
die "$SNI не отвечает по TLS 1.3 + HTTP/2 — REALITY с ним работать не будет. Выберите другой сайт."
|
||||
fi
|
||||
say "Маскировка: ${B}$SNI${N}"
|
||||
# Для режима «всё на 443» XHTTP и MTProto нужны свои сайты: nginx различает их по SNI.
|
||||
for s in "${SNI_CANDIDATES[@]}"; do
|
||||
[[ $s == "$SNI" ]] && continue
|
||||
if [[ -z $SNI2 ]] && sni_ok "$s"; then SNI2=$s; continue; fi
|
||||
[[ -z $SNI3 && -n $SNI2 ]] && { SNI3=$s; break; }
|
||||
done
|
||||
SNI2=${SNI2:-$SNI}; SNI3=${SNI3:-www.cloudflare.com}
|
||||
|
||||
# --- официальный установщик 3X-UI с закреплённой версией ---
|
||||
local panel_port panel_path panel_user panel_pass tmp
|
||||
@@ -246,6 +258,15 @@ main() {
|
||||
# --- подключения: все выбранные протоколы, один subId на пользователя ---
|
||||
EXISTING=$(api GET inbounds/list)
|
||||
SUBID=""
|
||||
# «Всё на 443» — для новых установок с доверенным сертификатом. Старую многопортовую
|
||||
# установку не переделываем: перенос работающих подключений — осознанное решение.
|
||||
if [[ $TRUSTED == yes && $multi == no ]]; then
|
||||
if jq -e 'any(.[]; .remark == "REALITY" and (.listen // "") != "127.0.0.1")' <<<"$EXISTING" >/dev/null; then
|
||||
warn "Установка уже работает в режиме с отдельными портами — оставляю его."
|
||||
else
|
||||
SINGLE=yes
|
||||
fi
|
||||
fi
|
||||
local p
|
||||
for p in "${PROTOS[@]}"; do "proto_$p"; done
|
||||
# Первый пользователь — сразу на всех протоколах (как «kit user add»).
|
||||
@@ -253,6 +274,7 @@ main() {
|
||||
|
||||
# --- подписка: ссылки, Clash/Mihomo и JSON с автоопределением клиента ---
|
||||
setup_subscription
|
||||
[[ $SINGLE == yes ]] && setup_nginx
|
||||
install_kit_cli
|
||||
|
||||
# --- файрвол ---
|
||||
@@ -261,7 +283,7 @@ main() {
|
||||
ssh_port=$(ss -H -ltnp 2>/dev/null | awk '/sshd/ {sub(/.*:/,"",$4); print $4; exit}')
|
||||
ssh_port=${ssh_port:-22}
|
||||
OPEN+=("$ssh_port/tcp")
|
||||
[[ $TRUSTED == yes ]] && OPEN+=("$XUI_PANEL_PORT/tcp" "$SUB_PORT/tcp")
|
||||
[[ $TRUSTED == yes && $SINGLE == no ]] && OPEN+=("$XUI_PANEL_PORT/tcp" "$SUB_PORT/tcp")
|
||||
[[ $PANEL_SSL == ip ]] && OPEN+=("80/tcp")
|
||||
say "Настраиваю ufw: ${OPEN[*]}"
|
||||
local o
|
||||
@@ -271,7 +293,9 @@ main() {
|
||||
|
||||
# --- итог ---
|
||||
local panel_url links
|
||||
if [[ $TRUSTED == yes ]]; then
|
||||
if [[ $SINGLE == yes ]]; then
|
||||
panel_url="https://$HOST/${XUI_WEB_BASE_PATH#/}"
|
||||
elif [[ $TRUSTED == yes ]]; then
|
||||
panel_url="https://$HOST:$XUI_PANEL_PORT/$XUI_WEB_BASE_PATH"
|
||||
else
|
||||
panel_url="http://127.0.0.1:$XUI_PANEL_PORT/$XUI_WEB_BASE_PATH (через SSH-туннель: ssh -L $XUI_PANEL_PORT:127.0.0.1:$XUI_PANEL_PORT root@$HOST)"
|
||||
@@ -282,6 +306,8 @@ main() {
|
||||
extra=$(sub_links "$SUBID-awg" 1; sub_links "$SUBID-tg" 1)
|
||||
[[ -n $extra ]] && links+=$'\n'"$extra"
|
||||
AWG_LINKS=$(grep '^vpn://' <<<"$links" || true)
|
||||
# MTProto слушает localhost, а клиенты приходят через nginx на 443.
|
||||
[[ $SINGLE == yes ]] && links=$(sed "s/^\(tg:\/\/proxy?\)\(.*\)port=${INNER[mtproto]}/\1\2port=443/" <<<"$links")
|
||||
umask 077
|
||||
{
|
||||
echo "3X-UI $XUI_VERSION — данные для входа (файл виден только root)"
|
||||
@@ -373,20 +399,25 @@ client_base() { # суффикс
|
||||
uuid() { cat /proc/sys/kernel/random/uuid; }
|
||||
rnd() { shuf -i "$1-$2" -n 1; }
|
||||
|
||||
# add_inbound remark port proto(tcp|udp|both) protocol settings stream
|
||||
# add_inbound remark port proto(tcp|udp|both|inner) protocol settings stream
|
||||
# inner — подключение за nginx: слушает 127.0.0.1, наружу порт не открываем.
|
||||
add_inbound() {
|
||||
local remark=$1 port=$2 net=$3 protocol=$4 settings=$5 stream=$6 body
|
||||
local remark=$1 port=$2 net=$3 protocol=$4 settings=$5 stream=$6 body listen=""
|
||||
[[ $net == inner ]] && listen=127.0.0.1
|
||||
if jq -e --arg r "$remark" 'any(.[]; .remark == $r)' <<<"$EXISTING" >/dev/null; then
|
||||
CREATED+=("$remark"); open_port "$port" "$net"; return
|
||||
fi
|
||||
# Клиентов в подключение не кладём: пользователь добавляется потом сразу во все подключения.
|
||||
settings=$(jq -c 'if has("clients") then .clients = [] else . end' <<<"$settings")
|
||||
local n
|
||||
for n in ${net/both/tcp udp}; do
|
||||
local nets=$net
|
||||
[[ $net == both ]] && nets="tcp udp"
|
||||
[[ $net == inner ]] && nets=tcp
|
||||
for n in $nets; do
|
||||
if port_busy "$port" "$n"; then warn "$remark пропущен: порт $port/$n занят"; return; fi
|
||||
done
|
||||
body=$(jq -nc --arg rm "$remark" --argjson port "$port" --arg p "$protocol" --arg s "$settings" --arg st "$stream" '{
|
||||
remark: $rm, enable: true, listen: "", port: $port, protocol: $p, settings: $s, streamSettings: $st,
|
||||
body=$(jq -nc --arg rm "$remark" --argjson port "$port" --arg p "$protocol" --arg s "$settings" --arg st "$stream" --arg l "$listen" '{
|
||||
remark: $rm, enable: true, listen: $l, port: $port, protocol: $p, settings: $s, streamSettings: $st,
|
||||
sniffing: "{\"enabled\":true,\"destOverride\":[\"http\",\"tls\",\"quic\"],\"metadataOnly\":false,\"routeOnly\":false}",
|
||||
expiryTime: 0, total: 0}')
|
||||
local out
|
||||
@@ -402,11 +433,21 @@ add_inbound() {
|
||||
|
||||
open_port() { # port net
|
||||
case $2 in
|
||||
inner) ;;
|
||||
tcp|udp) OPEN+=("$1/$2") ;;
|
||||
both) OPEN+=("$1/tcp" "$1/udp") ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# External Proxy 3X-UI: ссылки ведут на HOST:443, хотя подключение слушает localhost.
|
||||
# SNI — только для TLS через nginx: у REALITY своё имя сайта маскировки, его не трогаем.
|
||||
ext_proxy() { # forceTls(same|tls) alpn(JSON)
|
||||
local sni=""
|
||||
[[ $HOST =~ ^[0-9.]+$ ]] || sni=$HOST
|
||||
jq -nc --arg f "$1" --arg h "$HOST" --arg sni "$sni" --argjson alpn "${2:-null}" '[{forceTls: $f, dest: $h, port: 443, remark: ""}
|
||||
+ (if $f == "tls" then {fingerprint: "chrome", alpn: $alpn} + (if $sni != "" then {sni: $sni} else {} end) else {} end)]'
|
||||
}
|
||||
|
||||
proto_reality() {
|
||||
local keys stream settings
|
||||
keys=$(api GET server/getNewX25519Cert)
|
||||
@@ -417,7 +458,12 @@ proto_reality() {
|
||||
minClientVer: "", maxClientVer: "", maxTimediff: 0, shortIds: [$sid],
|
||||
settings: {publicKey: $k.publicKey, fingerprint: "chrome", serverName: "", spiderX: "/"}},
|
||||
tcpSettings: {acceptProxyProtocol: false, header: {type: "none"}}}')
|
||||
add_inbound "REALITY" "$PORT" tcp vless "$settings" "$stream"
|
||||
if [[ $SINGLE == yes ]]; then
|
||||
stream=$(jq -c --argjson e "$(ext_proxy same)" '.externalProxy = $e | .tcpSettings.acceptProxyProtocol = true' <<<"$stream")
|
||||
add_inbound "REALITY" "${INNER[reality]}" inner vless "$settings" "$stream"
|
||||
else
|
||||
add_inbound "REALITY" "$PORT" tcp vless "$settings" "$stream"
|
||||
fi
|
||||
}
|
||||
|
||||
proto_xhttp() {
|
||||
@@ -428,28 +474,49 @@ proto_xhttp() {
|
||||
network: "xhttp", security: "reality", xhttpSettings: {path: $path, mode: "auto"},
|
||||
realitySettings: {target: ($sni + ":443"), serverNames: [$sni], privateKey: $k.privateKey, shortIds: [$sid],
|
||||
settings: {publicKey: $k.publicKey, fingerprint: "chrome", spiderX: "/"}}}')
|
||||
add_inbound "XHTTP" "${PORTS[xhttp]}" tcp vless "$settings" "$stream"
|
||||
if [[ $SINGLE == yes ]]; then
|
||||
stream=$(jq -c --arg sni "$SNI2" --argjson e "$(ext_proxy same)" '.realitySettings.target = ($sni + ":443") | .realitySettings.serverNames = [$sni]
|
||||
| .externalProxy = $e | .sockopt = {acceptProxyProtocol: true}' <<<"$stream")
|
||||
add_inbound "XHTTP" "${INNER[xhttp]}" inner vless "$settings" "$stream"
|
||||
else
|
||||
add_inbound "XHTTP" "${PORTS[xhttp]}" tcp vless "$settings" "$stream"
|
||||
fi
|
||||
}
|
||||
|
||||
proto_ws() {
|
||||
local settings stream
|
||||
settings=$(jq -nc --arg id "$(uuid)" --argjson c "$(client_base ws)" '{clients: [$c + {id: $id, flow: ""}], decryption: "none"}')
|
||||
stream=$(jq -nc --argjson t "$(tls_json '["http/1.1"]')" --arg path "/$(rand_str 10 | tr 'A-Z' 'a-z')" '{network: "ws", security: "tls", wsSettings: {path: $path}, tlsSettings: $t}')
|
||||
add_inbound "VLESS-WS" "${PORTS[ws]}" tcp vless "$settings" "$stream"
|
||||
if [[ $SINGLE == yes ]]; then
|
||||
stream=$(jq -c --argjson e "$(ext_proxy tls '["http/1.1"]')" '{network, wsSettings, security: "none", externalProxy: $e}' <<<"$stream")
|
||||
add_inbound "VLESS-WS" "${INNER[ws]}" inner vless "$settings" "$stream"
|
||||
else
|
||||
add_inbound "VLESS-WS" "${PORTS[ws]}" tcp vless "$settings" "$stream"
|
||||
fi
|
||||
}
|
||||
|
||||
proto_trojan() {
|
||||
local settings stream
|
||||
settings=$(jq -nc --arg pw "$(rand_str 16)" --argjson c "$(client_base trojan)" '{clients: [$c + {password: $pw}]}')
|
||||
stream=$(jq -nc --argjson t "$(tls_json '["h2"]')" --arg sn "$(rand_str 8 | tr 'A-Z' 'a-z')" '{network: "grpc", security: "tls", grpcSettings: {serviceName: $sn}, tlsSettings: $t}')
|
||||
add_inbound "Trojan-gRPC" "${PORTS[trojan]}" tcp trojan "$settings" "$stream"
|
||||
if [[ $SINGLE == yes ]]; then
|
||||
stream=$(jq -c --argjson e "$(ext_proxy tls '["h2"]')" '{network, grpcSettings, security: "none", externalProxy: $e}' <<<"$stream")
|
||||
add_inbound "Trojan-gRPC" "${INNER[trojan]}" inner trojan "$settings" "$stream"
|
||||
else
|
||||
add_inbound "Trojan-gRPC" "${PORTS[trojan]}" tcp trojan "$settings" "$stream"
|
||||
fi
|
||||
}
|
||||
|
||||
proto_vmess() {
|
||||
local settings stream
|
||||
settings=$(jq -nc --arg id "$(uuid)" --argjson c "$(client_base vmess)" '{clients: [$c + {id: $id, security: "auto", alterId: 0}]}')
|
||||
stream=$(jq -nc --argjson t "$(tls_json '["http/1.1"]')" --arg path "/$(rand_str 10 | tr 'A-Z' 'a-z')" '{network: "ws", security: "tls", wsSettings: {path: $path}, tlsSettings: $t}')
|
||||
add_inbound "VMess-WS" "${PORTS[vmess]}" tcp vmess "$settings" "$stream"
|
||||
if [[ $SINGLE == yes ]]; then
|
||||
stream=$(jq -c --argjson e "$(ext_proxy tls '["http/1.1"]')" '{network, wsSettings, security: "none", externalProxy: $e}' <<<"$stream")
|
||||
add_inbound "VMess-WS" "${INNER[vmess]}" inner vmess "$settings" "$stream"
|
||||
else
|
||||
add_inbound "VMess-WS" "${PORTS[vmess]}" tcp vmess "$settings" "$stream"
|
||||
fi
|
||||
}
|
||||
|
||||
proto_ss() {
|
||||
@@ -542,8 +609,14 @@ proto_mtproto() {
|
||||
return
|
||||
fi
|
||||
local settings
|
||||
settings=$(jq -nc --argjson c "$(client_base mtproto)" '{fakeTlsDomain: "www.cloudflare.com", clients: [$c + {secret: ""}]}')
|
||||
add_inbound "MTProto" "${PORTS[mtproto]}" tcp mtproto "$settings" '{}'
|
||||
if [[ $SINGLE == yes ]]; then
|
||||
# FakeTLS-домен — свой сайт: nginx узнаёт MTProto по нему и передаёт mtg с реальным IP клиента.
|
||||
settings=$(jq -nc --arg d "$SNI3" --argjson c "$(client_base mtproto)" '{fakeTlsDomain: $d, proxyProtocolListener: true, clients: [$c + {secret: ""}]}')
|
||||
add_inbound "MTProto" "${INNER[mtproto]}" inner mtproto "$settings" '{}'
|
||||
else
|
||||
settings=$(jq -nc --argjson c "$(client_base mtproto)" '{fakeTlsDomain: "www.cloudflare.com", clients: [$c + {secret: ""}]}')
|
||||
add_inbound "MTProto" "${PORTS[mtproto]}" tcp mtproto "$settings" '{}'
|
||||
fi
|
||||
}
|
||||
|
||||
# ---------- пользователи ----------
|
||||
@@ -608,6 +681,8 @@ install_kit_cli() {
|
||||
printf 'SUB_BASE=%q\n' "${SUB_URL%$SUBID}"
|
||||
printf 'SUB_PATH=%q\n' "$SUB_PATH"
|
||||
printf 'SUB_INTERNAL=%q\n' "${SUB_INTERNAL:-$SUB_PORT}"
|
||||
printf 'SINGLE=%q\n' "$SINGLE"
|
||||
printf 'MTPROTO_INNER=%q\n' "${INNER[mtproto]}"
|
||||
} >/etc/kit/kit.env
|
||||
chmod 600 /etc/kit/kit.env
|
||||
local d src=""
|
||||
@@ -618,6 +693,130 @@ install_kit_cli() {
|
||||
bash -n /usr/local/bin/kit || die "Команда kit скачалась повреждённой"
|
||||
}
|
||||
|
||||
# ---------- всё на 443: nginx ----------
|
||||
|
||||
setup_nginx() {
|
||||
say "Настраиваю nginx: всё TCP через порт 443"
|
||||
apt-get install -y -qq nginx libnginx-mod-stream >/dev/null
|
||||
# Порт 80 нужен Let's Encrypt для продления сертификата — сайт nginx по умолчанию убираем.
|
||||
rm -f /etc/nginx/sites-enabled/default
|
||||
|
||||
# Панель — только через nginx.
|
||||
local all
|
||||
all=$(api POST setting/all '{}')
|
||||
if [[ $(jq -r '.webListen' <<<"$all") != 127.0.0.1 ]]; then
|
||||
api POST setting/update "$(jq -c '.webListen = "127.0.0.1"' <<<"$all")" >/dev/null
|
||||
systemctl restart x-ui
|
||||
wait_panel
|
||||
fi
|
||||
|
||||
install -d -m 755 /var/www/kit
|
||||
[[ -f /var/www/kit/index.html ]] || cat >/var/www/kit/index.html <<'HTML'
|
||||
<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Welcome</title><style>body{font-family:-apple-system,Segoe UI,Roboto,sans-serif;margin:0;min-height:100vh;display:grid;place-items:center;background:#f6f7f9;color:#1f2328}main{text-align:center;padding:24px}h1{font-weight:600;font-size:28px}p{color:#57606a}</style></head><body><main><h1>Site is under construction</h1><p>Please check back soon.</p></main></body></html>
|
||||
HTML
|
||||
|
||||
# Маршруты — из текущих подключений панели: сайты REALITY, пути WebSocket, сервисы gRPC.
|
||||
local list reality_sni xhttp_sni mt_sni locs="" kind path port
|
||||
list=$(api GET inbounds/list)
|
||||
reality_sni=$(jq -r '.[] | select(.remark == "REALITY" and .listen == "127.0.0.1") | (.streamSettings | if type == "string" then fromjson else . end).realitySettings.serverNames[0]' <<<"$list")
|
||||
xhttp_sni=$(jq -r '.[] | select(.remark == "XHTTP" and .listen == "127.0.0.1") | (.streamSettings | if type == "string" then fromjson else . end).realitySettings.serverNames[0]' <<<"$list")
|
||||
mt_sni=$(jq -r '.[] | select(.protocol == "mtproto" and .listen == "127.0.0.1") | (.settings | if type == "string" then fromjson else . end).fakeTlsDomain' <<<"$list")
|
||||
while IFS=$'\t' read -r kind path port; do
|
||||
[[ -n $path ]] || continue
|
||||
if [[ $kind == ws ]]; then
|
||||
locs+="
|
||||
location = $path {
|
||||
proxy_pass http://127.0.0.1:$port;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade \$http_upgrade;
|
||||
proxy_set_header Connection \"upgrade\";
|
||||
proxy_set_header Host \$host;
|
||||
proxy_set_header X-Forwarded-For \$proxy_protocol_addr;
|
||||
proxy_read_timeout 1h;
|
||||
}"
|
||||
else
|
||||
locs+="
|
||||
location /$path/ {
|
||||
grpc_pass grpc://127.0.0.1:$port;
|
||||
grpc_set_header X-Real-IP \$proxy_protocol_addr;
|
||||
grpc_read_timeout 1h;
|
||||
grpc_send_timeout 1h;
|
||||
client_max_body_size 0;
|
||||
}"
|
||||
fi
|
||||
done < <(jq -r '.[] | select(.listen == "127.0.0.1") | (.streamSettings | if type == "string" then fromjson else . end) as $st
|
||||
| if $st.network == "ws" then ["ws", $st.wsSettings.path, .port]
|
||||
elif $st.network == "grpc" then ["grpc", $st.grpcSettings.serviceName, .port]
|
||||
else empty end | @tsv' <<<"$list")
|
||||
|
||||
local panel_path=/${XUI_WEB_BASE_PATH#/}
|
||||
panel_path=${panel_path%/}/
|
||||
{
|
||||
echo "# Сгенерировано 3x-ui.sh (3X-UI + Hysteria2 Kit) — перезаписывается при повторном запуске."
|
||||
echo "stream {"
|
||||
echo " map \$ssl_preread_server_name \$kit_upstream {"
|
||||
[[ -n $reality_sni ]] && echo " $reality_sni 127.0.0.1:${INNER[reality]};"
|
||||
[[ -n $xhttp_sni && $xhttp_sni != "$reality_sni" ]] && echo " $xhttp_sni 127.0.0.1:${INNER[xhttp]};"
|
||||
[[ -n $mt_sni ]] && echo " $mt_sni 127.0.0.1:${INNER[mtproto]};"
|
||||
echo " default 127.0.0.1:${INNER[web]};"
|
||||
echo " }"
|
||||
echo " server {"
|
||||
echo " listen 443;"
|
||||
echo " listen [::]:443;"
|
||||
echo " ssl_preread on;"
|
||||
echo " proxy_pass \$kit_upstream;"
|
||||
echo " proxy_protocol on;"
|
||||
echo " proxy_connect_timeout 10s;"
|
||||
echo " proxy_timeout 1h;"
|
||||
echo " }"
|
||||
echo "}"
|
||||
} >/etc/nginx/kit-stream.conf
|
||||
cat >/etc/nginx/conf.d/kit.conf <<NGX
|
||||
# Сгенерировано 3x-ui.sh (3X-UI + Hysteria2 Kit) — перезаписывается при повторном запуске.
|
||||
server {
|
||||
listen 127.0.0.1:${INNER[web]} ssl http2 proxy_protocol;
|
||||
server_name _;
|
||||
ssl_certificate $CERT;
|
||||
ssl_certificate_key $KEY;
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
set_real_ip_from 127.0.0.1;
|
||||
real_ip_header proxy_protocol;
|
||||
server_tokens off;
|
||||
access_log off;
|
||||
$locs
|
||||
location $SUB_PATH {
|
||||
proxy_pass http://127.0.0.1:${INNER[sub]};
|
||||
proxy_set_header Host \$host;
|
||||
}
|
||||
location $panel_path {
|
||||
proxy_pass https://127.0.0.1:$XUI_PANEL_PORT;
|
||||
proxy_ssl_verify off;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade \$http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host \$host;
|
||||
proxy_set_header X-Real-IP \$proxy_protocol_addr;
|
||||
proxy_set_header X-Forwarded-For \$proxy_protocol_addr;
|
||||
proxy_set_header X-Forwarded-Proto https;
|
||||
}
|
||||
location / {
|
||||
root /var/www/kit;
|
||||
index index.html;
|
||||
}
|
||||
}
|
||||
NGX
|
||||
grep -q 'kit-stream.conf' /etc/nginx/nginx.conf || echo 'include /etc/nginx/kit-stream.conf;' >>/etc/nginx/nginx.conf
|
||||
nginx -t >/tmp/nginx-test.log 2>&1 || { cat /tmp/nginx-test.log >&2; die "nginx не принял конфиг — лог выше."; }
|
||||
systemctl enable nginx >/dev/null 2>&1
|
||||
systemctl restart nginx
|
||||
# Let's Encrypt на IP продлевается каждые несколько дней — nginx раз в сутки перечитывает сертификат.
|
||||
echo '17 4 * * * root systemctl reload nginx >/dev/null 2>&1' >/etc/cron.d/kit-nginx-reload
|
||||
OPEN+=("443/tcp")
|
||||
local i
|
||||
for i in $(seq 1 10); do port_busy 443 tcp && return 0; sleep 1; done
|
||||
die "nginx не открыл порт 443."
|
||||
}
|
||||
|
||||
# ---------- подписка ----------
|
||||
|
||||
setup_subscription() {
|
||||
@@ -644,7 +843,8 @@ setup_subscription() {
|
||||
wait_panel
|
||||
fi
|
||||
[[ $TRUSTED == yes ]] && install_kit_sub
|
||||
if [[ $TRUSTED == yes ]]; then SUB_URL="https://$HOST:$SUB_PORT$SUB_PATH$SUBID"; else SUB_URL="http://127.0.0.1:$SUB_PORT$SUB_PATH$SUBID"; fi
|
||||
if [[ $SINGLE == yes ]]; then SUB_URL="https://$HOST$SUB_PATH$SUBID"
|
||||
elif [[ $TRUSTED == yes ]]; then SUB_URL="https://$HOST:$SUB_PORT$SUB_PATH$SUBID"; else SUB_URL="http://127.0.0.1:$SUB_PORT$SUB_PATH$SUBID"; fi
|
||||
SUB_FETCH="$(if [[ $TRUSTED == yes ]]; then echo https; else echo http; fi)://$HOST:$SUB_PORT$SUB_PATH$SUBID"
|
||||
}
|
||||
|
||||
@@ -662,9 +862,15 @@ install_kit_sub() {
|
||||
if [[ -n $src ]]; then install -m 644 "$src" /usr/local/lib/kit-sub/kit_sub.py
|
||||
else curl -fsSL --retry 3 -o /usr/local/lib/kit-sub/kit_sub.py "$KIT_SUB_URL"; fi
|
||||
python3 -c "import ast,sys; ast.parse(open(sys.argv[1]).read())" /usr/local/lib/kit-sub/kit_sub.py || die "kit-sub скачался повреждённым"
|
||||
jq -n --arg path "$SUB_PATH" --argjson port "$SUB_PORT" --arg up "http://127.0.0.1:$SUB_INTERNAL" \
|
||||
--arg cert "$CERT" --arg key "$KEY" --arg host "$HOST" \
|
||||
'{listen: "0.0.0.0", port: $port, path: $path, upstream: $up, cert: $cert, key: $key, host: $host}' >/etc/kit-sub/config.json
|
||||
if [[ $SINGLE == yes ]]; then
|
||||
# За nginx: слушаем только localhost, TLS снимает nginx на 443.
|
||||
jq -n --arg path "$SUB_PATH" --argjson port "${INNER[sub]}" --arg up "http://127.0.0.1:$SUB_INTERNAL" --arg host "$HOST" \
|
||||
'{listen: "127.0.0.1", port: $port, path: $path, upstream: $up, host: $host}' >/etc/kit-sub/config.json
|
||||
else
|
||||
jq -n --arg path "$SUB_PATH" --argjson port "$SUB_PORT" --arg up "http://127.0.0.1:$SUB_INTERNAL" \
|
||||
--arg cert "$CERT" --arg key "$KEY" --arg host "$HOST" \
|
||||
'{listen: "0.0.0.0", port: $port, path: $path, upstream: $up, cert: $cert, key: $key, host: $host}' >/etc/kit-sub/config.json
|
||||
fi
|
||||
chmod 600 /etc/kit-sub/config.json
|
||||
cat >/etc/systemd/system/kit-sub.service <<'UNIT'
|
||||
[Unit]
|
||||
@@ -693,7 +899,9 @@ UNIT
|
||||
systemctl enable kit-sub >/dev/null 2>&1
|
||||
systemctl restart kit-sub
|
||||
local i
|
||||
for i in $(seq 1 20); do port_busy "$SUB_PORT" tcp && return 0; sleep 1; done
|
||||
local kp=$SUB_PORT
|
||||
[[ $SINGLE == yes ]] && kp=${INNER[sub]}
|
||||
for i in $(seq 1 20); do port_busy "$kp" tcp && return 0; sleep 1; done
|
||||
journalctl -u kit-sub -n 20 --no-pager >&2 || true
|
||||
die "kit-sub не запустился — лог выше."
|
||||
}
|
||||
|
||||
+9
-2
@@ -130,8 +130,10 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
||||
|
||||
def setup(self):
|
||||
# TLS-рукопожатие — в потоке запроса, а не в общем цикле приёма соединений.
|
||||
# Без сертификата (за nginx, на 127.0.0.1) работаем по обычному HTTP.
|
||||
self.request.settimeout(self.timeout)
|
||||
self.request = self.server.ssl_ctx.wrap_socket(self.request, server_side=True)
|
||||
if self.server.ssl_ctx is not None:
|
||||
self.request = self.server.ssl_ctx.wrap_socket(self.request, server_side=True)
|
||||
super().setup()
|
||||
|
||||
def handle(self):
|
||||
@@ -208,7 +210,12 @@ class Server(http.server.ThreadingHTTPServer):
|
||||
|
||||
|
||||
def main():
|
||||
cert, key = CONF["cert"], CONF["key"]
|
||||
cert, key = CONF.get("cert"), CONF.get("key")
|
||||
if not cert:
|
||||
srv = Server((CONF.get("listen", "127.0.0.1"), int(CONF["port"])), Handler)
|
||||
log(f"kit-sub слушает http://{CONF.get('listen', '127.0.0.1')}:{CONF['port']}{PATH} (TLS снимает nginx)")
|
||||
srv.serve_forever()
|
||||
return
|
||||
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
|
||||
ctx.minimum_version = ssl.TLSVersion.TLSv1_2
|
||||
ctx.load_cert_chain(cert, key)
|
||||
|
||||
@@ -137,6 +137,7 @@ cmd_link() {
|
||||
grep -q '://' <<<"$raw" || raw=$(base64 -d <<<"$raw" 2>/dev/null || true)
|
||||
out+=$(grep -E '^(vpn|tg)://' <<<"$raw" || true)$'\n'
|
||||
done
|
||||
[[ ${SINGLE:-no} == yes ]] && out=$(sed "s/^\(tg:\/\/proxy?\)\(.*\)port=${MTPROTO_INNER:-10445}/\1\2port=443/" <<<"$out")
|
||||
echo; grep . <<<"$out" || echo "Отдельных ссылок нет."
|
||||
fi
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user