Правки по независимому аудиту: --restore (сертификат домена, xver при сбое, LINK_HOST), REALITY-2 при панели по домену, резервная копия базы через SQLite, ответы kit-sub как у nginx, меньше параллельность подбора сайта, перемешивание запасных сайтов, нейтральные тексты
This commit is contained in:
@@ -1 +1,3 @@
|
|||||||
|
*.pyc
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
__pycache__/
|
||||||
|
|||||||
+1
-1
@@ -5,7 +5,7 @@
|
|||||||
* Панель 3X-UI обновлена до v3.9.0, а ядро Xray осталось проверенным: новое не всегда лучше, это показал тест. `kit update --panel` обновит панель без смены ядра и настроек, откатит всё при сбое и не спросит лишнего.
|
* Панель 3X-UI обновлена до v3.9.0, а ядро Xray осталось проверенным: новое не всегда лучше, это показал тест. `kit update --panel` обновит панель без смены ядра и настроек, откатит всё при сбое и не спросит лишнего.
|
||||||
* `kit` без аргументов открывает меню, как у x-ui: нажали цифру – готово. Всё про сеть собрано в `kit net`: порты, протоколы, сайт маскировки, отпечаток. Команды остались для скриптов, команд стало меньше, а умеет `kit` больше.
|
* `kit` без аргументов открывает меню, как у x-ui: нажали цифру – готово. Всё про сеть собрано в `kit net`: порты, протоколы, сайт маскировки, отпечаток. Команды остались для скриптов, команд стало меньше, а умеет `kit` больше.
|
||||||
* Установка теперь по шагам, а все ссылки разложены по полочкам: основные и запасные. Раньше они сыпались потоком, как лента новостей.
|
* Установка теперь по шагам, а все ссылки разложены по полочкам: основные и запасные. Раньше они сыпались потоком, как лента новостей.
|
||||||
* Сайт-прикрытие подбирается среди соседей по подсети, а не «у всех один». Свой домен по-прежнему лучший вариант. REALITY теперь с `xtls-rprx-vision`, Hysteria2 отвечает на чужой запрос страницей сайта.
|
* Сайт для маскировки подбирается среди соседей по подсети, а не «у всех один». Свой домен по-прежнему лучший вариант. REALITY теперь с `xtls-rprx-vision`, Hysteria2 отвечает на чужой запрос страницей сайта.
|
||||||
* `kit check --deep` подключается к серверу настоящим клиентом, `--fix` лечит безопасное. Общий лимит трафика считается по всем подключениям пользователя, а в подписке Clash есть «Авто»: клиент сам выбирает быстрейший протокол. DNS в подписках теперь по DoH через прокси: запросы имён не гуляют где попало.
|
* `kit check --deep` подключается к серверу настоящим клиентом, `--fix` лечит безопасное. Общий лимит трафика считается по всем подключениям пользователя, а в подписке Clash есть «Авто»: клиент сам выбирает быстрейший протокол. DNS в подписках теперь по DoH через прокси: запросы имён не гуляют где попало.
|
||||||
* Со своим доменом панель и подписку можно открывать и по домену: `--panel-on domain` при установке или `kit net panel domain` потом. Вернулся `--restore`: чистый сервер поднимается из `kit backup`, как из резервной копии настроек телефона. Ссылки `kit user link --amnezia` и `--telegram` для AmneziaVPN и Telegram теперь одной командой, а исправление времени на Debian 13 прислал [xlok1e](https://github.com/xlok1e), спасибо.
|
* Со своим доменом панель и подписку можно открывать и по домену: `--panel-on domain` при установке или `kit net panel domain` потом. Вернулся `--restore`: чистый сервер поднимается из `kit backup`, как из резервной копии настроек телефона. Ссылки `kit user link --amnezia` и `--telegram` для AmneziaVPN и Telegram теперь одной командой, а исправление времени на Debian 13 прислал [xlok1e](https://github.com/xlok1e), спасибо.
|
||||||
|
|
||||||
|
|||||||
@@ -48,8 +48,8 @@
|
|||||||
сразу во все протоколы с общим лимитом трафика, сроком и числом устройств.
|
сразу во все протоколы с общим лимитом трафика, сроком и числом устройств.
|
||||||
- 🧭 **Меню `kit`, как у x-ui.** Пользователи, протоколы, порты, сайт маскировки, проверка
|
- 🧭 **Меню `kit`, как у x-ui.** Пользователи, протоколы, порты, сайт маскировки, проверка
|
||||||
и обновление – цифрой, без запоминания команд.
|
и обновление – цифрой, без запоминания команд.
|
||||||
- 🕵️ **Сайт-прикрытие подбирается среди соседей по подсети**, а со своим доменом сервер изображает
|
- 🌐 **Сайт для маскировки подбирается среди соседей по подсети**, а со своим доменом на сервере
|
||||||
ваш собственный сайт.
|
стоит ваш собственный сайт.
|
||||||
- 🔄 **Обновляется сам, но только подписанными релизами**, а `kit backup` сохраняет
|
- 🔄 **Обновляется сам, но только подписанными релизами**, а `kit backup` сохраняет
|
||||||
копию сервера, которую `--restore` поднимает на чистом VPS.
|
копию сервера, которую `--restore` поднимает на чистом VPS.
|
||||||
- 🔒 **Сертификат Let's Encrypt на IP** выпускается и продлевается сам, панель скрыта на
|
- 🔒 **Сертификат Let's Encrypt на IP** выпускается и продлевается сам, панель скрыта на
|
||||||
@@ -278,11 +278,11 @@ sh -c "$(curl -sSL https://raw.githubusercontent.com/jameszeroX/XKeen/main/insta
|
|||||||
2. WireGuard и AmneziaWG приложение разрешает по обычному UDP внутри туннеля, поэтому в «Авто» они не входят (выбираются вручную).
|
2. WireGuard и AmneziaWG приложение разрешает по обычному UDP внутри туннеля, поэтому в «Авто» они не входят (выбираются вручную).
|
||||||
3. Проверьте на [dnsleaktest.com](https://dnsleaktest.com) или [ipleak.net](https://ipleak.net): DNS вашего провайдера там появляться не должен.
|
3. Проверьте на [dnsleaktest.com](https://dnsleaktest.com) или [ipleak.net](https://ipleak.net): DNS вашего провайдера там появляться не должен.
|
||||||
|
|
||||||
## Если связь пропала
|
## Диагностика и перенос сервера
|
||||||
|
|
||||||
1. `kit check --deep` – проверит сервер и подключится к нему клиентом: так видно, виноват сервер или сеть.
|
1. `kit check --deep` – проверит сервер и подключится к нему клиентом: так видно, виноват сервер или сеть.
|
||||||
2. `kit net fp firefox` – другой отпечаток клиента, иногда помогает одной командой (вернуть: `kit net fp chrome`).
|
2. `kit net fp firefox` – другой отпечаток клиента одной командой (вернуть: `kit net fp chrome`).
|
||||||
3. `kit net site` – новый сайт-прикрытие; `kit net on reality2` – дополнительный REALITY на другом порту.
|
3. `kit net site` – другой сайт для маскировки; `kit net on reality2` – дополнительный REALITY на другом порту.
|
||||||
Один протокол не подключается – попробуйте соседний: у каждого свои сильные стороны (Hysteria2 работает по UDP, остальные по TCP). В подписке Clash «Авто» выбирает сама, в остальных приложениях – вручную.
|
Один протокол не подключается – попробуйте соседний: у каждого свои сильные стороны (Hysteria2 работает по UDP, остальные по TCP). В подписке Clash «Авто» выбирает сама, в остальных приложениях – вручную.
|
||||||
4. Не помогло – перенесите сервер на другой адрес: `kit backup`, на новом сервере `bash 3x-ui.sh --restore файл`.
|
4. Не помогло – перенесите сервер на другой адрес: `kit backup`, на новом сервере `bash 3x-ui.sh --restore файл`.
|
||||||
Если подписка выдавалась на домене, а не на IP, клиентам достаточно сменить адрес в DNS.
|
Если подписка выдавалась на домене, а не на IP, клиентам достаточно сменить адрес в DNS.
|
||||||
|
|||||||
+2
-2
@@ -186,8 +186,8 @@ kit net panel domain # панель и подписка открываю
|
|||||||
или ссылки, которая не зависит от IP), выберите «по домену» при установке (`--panel-on domain`) или позже: `kit net panel domain`. Путь у них секретный и по домену, как и по IP.
|
или ссылки, которая не зависит от IP), выберите «по домену» при установке (`--panel-on domain`) или позже: `kit net panel domain`. Путь у них секретный и по домену, как и по IP.
|
||||||
|
|
||||||
Лучший сайт маскировки – ваш собственный домен (`--domain` при установке): сервер изображает ваш сайт, а сертификат
|
Лучший сайт маскировки – ваш собственный домен (`--domain` при установке): сервер изображает ваш сайт, а сертификат
|
||||||
и имя совпадают. Без домена установщик ищет сайт среди соседей по подсети сервера: он естественнее, чем известный
|
и имя совпадают. Без домена установщик подбирает сайт среди соседей по подсети сервера: около 250 коротких обращений к порту 443
|
||||||
сайт на чужом IP. Основные протоколы – REALITY, XHTTP и Hysteria2; остальные запасные, их можно выключить командой
|
соседних адресов (не нужно – укажите `--sni сайт` или `--domain`). Основные протоколы – REALITY, XHTTP и Hysteria2; остальные запасные, их можно выключить командой
|
||||||
`kit net off`.
|
`kit net off`.
|
||||||
|
|
||||||
## Резервная копия
|
## Резервная копия
|
||||||
|
|||||||
+24
-13
@@ -58,7 +58,7 @@ ALL_PROTOS=(reality reality2 hy2 xhttp ws trojan vmess ss tuic wg awg awg3 mtpro
|
|||||||
# (проверено 2026-09-27: рукопожатие доходит до сервера, ответ – нет). По умолчанию не ставим.
|
# (проверено 2026-09-27: рукопожатие доходит до сервера, ответ – нет). По умолчанию не ставим.
|
||||||
DEFAULT_PROTOS=(reality hy2 xhttp ws trojan vmess ss tuic 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)
|
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=()
|
PROTOS=(); CREATED=(); OPEN=(); NB_ALL=()
|
||||||
# Режим «всё TCP на 443»: nginx разводит по SNI и путям, подключения слушают только localhost.
|
# Режим «всё TCP на 443»: nginx разводит по SNI и путям, подключения слушают только localhost.
|
||||||
SINGLE=no
|
SINGLE=no
|
||||||
declare -A INNER=([reality]=10443 [xhttp]=10444 [mtproto]=10445 [web]=10446 [selfweb]=10447 [ws]=10451 [vmess]=10452 [trojan]=10453 [sub]=10460)
|
declare -A INNER=([reality]=10443 [xhttp]=10444 [mtproto]=10445 [web]=10446 [selfweb]=10447 [ws]=10451 [vmess]=10452 [trojan]=10453 [sub]=10460)
|
||||||
@@ -119,7 +119,7 @@ SNI_RE='^[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?(\.[A-Za-z0-9]([A-Za-z0-9-]*[A-Za
|
|||||||
|
|
||||||
# Бесплатные и динамические имена (sslip.io, work.gd и т. п.) часто используют чужие прокси-серверы: под них маскироваться не стоит.
|
# Бесплатные и динамические имена (sslip.io, work.gd и т. п.) часто используют чужие прокси-серверы: под них маскироваться не стоит.
|
||||||
SNI_DYN_RE='(^|\.)(sslip\.io|nip\.io|xip\.io|traefik\.me|work\.gd|duckdns\.org|ddns\.net|hopto\.org|zapto\.org|myftp\.biz|dynu\.net|freeddns\.org|no-ip\.(org|biz|info)|nom\.za|tk|ml|ga|cf|gq)$'
|
SNI_DYN_RE='(^|\.)(sslip\.io|nip\.io|xip\.io|traefik\.me|work\.gd|duckdns\.org|ddns\.net|hopto\.org|zapto\.org|myftp\.biz|dynu\.net|freeddns\.org|no-ip\.(org|biz|info)|nom\.za|tk|ml|ga|cf|gq)$'
|
||||||
# Известные сайты на чужом IP не подходят: сайт-прикрытие должен быть «своим» для подсети сервера.
|
# Известные сайты на чужом IP не подходят: сайт для маскировки должен быть «своим» для подсети сервера.
|
||||||
SNI_BRAND_RE='(^|\.)(google|googleapis|gstatic|youtube|microsoft|windows|apple|icloud|amazon|amazonaws|samsung|yahoo|cloudflare|facebook|instagram|netflix|github|telegram)\.[a-z.]+$'
|
SNI_BRAND_RE='(^|\.)(google|googleapis|gstatic|youtube|microsoft|windows|apple|icloud|amazon|amazonaws|samsung|yahoo|cloudflare|facebook|instagram|netflix|github|telegram)\.[a-z.]+$'
|
||||||
|
|
||||||
# Имена с «сомнительными» словами не берём: брать такой сайт для маскировки неприятно и небезопасно для вас.
|
# Имена с «сомнительными» словами не берём: брать такой сайт для маскировки неприятно и небезопасно для вас.
|
||||||
@@ -164,7 +164,7 @@ same_net() { # имя
|
|||||||
# Имена из сертификатов соседних адресов /24: заходим на 443 без имени и читаем subjectAltName.
|
# Имена из сертификатов соседних адресов /24: заходим на 443 без имени и читаем subjectAltName.
|
||||||
nearby_scan() { # основа "a.b.c"
|
nearby_scan() { # основа "a.b.c"
|
||||||
local me; me=$(host_ip)
|
local me; me=$(host_ip)
|
||||||
seq 1 254 | xargs -P 24 -I{} bash -c '
|
seq 1 254 | xargs -P 8 -I{} bash -c '
|
||||||
ip=$1; [ "$ip" = "$2" ] && exit 0
|
ip=$1; [ "$ip" = "$2" ] && exit 0
|
||||||
echo | timeout 4 openssl s_client -connect "$ip:443" -tls1_3 -alpn h2 2>/dev/null | openssl x509 -noout -ext subjectAltName 2>/dev/null \
|
echo | timeout 4 openssl s_client -connect "$ip:443" -tls1_3 -alpn h2 2>/dev/null | openssl x509 -noout -ext subjectAltName 2>/dev/null \
|
||||||
| tr "," "\n" | sed -n "s/^ *DNS://p"' _ "$1.{}" "$me" 2>/dev/null | sort -u | grep -E "$SNI_RE" | head -40 || true
|
| tr "," "\n" | sed -n "s/^ *DNS://p"' _ "$1.{}" "$me" 2>/dev/null | sort -u | grep -E "$SNI_RE" | head -40 || true
|
||||||
@@ -541,9 +541,10 @@ main() {
|
|||||||
SNI=$DOMAIN
|
SNI=$DOMAIN
|
||||||
elif [[ -z $SNI ]]; then
|
elif [[ -z $SNI ]]; then
|
||||||
echo
|
echo
|
||||||
echo "Ищу сайт-прикрытие в подсети сервера… (около минуты)"
|
echo "Подбираю сайт для маскировки среди соседей по подсети… (около 250 коротких обращений к порту 443 соседних адресов, около минуты; не нужно – запустите с --sni или --domain)"
|
||||||
local -a nb=()
|
local -a nb=()
|
||||||
mapfile -t nb < <(nearby_sites | shuf)
|
mapfile -t nb < <(nearby_sites | shuf)
|
||||||
|
NB_ALL=("${nb[@]}")
|
||||||
if ((${#nb[@]})); then
|
if ((${#nb[@]})); then
|
||||||
SNI=${nb[0]}; SNI2=${nb[1]:-}; SNI3=${nb[2]:-}
|
SNI=${nb[0]}; SNI2=${nb[1]:-}; SNI3=${nb[2]:-}
|
||||||
ok "найден: ${B}$SNI${N} · та же подсеть · TLS 1.3, h2, сертификат верный"
|
ok "найден: ${B}$SNI${N} · та же подсеть · TLS 1.3, h2, сертификат верный"
|
||||||
@@ -554,7 +555,7 @@ main() {
|
|||||||
ask_tty " Продолжить с запасным сайтом из списка? [y/N] " || true
|
ask_tty " Продолжить с запасным сайтом из списка? [y/N] " || true
|
||||||
[[ $REPLY =~ ^[yYдД]$ ]] || die "Остановился по вашей просьбе. Ставить можно снова в любой момент."
|
[[ $REPLY =~ ^[yYдД]$ ]] || die "Остановился по вашей просьбе. Ставить можно снова в любой момент."
|
||||||
else
|
else
|
||||||
later "Сайт-прикрытие взят из общего списка (в подсети сервера подходящего не нашлось). Надёжнее свой домен или kit net site."
|
later "Сайт для маскировки взят из общего списка (в подсети сервера подходящего не нашлось). Надёжнее свой домен или kit net site."
|
||||||
fi
|
fi
|
||||||
local -a pool=()
|
local -a pool=()
|
||||||
mapfile -t pool < <(printf '%s\n' "${SNI_CANDIDATES[@]}" | shuf)
|
mapfile -t pool < <(printf '%s\n' "${SNI_CANDIDATES[@]}" | shuf)
|
||||||
@@ -573,15 +574,21 @@ main() {
|
|||||||
if [[ $TRUSTED == yes && $multi == no && ( -z $SNI2 || -z $SNI3 ) ]]; then
|
if [[ $TRUSTED == yes && $multi == no && ( -z $SNI2 || -z $SNI3 ) ]]; then
|
||||||
local -a nb2=()
|
local -a nb2=()
|
||||||
echo
|
echo
|
||||||
echo "Подбираю сайты для XHTTP и MTProto… (около минуты)"
|
if ((${#NB_ALL[@]})); then
|
||||||
mapfile -t nb2 < <(nearby_sites | shuf)
|
nb2=("${NB_ALL[@]}")
|
||||||
|
else
|
||||||
|
echo "Подбираю сайты для XHTTP и MTProto… (около 250 коротких обращений к порту 443 соседних адресов вашей подсети)"
|
||||||
|
mapfile -t nb2 < <(nearby_sites | shuf)
|
||||||
|
fi
|
||||||
for s in "${nb2[@]}"; do
|
for s in "${nb2[@]}"; do
|
||||||
[[ $s == "$SNI" || $s == "$SNI2" || $s == "$SNI3" ]] && continue
|
[[ $s == "$SNI" || $s == "$SNI2" || $s == "$SNI3" ]] && continue
|
||||||
if [[ -z $SNI2 ]]; then SNI2=$s; elif [[ -z $SNI3 ]]; then SNI3=$s; fi
|
if [[ -z $SNI2 ]]; then SNI2=$s; elif [[ -z $SNI3 ]]; then SNI3=$s; fi
|
||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
# Для режима «всё на 443» XHTTP и MTProto нужны свои сайты: nginx различает их по SNI.
|
# Для режима «всё на 443» XHTTP и MTProto нужны свои сайты: nginx различает их по SNI.
|
||||||
for s in "${SNI_CANDIDATES[@]}"; do
|
local -a pool2=()
|
||||||
|
mapfile -t pool2 < <(printf '%s\n' "${SNI_CANDIDATES[@]}" | shuf)
|
||||||
|
for s in "${pool2[@]}"; do
|
||||||
[[ $s == "$SNI" || $s == "$SNI2" || $s == "$SNI3" ]] && continue
|
[[ $s == "$SNI" || $s == "$SNI2" || $s == "$SNI3" ]] && continue
|
||||||
if [[ -z $SNI2 ]] && sni_ok "$s"; then SNI2=$s; continue; fi
|
if [[ -z $SNI2 ]] && sni_ok "$s"; then SNI2=$s; continue; fi
|
||||||
[[ -z $SNI3 && -n $SNI2 ]] && { SNI3=$s; break; }
|
[[ -z $SNI3 && -n $SNI2 ]] && { SNI3=$s; break; }
|
||||||
@@ -1487,8 +1494,10 @@ server {
|
|||||||
NGX
|
NGX
|
||||||
fi
|
fi
|
||||||
grep -q 'kit-stream.conf' /etc/nginx/nginx.conf || echo 'include /etc/nginx/kit-stream.conf;' >>/etc/nginx/nginx.conf
|
grep -q 'kit-stream.conf' /etc/nginx/nginx.conf || echo 'include /etc/nginx/kit-stream.conf;' >>/etc/nginx/nginx.conf
|
||||||
if ! nginx -t >/tmp/nginx-test.log 2>&1; then
|
local ngx_log
|
||||||
cat /tmp/nginx-test.log >&2
|
ngx_log=$(mktemp)
|
||||||
|
if ! nginx -t >"$ngx_log" 2>&1; then
|
||||||
|
cat "$ngx_log" >&2; rm -f "$ngx_log"
|
||||||
# Наш include убираем, чтобы не оставить чужой nginx сломанным.
|
# Наш include убираем, чтобы не оставить чужой nginx сломанным.
|
||||||
sed -i '/kit-stream\.conf/d' /etc/nginx/nginx.conf
|
sed -i '/kit-stream\.conf/d' /etc/nginx/nginx.conf
|
||||||
rm -f /etc/nginx/kit-stream.conf
|
rm -f /etc/nginx/kit-stream.conf
|
||||||
@@ -1640,7 +1649,7 @@ sub_links() {
|
|||||||
# ИМЯ=значение без подстановок и команд.
|
# ИМЯ=значение без подстановок и команд.
|
||||||
safe_env() {
|
safe_env() {
|
||||||
[[ -r $1 ]] || die "В копии нет файла ${1##*/}."
|
[[ -r $1 ]] || die "В копии нет файла ${1##*/}."
|
||||||
grep -qvE "^([A-Z][A-Z0-9_]*=([A-Za-z0-9._:/@%+,=-]*|'[^']*'))?$" "$1" && die "В копии подозрительный файл ${1##*/} – не восстанавливаю."
|
grep -qvE "^([A-Z][A-Z0-9_]*=([A-Za-z0-9._:/@%+,=_-]*|'[^']*'))?$" "$1" && die "В копии подозрительный файл ${1##*/} – не восстанавливаю."
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1756,6 +1765,8 @@ PY
|
|||||||
if [[ -f $tmp/etc/kit/kit.env ]]; then
|
if [[ -f $tmp/etc/kit/kit.env ]]; then
|
||||||
SINGLE=$(. "$tmp/etc/kit/kit.env"; echo "${SINGLE:-no}")
|
SINGLE=$(. "$tmp/etc/kit/kit.env"; echo "${SINGLE:-no}")
|
||||||
SUB_PATH=$(. "$tmp/etc/kit/kit.env"; echo "${SUB_PATH:-}")
|
SUB_PATH=$(. "$tmp/etc/kit/kit.env"; echo "${SUB_PATH:-}")
|
||||||
|
LINK_HOST=$(. "$tmp/etc/kit/kit.env"; echo "${LINK_HOST:-}")
|
||||||
|
[[ $LINK_HOST =~ ^[A-Za-z0-9.:-]*$ ]] || LINK_HOST=""
|
||||||
SUB_INTERNAL=$(. "$tmp/etc/kit/kit.env"; echo "${SUB_INTERNAL:-}")
|
SUB_INTERNAL=$(. "$tmp/etc/kit/kit.env"; echo "${SUB_INTERNAL:-}")
|
||||||
[[ $SINGLE =~ ^(yes|no)$ && $SUB_PATH =~ ^/[A-Za-z0-9_-]+/$ && $SUB_INTERNAL =~ ^[0-9]{1,5}$ ]] \
|
[[ $SINGLE =~ ^(yes|no)$ && $SUB_PATH =~ ^/[A-Za-z0-9_-]+/$ && $SUB_INTERNAL =~ ^[0-9]{1,5}$ ]] \
|
||||||
|| die "В копии странные настройки kit (kit.env) – не восстанавливаю."
|
|| die "В копии странные настройки kit (kit.env) – не восстанавливаю."
|
||||||
@@ -1804,7 +1815,7 @@ PY
|
|||||||
fi
|
fi
|
||||||
# Всё на 443: nginx строит маршруты по подключениям из базы, заглушка – из копии.
|
# Всё на 443: nginx строит маршруты по подключениям из базы, заглушка – из копии.
|
||||||
if [[ $SINGLE == yes ]]; then
|
if [[ $SINGLE == yes ]]; then
|
||||||
[[ -n $DOMAIN ]] && { issue_domain_cert || domain_cert_fallback; }
|
[[ -n $DOMAIN ]] && { issue_domain_cert || die "Не удалось получить сертификат для $DOMAIN: порт 80 должен быть свободен, а A-запись домена указывать на этот сервер. Лог: /var/log/kit-domain-cert.log"; }
|
||||||
install -d -m 755 /var/www/kit
|
install -d -m 755 /var/www/kit
|
||||||
[[ -f $tmp/var/www/kit/index.html ]] && install -m 644 "$tmp/var/www/kit/index.html" /var/www/kit/index.html
|
[[ -f $tmp/var/www/kit/index.html ]] && install -m 644 "$tmp/var/www/kit/index.html" /var/www/kit/index.html
|
||||||
setup_nginx
|
setup_nginx
|
||||||
@@ -1815,7 +1826,7 @@ PY
|
|||||||
# Панель по домену: nginx собран заново, значит блок домена нужно вернуть (домен должен указывать на этот сервер).
|
# Панель по домену: nginx собран заново, значит блок домена нужно вернуть (домен должен указывать на этот сервер).
|
||||||
if grep -q '^PANEL_ON=domain' /etc/kit/kit.env; then
|
if grep -q '^PANEL_ON=domain' /etc/kit/kit.env; then
|
||||||
sed -i 's/^PANEL_ON=.*/PANEL_ON=ip/' /etc/kit/kit.env
|
sed -i 's/^PANEL_ON=.*/PANEL_ON=ip/' /etc/kit/kit.env
|
||||||
/usr/local/bin/kit net panel domain >/dev/null 2>&1 || warn "Панель по домену не включилась (домен должен указывать на этот сервер): kit net panel domain"
|
/usr/local/bin/kit net panel domain >/dev/null 2>&1 || { /usr/local/bin/kit __xver 0 >/dev/null 2>&1 || true; warn "Панель по домену не включилась (домен должен указывать на этот сервер): kit net panel domain"; }
|
||||||
fi
|
fi
|
||||||
brand_xui_menu
|
brand_xui_menu
|
||||||
/usr/local/bin/kit update --auto >/dev/null 2>&1 || later "Автообновление не включилось – включите позже: kit update --auto"
|
/usr/local/bin/kit update --auto >/dev/null 2>&1 || later "Автообновление не включилось – включите позже: kit update --auto"
|
||||||
|
|||||||
Binary file not shown.
+18
-7
@@ -157,7 +157,7 @@ def add_auto(clash_yaml):
|
|||||||
"interval": 300, "tolerance": 100, "lazy": True, "proxies": auto_names})
|
"interval": 300, "tolerance": 100, "lazy": True, "proxies": auto_names})
|
||||||
for g in groups[1:]:
|
for g in groups[1:]:
|
||||||
lst = g.get("proxies") if isinstance(g, dict) else None
|
lst = g.get("proxies") if isinstance(g, dict) else None
|
||||||
if g.get("type") == "select" and isinstance(lst, list) and any(x in names for x in lst):
|
if isinstance(g, dict) and g.get("type") == "select" and isinstance(lst, list) and any(x in names for x in lst):
|
||||||
g["proxies"] = [AUTO_GROUP] + lst
|
g["proxies"] = [AUTO_GROUP] + lst
|
||||||
break
|
break
|
||||||
return yaml.safe_dump(cfg, allow_unicode=True, sort_keys=False).encode()
|
return yaml.safe_dump(cfg, allow_unicode=True, sort_keys=False).encode()
|
||||||
@@ -221,12 +221,23 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
|||||||
self.wfile.write(body)
|
self.wfile.write(body)
|
||||||
|
|
||||||
def send_error(self, code, message=None, explain=None):
|
def send_error(self, code, message=None, explain=None):
|
||||||
# Свои короткие ответы вместо страницы ошибок Python: сканеру не видно, чем отвечает сервер.
|
# Свои ответы вместо страницы ошибок Python, в том же виде, что у обычного nginx.
|
||||||
self.close_connection = True
|
self.close_connection = True
|
||||||
self.send_plain(code, f"{code} {self.responses.get(code, ('error',))[0].lower()}")
|
self.send_html_error(code)
|
||||||
|
|
||||||
|
def send_html_error(self, code):
|
||||||
|
phrase = self.responses.get(code, ("Error",))[0]
|
||||||
|
body = (f"<html>\r\n<head><title>{code} {phrase}</title></head>\r\n<body>\r\n<center><h1>{code} {phrase}</h1></center>\r\n"
|
||||||
|
"<hr><center>nginx</center>\r\n</body>\r\n</html>\r\n").encode()
|
||||||
|
self.send_response(code)
|
||||||
|
self.send_header("Content-Type", "text/html")
|
||||||
|
self.send_header("Content-Length", str(len(body)))
|
||||||
|
self.end_headers()
|
||||||
|
if self.command != "HEAD":
|
||||||
|
self.wfile.write(body)
|
||||||
|
|
||||||
def do_other(self):
|
def do_other(self):
|
||||||
self.send_plain(404, "404 page not found")
|
self.send_html_error(404)
|
||||||
|
|
||||||
do_POST = do_PUT = do_DELETE = do_PATCH = do_OPTIONS = do_other
|
do_POST = do_PUT = do_DELETE = do_PATCH = do_OPTIONS = do_other
|
||||||
|
|
||||||
@@ -236,16 +247,16 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
|||||||
def do_GET(self):
|
def do_GET(self):
|
||||||
path = self.path.split("?", 1)[0]
|
path = self.path.split("?", 1)[0]
|
||||||
if not path.startswith(PATH):
|
if not path.startswith(PATH):
|
||||||
return self.send_plain(404, "404 page not found")
|
return self.send_html_error(404)
|
||||||
sub_id = path[len(PATH):]
|
sub_id = path[len(PATH):]
|
||||||
if not SUB_ID.match(sub_id):
|
if not SUB_ID.match(sub_id):
|
||||||
return self.send_plain(404, "404 page not found")
|
return self.send_html_error(404)
|
||||||
ua = self.headers.get("User-Agent", "")
|
ua = self.headers.get("User-Agent", "")
|
||||||
host = CONF.get("link_host") or self.headers.get("Host", CONF.get("host", ""))
|
host = CONF.get("link_host") or self.headers.get("Host", CONF.get("host", ""))
|
||||||
accept = self.headers.get("Accept", "")
|
accept = self.headers.get("Accept", "")
|
||||||
code, headers, body = upstream(sub_id, ua, host, accept)
|
code, headers, body = upstream(sub_id, ua, host, accept)
|
||||||
if code is None:
|
if code is None:
|
||||||
return self.send_plain(502, "subscription backend is unavailable")
|
return self.send_html_error(502)
|
||||||
|
|
||||||
clash = bool(CLASH_UA.search(ua)) and "yaml" in headers.get("content-type", "")
|
clash = bool(CLASH_UA.search(ua)) and "yaml" in headers.get("content-type", "")
|
||||||
awg = clash and not NO_AWG_UA.search(ua)
|
awg = clash and not NO_AWG_UA.search(ua)
|
||||||
|
|||||||
+33
-12
@@ -382,7 +382,7 @@ cmd_vision() { # имя|--all [off]
|
|||||||
cmd_toggle() { # имя true|false
|
cmd_toggle() { # имя true|false
|
||||||
valid_name "$1"
|
valid_name "$1"
|
||||||
[[ -n $(client "$1") ]] || die "Нет пользователя $1"
|
[[ -n $(client "$1") ]] || die "Нет пользователя $1"
|
||||||
update_user "$1" ".enable = \$v" --argjson v "$2"
|
update_user "$1" ".enable = \$v | .comment = \"kit\"" --argjson v "$2"
|
||||||
if [[ $2 == true ]]; then say "Пользователь $1 включён."; else say "Пользователь $1 выключен – подписка и подключения не работают."; fi
|
if [[ $2 == true ]]; then say "Пользователь $1 включён."; else say "Пользователь $1 выключен – подписка и подключения не работают."; fi
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1231,7 +1231,7 @@ SNI_RE='^[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?(\.[A-Za-z0-9]([A-Za-z0-9-]*[A-Za
|
|||||||
|
|
||||||
# Бесплатные и динамические имена (sslip.io, work.gd и т. п.) часто используют чужие прокси-серверы: под них маскироваться не стоит.
|
# Бесплатные и динамические имена (sslip.io, work.gd и т. п.) часто используют чужие прокси-серверы: под них маскироваться не стоит.
|
||||||
SNI_DYN_RE='(^|\.)(sslip\.io|nip\.io|xip\.io|traefik\.me|work\.gd|duckdns\.org|ddns\.net|hopto\.org|zapto\.org|myftp\.biz|dynu\.net|freeddns\.org|no-ip\.(org|biz|info)|nom\.za|tk|ml|ga|cf|gq)$'
|
SNI_DYN_RE='(^|\.)(sslip\.io|nip\.io|xip\.io|traefik\.me|work\.gd|duckdns\.org|ddns\.net|hopto\.org|zapto\.org|myftp\.biz|dynu\.net|freeddns\.org|no-ip\.(org|biz|info)|nom\.za|tk|ml|ga|cf|gq)$'
|
||||||
# Известные сайты на чужом IP не подходят: сайт-прикрытие должен быть «своим» для подсети сервера.
|
# Известные сайты на чужом IP не подходят: сайт для маскировки должен быть «своим» для подсети сервера.
|
||||||
SNI_BRAND_RE='(^|\.)(google|googleapis|gstatic|youtube|microsoft|windows|apple|icloud|amazon|amazonaws|samsung|yahoo|cloudflare|facebook|instagram|netflix|github|telegram)\.[a-z.]+$'
|
SNI_BRAND_RE='(^|\.)(google|googleapis|gstatic|youtube|microsoft|windows|apple|icloud|amazon|amazonaws|samsung|yahoo|cloudflare|facebook|instagram|netflix|github|telegram)\.[a-z.]+$'
|
||||||
|
|
||||||
# Имена с «сомнительными» словами не берём: брать такой сайт для маскировки неприятно и небезопасно для вас.
|
# Имена с «сомнительными» словами не берём: брать такой сайт для маскировки неприятно и небезопасно для вас.
|
||||||
@@ -1276,7 +1276,7 @@ same_net() { # имя
|
|||||||
# Имена из сертификатов соседних адресов /24: заходим на 443 без имени и читаем subjectAltName.
|
# Имена из сертификатов соседних адресов /24: заходим на 443 без имени и читаем subjectAltName.
|
||||||
nearby_scan() { # основа "a.b.c"
|
nearby_scan() { # основа "a.b.c"
|
||||||
local me; me=$(host_ip)
|
local me; me=$(host_ip)
|
||||||
seq 1 254 | xargs -P 24 -I{} bash -c '
|
seq 1 254 | xargs -P 8 -I{} bash -c '
|
||||||
ip=$1; [ "$ip" = "$2" ] && exit 0
|
ip=$1; [ "$ip" = "$2" ] && exit 0
|
||||||
echo | timeout 4 openssl s_client -connect "$ip:443" -tls1_3 -alpn h2 2>/dev/null | openssl x509 -noout -ext subjectAltName 2>/dev/null \
|
echo | timeout 4 openssl s_client -connect "$ip:443" -tls1_3 -alpn h2 2>/dev/null | openssl x509 -noout -ext subjectAltName 2>/dev/null \
|
||||||
| tr "," "\n" | sed -n "s/^ *DNS://p"' _ "$1.{}" "$me" 2>/dev/null | sort -u | grep -E "$SNI_RE" | head -40 || true
|
| tr "," "\n" | sed -n "s/^ *DNS://p"' _ "$1.{}" "$me" 2>/dev/null | sort -u | grep -E "$SNI_RE" | head -40 || true
|
||||||
@@ -1398,7 +1398,16 @@ sni_rotate() { # [--nearby] [--dry-run] [сайт]
|
|||||||
|
|
||||||
local bak
|
local bak
|
||||||
bak=/root/x-ui-before-sni-$(date +%Y%m%d-%H%M%S).db
|
bak=/root/x-ui-before-sni-$(date +%Y%m%d-%H%M%S).db
|
||||||
install -m 600 /dev/null "$bak"; cat /etc/x-ui/x-ui.db >"$bak"
|
install -m 600 /dev/null "$bak"
|
||||||
|
python3 - /etc/x-ui/x-ui.db "$bak" <<'PY' || die "Не удалось сохранить копию базы панели. Ничего не менял."
|
||||||
|
import sqlite3, sys
|
||||||
|
src = sqlite3.connect("file:%s?mode=ro" % sys.argv[1], uri=True)
|
||||||
|
dst = sqlite3.connect(sys.argv[2])
|
||||||
|
src.backup(dst)
|
||||||
|
dst.close(); src.close()
|
||||||
|
PY
|
||||||
|
# Храним три последние копии: в них ключи сервера.
|
||||||
|
ls -1t /root/x-ui-before-sni-*.db 2>/dev/null | tail -n +4 | while read -r old; do rm -f -- "$old"; done
|
||||||
say "Копия базы панели: $bak"
|
say "Копия базы панели: $bak"
|
||||||
|
|
||||||
local -a done_i=()
|
local -a done_i=()
|
||||||
@@ -1545,7 +1554,7 @@ reality_add() { # [порт]
|
|||||||
settings=$(jq -nc '{clients: [], decryption: "none", fallbacks: []}')
|
settings=$(jq -nc '{clients: [], decryption: "none", fallbacks: []}')
|
||||||
# Сайт маскировки (или свой домен) – как у основного REALITY, ключи и shortId – свои.
|
# Сайт маскировки (или свой домен) – как у основного REALITY, ключи и shortId – свои.
|
||||||
stream=$(jq -c --argjson k "$keys" --arg sid "$sid" '(.streamSettings | if type == "string" then fromjson else . end)
|
stream=$(jq -c --argjson k "$keys" --arg sid "$sid" '(.streamSettings | if type == "string" then fromjson else . end)
|
||||||
| .externalProxy = [] | .tcpSettings.acceptProxyProtocol = false | .realitySettings.xver = 0
|
| .externalProxy = [] | .tcpSettings.acceptProxyProtocol = false
|
||||||
| .realitySettings.privateKey = $k.privateKey | .realitySettings.shortIds = [$sid]
|
| .realitySettings.privateKey = $k.privateKey | .realitySettings.shortIds = [$sid]
|
||||||
| .realitySettings.settings.publicKey = $k.publicKey | del(.sockopt)' <<<"$base")
|
| .realitySettings.settings.publicKey = $k.publicKey | del(.sockopt)' <<<"$base")
|
||||||
body=$(jq -nc --argjson port "$port" --arg s "$settings" --arg st "$stream" '{remark: "REALITY-2", enable: true, listen: "", port: $port, protocol: "vless",
|
body=$(jq -nc --argjson port "$port" --arg s "$settings" --arg st "$stream" '{remark: "REALITY-2", enable: true, listen: "", port: $port, protocol: "vless",
|
||||||
@@ -1799,7 +1808,7 @@ net_panel() { # [domain|ip]
|
|||||||
cur=${PANEL_ON:-ip}
|
cur=${PANEL_ON:-ip}
|
||||||
if [[ -z $want ]]; then
|
if [[ -z $want ]]; then
|
||||||
if [[ $cur == domain ]]; then echo "Панель и подписка открываются по домену $domain (вернуть на IP: kit net panel ip)"
|
if [[ $cur == domain ]]; then echo "Панель и подписка открываются по домену $domain (вернуть на IP: kit net panel ip)"
|
||||||
else echo "Панель и подписка открываются по IP ${HOST:-?}; домен $domain – сайт-прикрытие (открывать и по домену: kit net panel domain)"; fi
|
else echo "Панель и подписка открываются по IP ${HOST:-?}; домен $domain – сайт для маскировки (открывать и по домену: kit net panel domain)"; fi
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
[[ $want == domain || $want == ip ]] || die "kit net panel [domain|ip]"
|
[[ $want == domain || $want == ip ]] || die "kit net panel [domain|ip]"
|
||||||
@@ -1811,18 +1820,25 @@ net_panel() { # [domain|ip]
|
|||||||
fi
|
fi
|
||||||
tmp=$(mktemp); bak=$(mktemp); cp -p "$PANEL_CONF" "$bak"
|
tmp=$(mktemp); bak=$(mktemp); cp -p "$PANEL_CONF" "$bak"
|
||||||
panel_conf_py render "$PANEL_CONF" "$want" "$SUB_PATH" "$panel_path" >"$tmp" || { rm -f "$tmp" "$bak"; die "Не удалось собрать настройки nginx. Ничего не изменилось."; }
|
panel_conf_py render "$PANEL_CONF" "$want" "$SUB_PATH" "$panel_path" >"$tmp" || { rm -f "$tmp" "$bak"; die "Не удалось собрать настройки nginx. Ничего не изменилось."; }
|
||||||
rid=$(sni_targets | awk -F'\t' '$2 == "self" && $3 == "REALITY" {print $1; exit}')
|
local -a rids=()
|
||||||
[[ -n $rid ]] || { rm -f "$tmp" "$bak"; die "Не нашёл REALITY со своим доменом."; }
|
mapfile -t rids < <(sni_targets | awk -F'\t' '$2 == "self" {print $1}')
|
||||||
|
((${#rids[@]})) || { rm -f "$tmp" "$bak"; die "Не нашёл REALITY со своим доменом."; }
|
||||||
|
panel_xver() { # 0|1 – у всех REALITY, чей сайт – блок домена (в том числе REALITY-2)
|
||||||
|
local r
|
||||||
|
for r in "${rids[@]}"; do
|
||||||
|
inbound_patch "$r" '.streamSettings |= ((if type == "string" then fromjson else . end) | .realitySettings.xver = $x)' --argjson x "$1" || return 1
|
||||||
|
done
|
||||||
|
}
|
||||||
# Передача настоящего IP клиента сайту домена (PROXY protocol) нужна, чтобы панель видела, кто заходит.
|
# Передача настоящего IP клиента сайту домена (PROXY protocol) нужна, чтобы панель видела, кто заходит.
|
||||||
local xv=0; [[ $want == domain ]] && xv=1
|
local xv=0; [[ $want == domain ]] && xv=1
|
||||||
panel_revert() { cat "$bak" >"$PANEL_CONF"; inbound_patch "$rid" '.streamSettings |= ((if type == "string" then fromjson else . end) | .realitySettings.xver = $x)' --argjson x "$((1 - xv))" || true; nginx -t >/dev/null 2>&1 && systemctl reload nginx || true; }
|
panel_revert() { cat "$bak" >"$PANEL_CONF"; panel_xver "$((1 - xv))" || true; nginx -t >/dev/null 2>&1 && systemctl reload nginx || true; }
|
||||||
if [[ $want == domain ]]; then
|
if [[ $want == domain ]]; then
|
||||||
cat "$tmp" >"$PANEL_CONF"
|
cat "$tmp" >"$PANEL_CONF"
|
||||||
nginx -t >/dev/null 2>&1 || { cat "$bak" >"$PANEL_CONF"; rm -f "$tmp" "$bak"; die "nginx не принял настройки. Ничего не изменилось."; }
|
nginx -t >/dev/null 2>&1 || { cat "$bak" >"$PANEL_CONF"; rm -f "$tmp" "$bak"; die "nginx не принял настройки. Ничего не изменилось."; }
|
||||||
inbound_patch "$rid" '.streamSettings |= ((if type == "string" then fromjson else . end) | .realitySettings.xver = $x)' --argjson x 1 || { cat "$bak" >"$PANEL_CONF"; rm -f "$tmp" "$bak"; die "Панель не приняла изменение. Ничего не изменилось."; }
|
panel_xver 1 || { cat "$bak" >"$PANEL_CONF"; rm -f "$tmp" "$bak"; die "Панель не приняла изменение. Ничего не изменилось."; }
|
||||||
systemctl reload nginx || { panel_revert; rm -f "$tmp" "$bak"; die "nginx не перезагрузился. Всё возвращено."; }
|
systemctl reload nginx || { panel_revert; rm -f "$tmp" "$bak"; die "nginx не перезагрузился. Всё возвращено."; }
|
||||||
else
|
else
|
||||||
inbound_patch "$rid" '.streamSettings |= ((if type == "string" then fromjson else . end) | .realitySettings.xver = $x)' --argjson x 0 || { rm -f "$tmp" "$bak"; die "Панель не приняла изменение. Ничего не изменилось."; }
|
panel_xver 0 || { rm -f "$tmp" "$bak"; die "Панель не приняла изменение. Ничего не изменилось."; }
|
||||||
cat "$tmp" >"$PANEL_CONF"
|
cat "$tmp" >"$PANEL_CONF"
|
||||||
if ! nginx -t >/dev/null 2>&1 || ! systemctl reload nginx; then panel_revert; rm -f "$tmp" "$bak"; die "nginx не принял настройки. Всё возвращено."; fi
|
if ! nginx -t >/dev/null 2>&1 || ! systemctl reload nginx; then panel_revert; rm -f "$tmp" "$bak"; die "nginx не принял настройки. Всё возвращено."; fi
|
||||||
fi
|
fi
|
||||||
@@ -1960,7 +1976,7 @@ net_fp() { # отпечаток
|
|||||||
done < <(api GET inbounds/list | jq -r '.[] | select((.streamSettings | if type == "string" then fromjson else . end) | (.security == "reality" or .security == "tls")) | .id')
|
done < <(api GET inbounds/list | jq -r '.[] | select((.streamSettings | if type == "string" then fromjson else . end) | (.security == "reality" or .security == "tls")) | .id')
|
||||||
say "Отпечаток $old → ${B}$f${N}, обновлено подключений: $n."
|
say "Отпечаток $old → ${B}$f${N}, обновлено подключений: $n."
|
||||||
echo "Подписка обновится сама; вручную сохранённые ссылки замените (kit user link имя)."
|
echo "Подписка обновится сама; вручную сохранённые ссылки замените (kit user link имя)."
|
||||||
echo "Меняйте, если связь пропала сразу у многих, а не из-за одного неудачного раза."
|
echo "Вернуть прежний: kit net fp chrome."
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd_net() {
|
cmd_net() {
|
||||||
@@ -2228,6 +2244,11 @@ case "$cmd_key" in
|
|||||||
"reality add") shift 2; reality_add "$@" ;;
|
"reality add") shift 2; reality_add "$@" ;;
|
||||||
"panel update") shift 2; panel_update "$@" ;;
|
"panel update") shift 2; panel_update "$@" ;;
|
||||||
"__limit-timer on") limit_timer_on ;;
|
"__limit-timer on") limit_timer_on ;;
|
||||||
|
"__xver "*) # служебная: xver у всех REALITY со своим доменом (возврат после неудачного --restore)
|
||||||
|
[[ ${2:-} =~ ^[01]$ ]] || exit 1
|
||||||
|
while read -r _id; do
|
||||||
|
inbound_patch "$_id" '.streamSettings |= ((if type == "string" then fromjson else . end) | .realitySettings.xver = $x)' --argjson x "$2" || true
|
||||||
|
done < <(sni_targets | awk -F'\t' '$2 == "self" {print $1}') ;;
|
||||||
"__links "*) links_block "${2:-}" "${3:-}" ;;
|
"__links "*) links_block "${2:-}" "${3:-}" ;;
|
||||||
"update "*) shift; cmd_update "$@" ;;
|
"update "*) shift; cmd_update "$@" ;;
|
||||||
"backup "*) cmd_backup ;;
|
"backup "*) cmd_backup ;;
|
||||||
|
|||||||
Reference in New Issue
Block a user