1.1.2: исправление установки с сертификатом Let's Encrypt на IP, ufw и порты SSH, параметры, kit-sub, kit user
This commit is contained in:
+17
-33
@@ -1,44 +1,28 @@
|
||||
# Что нового
|
||||
|
||||
## v1.1.2: исправление установки и мелкие починки
|
||||
|
||||
* Исправил «Ошибка в строке 452»: установка спотыкалась о собственную проверку, как человек о порог в тёмной комнате. Теперь идёт ровно. Спасибо [ram0973](https://github.com/ram0973), который сообщил первым, и [xlok1e](https://github.com/xlok1e) за точную правку.
|
||||
* `ufw` больше не запирает вас снаружи на нестандартном порту SSH (сервер, закрытый от хозяина, – так себе охрана). Имя с точкой не цепляет чужих, а `kit fix` и откат обновления доводят дело до конца.
|
||||
* Параметры прощают `--port=8443` и `VPN.Example.COM`: я перестал придираться к регистру.
|
||||
|
||||
Сервер обновится сам этой ночью, нетерпеливым – `kit update`.
|
||||
|
||||
## v1.1.1: kit check и kit fix, исправления для Happ и скачивания ядра
|
||||
|
||||
**Новое**
|
||||
|
||||
* `kit check` и `kit fix`. Завёл для сервера врача. `kit check` осматривает службы, сертификат, подписку, права на файлы и сайт маскировки и честно говорит, что болит, а `kit fix` лечит только то, что безопасно лечить: перезапустит упавшее, вернёт права, включит автообновление. Никаких операций без наркоза. Заодно он спасает подписку, которая молча умирала после `x-ui cert`: она вела себя как кот, который делает вид, что вас не знает.
|
||||
* Инструкция для XKeen прямо в README. Восемь шагов от компонентов роутера до проверки, со скриншотами, чтобы не собирать её по десяти вкладкам. Автору XKeen огромное спасибо: всю тяжёлую работу сделал он, я только собрал шпаргалку.
|
||||
|
||||
**Исправлено**
|
||||
|
||||
* Маршрутизация Happ доходит до клиентов. `kit-sub` пропускал только свой короткий список заголовков, поэтому профиль в Happ просто не появлялся. Спасибо [xlok1e](https://github.com/xlok1e): он нашёл причину и прислал исправление, мне осталось только вежливо принять его и проверить.
|
||||
* Ядро Xray ставится, даже когда GitHub тормозит или молчит. Запасной путь идёт через зеркало, но только с проверкой SHA256: чужой бинарник не пройдёт, зеркалу я не верю ни на слово.
|
||||
* Установка со своим доменом больше не обрывается, если Let's Encrypt отказал в сертификате на IP (лимит выдачи, закрытый порт 80, сбой). Я объясню причины, спрошу «продолжить с самоподписанным сертификатом? [Y/n]» и честно предупрежу: ссылки на подключения работают, а подписка в приложениях может не открыться. Без драмы.
|
||||
* Подтянул мелочи по итогам проверки: права на ключ запасного сертификата, ожидание замка `dpkg` при установке `unzip` и защиту заголовков подписки от лишних символов.
|
||||
|
||||
Если у вас 1.1, сервер обновится сам этой ночью. Хотите сразу: `kit update`.
|
||||
* `kit check` осматривает сервер, как терапевт: «где болит?». `kit fix` лечит безопасное, без наркоза и без героизма.
|
||||
* Заголовки Happ доходят до клиентов, а не теряются по дороге, как носки в стиралке. Спасибо [xlok1e](https://github.com/xlok1e) за находку.
|
||||
* Xray ставится, даже когда GitHub молчит: через зеркало, но только с проверкой SHA256. Чужой бинарник не пройдёт, я никому не верю.
|
||||
* Со своим доменом установка не рвётся, если Let's Encrypt отказал в сертификате на IP. Инструкция для XKeen теперь в README.
|
||||
|
||||
## v1.1: безопасность, автообновление и новый генератор для роутера
|
||||
|
||||
**Новое**
|
||||
* Обновления приходят сами по ночам и только с моей подписью: «мам, удали старый VPN» больше не нужно. Не любите сюрпризы: `kit update --manual`. Копия сервера: `kit backup`.
|
||||
* Маскировка на выбор: чужой сайт или свой домен. На случайный заход сервер изображает кофейню, фотографа или блог про Linux.
|
||||
* Один генератор для роутера вместо двух: Keenetic и OpenWrt, Xray или Mihomo. Меньше вкладок, меньше «а я точно там нажал?».
|
||||
* Безопасность: установщики проверяются по SHA256 (подмену, как незнакомца с «выгодным предложением», не пускаем), Hysteria2 не исполняет почту как команду, `kit-sub` больше не root.
|
||||
|
||||
* Обновления приходят сами. Раз в сутки ночью сервер проверяет мою подпись и обновляет `kit`, а пользователи и ссылки остаются прежними. «Мам, удали старый VPN, вот новый» больше не нужно. Подпись не сошлась или подписка замолчала: откат. Не любите сюрпризы? `kit update --manual`.
|
||||
* Копия сервера одной командой. `kit backup` складывает подключения, ключи и пользователей в один файл.
|
||||
* Маскировка на выбор. Установщик спрашивает: стандартный чужой сайт (работает сразу) или свой домен (надёжнее: настоящая страница и сертификат Let's Encrypt). Без вопросов: `--domain vpn.example.com`. Под какой сайт будет выглядеть сервер, теперь решаете вы, а не скрипт за вас.
|
||||
* Заглушка с характером. На случайный заход сервер показывает кофейню, фотографа или блог про Linux. Тысячи серверов 3X-UI KIT больше не выглядят как близнецы.
|
||||
* Один генератор для роутера вместо двух. Выбираете Keenetic или OpenWrt, ядро Xray или Mihomo, вставляете ссылки и получаете конфиг плюс команду, которая сама положит его на роутер. Меньше вкладок, меньше «а я точно там нажал?». Старые адреса генераторов продолжают работать.
|
||||
* OpenWrt через Nikki (бета). Генератор собирает профиль Mihomo и команду, которая включит его и перезапустит Nikki. Xray на OpenWrt я не предлагаю: Nikki работает только на Mihomo, а кнопку, которая не заработает, лучше не рисовать.
|
||||
|
||||
**Исправлено**
|
||||
|
||||
* Установщик 3X-UI сверяется по SHA256. Если его кто-то подменит, скрипт посмотрит на него как на незнакомца в чате с «выгодным предложением» и запускать не станет. Файлы я теперь беру из тега версии, а не из `main`.
|
||||
* Hysteria больше не проверяется по справке, которую сама же принесла. Контрольная сумма записана прямо в скрипте. Проверять человека по его же рекомендательному письму – так себе идея.
|
||||
* Hysteria2 перестала исполнять почту. Особо изобретательный адрес раньше мог выполниться как команда. Теперь `me@gmail.com;rm -rf` получит вежливый отказ.
|
||||
* Подписка больше не root. Даже если `kit-sub` взломают, ключей от сервера у него нет. Пусть попробует открыть хоть одну дверь.
|
||||
|
||||
**Как обновиться с 1.0** (один раз, дальше само)
|
||||
|
||||
`curl -fsSL https://raw.githubusercontent.com/itsnotkubrick/3X-UI_KIT/v1.1/scripts/kit.sh -o /usr/local/bin/kit && chmod 755 /usr/local/bin/kit && kit update`
|
||||
|
||||
Hysteria2: `hy2 update`. Новые установки получают всё сразу.
|
||||
С 1.0 один раз: `curl -fsSL https://raw.githubusercontent.com/itsnotkubrick/3X-UI_KIT/v1.1/scripts/kit.sh -o /usr/local/bin/kit && chmod 755 /usr/local/bin/kit && kit update`
|
||||
|
||||
## v1.0
|
||||
|
||||
|
||||
@@ -15,11 +15,11 @@
|
||||
---
|
||||
|
||||
> [!TIP]
|
||||
> **Вышла версия 1.1.1:** `kit check` и `kit fix`, исправления для Happ и скачивания ядра, инструкции для XKeen.
|
||||
> [Что нового](https://github.com/itsnotkubrick/3X-UI_KIT/releases/tag/v1.1.1) ·
|
||||
> сервер на 1.1 обновится сам этой ночью, а на 1.0 одной командой:
|
||||
> **Вышла версия 1.1.2:** исправление установки на чистом сервере и мелкие починки.
|
||||
> [Что нового](https://github.com/itsnotkubrick/3X-UI_KIT/releases/tag/v1.1.2) ·
|
||||
> сервер на 1.1 и 1.1.1 обновится сам этой ночью, а на 1.0 одной командой:
|
||||
> ```bash
|
||||
> curl -fsSL https://raw.githubusercontent.com/itsnotkubrick/3X-UI_KIT/v1.1.1/scripts/kit.sh -o /usr/local/bin/kit && chmod 755 /usr/local/bin/kit && kit update
|
||||
> curl -fsSL https://raw.githubusercontent.com/itsnotkubrick/3X-UI_KIT/v1.1.2/scripts/kit.sh -o /usr/local/bin/kit && chmod 755 /usr/local/bin/kit && kit update
|
||||
> ```
|
||||
|
||||
> [!NOTE]
|
||||
|
||||
+1
-1
@@ -135,7 +135,7 @@ kit version # версии kit, панели и ядра, включе
|
||||
Сервер ставили версией 1.0? В ней команды `kit update` ещё нет, поэтому один раз:
|
||||
|
||||
```bash
|
||||
curl -fsSL https://raw.githubusercontent.com/itsnotkubrick/3X-UI_KIT/v1.1.1/scripts/kit.sh -o /usr/local/bin/kit && chmod 755 /usr/local/bin/kit && kit update
|
||||
curl -fsSL https://raw.githubusercontent.com/itsnotkubrick/3X-UI_KIT/v1.1.2/scripts/kit.sh -o /usr/local/bin/kit && chmod 755 /usr/local/bin/kit && kit update
|
||||
```
|
||||
|
||||
Дальше обновления будут приходить сами.
|
||||
|
||||
+61
-13
@@ -10,6 +10,9 @@
|
||||
# с форматом под каждый клиент и настраивает ufw. Домены не нужны.
|
||||
# Каждый протокол проверен настоящими клиентами – см. tests/matrix.
|
||||
|
||||
# Запуск через sh (dash) ломается на непонятной ошибке синтаксиса – подскажем сразу.
|
||||
[ -n "${BASH_VERSION:-}" ] || { echo "Запустите через bash, а не через sh." >&2; exit 1; }
|
||||
|
||||
set -Eeuo pipefail
|
||||
|
||||
# На свежем VPS в фоне идут автообновления системы и держат замок dpkg: ждём его, а не падаем.
|
||||
@@ -28,7 +31,7 @@ wait_apt_idle() {
|
||||
XUI_VERSION="v3.8.5"
|
||||
# SHA256 установщика 3X-UI этой версии: тег могут передвинуть, а хеш – нет (проверено 2026-09-30).
|
||||
XUI_INSTALL_SHA256="4e3fe7fe00ef8e904ce6a0e9c36fd8a0c7179fe5e786f23e31801aee84c6347d"
|
||||
KIT_VERSION="1.1.1"
|
||||
KIT_VERSION="1.1.2"
|
||||
# kit и kit-sub берём из того же релиза, что и этот скрипт, а не из меняющейся ветки main.
|
||||
KIT_RAW="https://raw.githubusercontent.com/itsnotkubrick/3X-UI_KIT/v$KIT_VERSION"
|
||||
# Ядро Xray для панели. С 26.7.x клиенты на Mihomo и sing-box (Hiddify, FlClash,
|
||||
@@ -146,20 +149,22 @@ choose_masking() {
|
||||
echo
|
||||
local d="" prev=""
|
||||
ask_tty "Ваш выбор [1]: " || return 0
|
||||
[[ $REPLY == 2 ]] || { echo; return 0; }
|
||||
REPLY=${REPLY//[[:space:]]/}
|
||||
[[ ${REPLY%.} == 2 ]] || { echo; return 0; }
|
||||
while :; do
|
||||
if [[ -n $prev ]]; then ask_tty "Ваш домен [$prev]: " || return 0
|
||||
else ask_tty "Ваш домен, например vpn.example.com (пусто – стандартный сайт): " || return 0; fi
|
||||
d=${REPLY,,}; d=${d// /}
|
||||
[[ -n $d ]] || d=$prev
|
||||
[[ -n $d ]] || return 0
|
||||
if [[ ! $d =~ $re_host ]]; then warn "Это не похоже на домен. Пример: vpn.example.com"; prev=""; continue; fi
|
||||
if [[ ! $d =~ $re_host ]]; then warn "Это не похоже на домен (нужно только имя, без https://). Пример: vpn.example.com"; prev=""; continue; fi
|
||||
if domain_points_here "$d"; then DOMAIN=$d; return 0; fi
|
||||
prev=$d
|
||||
ask_tty "Enter – проверить ещё раз (DNS обновляется не сразу), s – стандартный сайт, q – выйти: " || return 0
|
||||
REPLY=${REPLY//[[:space:]]/}
|
||||
case ${REPLY,,} in
|
||||
s) return 0 ;;
|
||||
q) die "Остановился по вашей просьбе. Ставить можно снова в любой момент." ;;
|
||||
s | ы | с) return 0 ;;
|
||||
q | й) die "Остановился по вашей просьбе. Ставить можно снова в любой момент." ;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
@@ -208,7 +213,8 @@ ip_cert_self_signed_fallback() {
|
||||
} >&2
|
||||
if [[ $yes == no && -t 1 ]] && { : </dev/tty; } 2>/dev/null; then
|
||||
ask_tty "Продолжить с самоподписанным сертификатом? [Y/n] " || REPLY=""
|
||||
[[ ${REPLY,,} == n* ]] && return 1
|
||||
REPLY=${REPLY//[[:space:]]/}
|
||||
[[ ${REPLY,,} == n* || ${REPLY,,} == н* ]] && return 1
|
||||
else
|
||||
say "Продолжаю с самоподписанным сертификатом (без вопросов)."
|
||||
fi
|
||||
@@ -277,6 +283,13 @@ ART
|
||||
}
|
||||
|
||||
main() {
|
||||
local a args=()
|
||||
for a in "$@"; do
|
||||
[[ $a == -h || $a == --help ]] && { usage; exit 0; }
|
||||
# --port=8443 понимаем так же, как --port 8443
|
||||
if [[ $a == --*=* ]]; then args+=("${a%%=*}" "${a#*=}"); else args+=("$a"); fi
|
||||
done
|
||||
set -- ${args[@]+"${args[@]}"}
|
||||
[[ $EUID -eq 0 ]] || die "Запустите от root: sudo -i, затем команду ещё раз."
|
||||
command -v systemctl >/dev/null || die "Нужен systemd."
|
||||
if [[ -f $RESULT && -x /usr/local/x-ui/x-ui ]]; then
|
||||
@@ -302,6 +315,10 @@ main() {
|
||||
|
||||
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 | --sni | --panel-ssl | --host | --user | --protocols | --domain | --cert | --key)
|
||||
[[ -n ${2-} ]] || die "У параметра $1 нет значения (см. --help)" ;;
|
||||
esac
|
||||
case $1 in
|
||||
--port) PORT=$2; shift 2 ;;
|
||||
--sni) SNI=$2; shift 2 ;;
|
||||
@@ -319,13 +336,24 @@ main() {
|
||||
*) die "Неизвестный параметр: $1 (см. --help)" ;;
|
||||
esac
|
||||
done
|
||||
[[ $PORT =~ ^[0-9]+$ ]] && ((PORT > 0 && PORT < 65536)) || die "Неверный порт: $PORT"
|
||||
local re_host='^([A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?\.)+[A-Za-z]{2,63}$'
|
||||
[[ $PORT =~ ^[0-9]{1,5}$ ]] && ((10#$PORT > 0 && 10#$PORT < 65536)) || die "Неверный порт: $PORT"
|
||||
PORT=$((10#$PORT))
|
||||
# Регистр и точка в конце не важны: vpn.Example.com. – это тот же vpn.example.com.
|
||||
local k
|
||||
for k in SNI DOMAIN HOST; do
|
||||
[[ -z ${!k} ]] || { local v=${!k,,}; v=${v%.}; printf -v "$k" '%s' "$v"; }
|
||||
done
|
||||
PANEL_SSL=${PANEL_SSL,,}; protos=${protos,,}; protos=${protos// /}
|
||||
local re_host='^([A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?\.)+([A-Za-z]{2,63}|xn--[A-Za-z0-9-]{1,59})$'
|
||||
for k in "$SNI" "$DOMAIN" "$HOST"; do
|
||||
[[ $k != *://* && $k != */* ]] || die "Нужно только имя, без https:// и без «/»: например vpn.example.com"
|
||||
[[ $k =~ ^[A-Za-z0-9.-]*$ ]] || die "Имя «$k» с не латинскими буквами не подойдёт: запишите его в виде punycode (xn--…), например через idn или в личном кабинете регистратора."
|
||||
done
|
||||
[[ -z $SNI || $SNI =~ $re_host ]] || die "--sni: нужно имя сайта, например dl.google.com"
|
||||
[[ -z $DOMAIN || $DOMAIN =~ $re_host ]] || die "--domain: нужно имя вашего домена, например vpn.example.com"
|
||||
[[ -z $DOMAIN || -z $SNI ]] || die "--sni и --domain вместе не нужны: выберите либо чужой сайт (--sni), либо свой домен (--domain)."
|
||||
[[ -z $DOMAIN || $multi == no ]] || die "Свой домен работает только в режиме «всё на 443» – уберите --multi-port."
|
||||
[[ -z $HOST || $HOST =~ $re_host || $HOST =~ ^[0-9]{1,3}(\.[0-9]{1,3}){3}$ ]] || die "--host: нужен IP или домен"
|
||||
[[ -z $HOST || $HOST =~ $re_host || $HOST =~ ^(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])(\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}$ ]] || die "--host: нужен IP (например 1.2.3.4) или домен"
|
||||
[[ $NAME =~ ^[A-Za-z0-9_.-]{1,32}$ ]] || die "Имя: латиница, цифры, _ . - (до 32 символов)."
|
||||
[[ $PANEL_SSL =~ ^(auto|ip|none)$ ]] || die "--panel-ssl: auto, ip или none"
|
||||
if [[ -n $ucert || -n $ukey ]]; then
|
||||
@@ -340,6 +368,9 @@ main() {
|
||||
local x
|
||||
for x in "${PROTOS[@]}"; do [[ " ${ALL_PROTOS[*]} " == *" $x "* ]] || die "Неизвестный протокол: $x. Доступны: ${ALL_PROTOS[*]}"; done ;;
|
||||
esac
|
||||
if [[ $multi == no && -d /etc/nginx ]] && grep -rqsE '^[[:space:]]*stream[[:space:]]*\{' /etc/nginx/nginx.conf /etc/nginx/conf.d /etc/nginx/sites-enabled /etc/nginx/streams-enabled 2>/dev/null; then
|
||||
die "В вашем nginx уже есть блок stream: установщик в режиме «всё на 443» не сможет с ним ужиться. Запустите с --multi-port или уберите свой блок."
|
||||
fi
|
||||
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
|
||||
@@ -645,10 +676,19 @@ set_xray_core() {
|
||||
fi
|
||||
}
|
||||
|
||||
# Порты SSH, на которых сервер слушает сейчас: из настроек sshd, из ss (в Ubuntu 24.04 порт держит
|
||||
# systemd, но сам sshd тоже в списке) и порт текущего подключения. Нужны, чтобы ufw не запер вас.
|
||||
ssh_ports() {
|
||||
{ sshd -T 2>/dev/null | awk '$1 == "port" {print $2}'
|
||||
ss -H -ltnp 2>/dev/null | awk '/"sshd"/ {n = split($4, a, ":"); print a[n]}'
|
||||
awk '{print $4}' <<<"${SSH_CONNECTION:-}"
|
||||
} | grep -E '^[0-9]{1,5}$' | sort -un || true
|
||||
}
|
||||
|
||||
setup_ufw() {
|
||||
local ssh_port o
|
||||
ssh_port=$(ss -H -ltnp 2>/dev/null | awk '/sshd/ {sub(/.*:/,"",$4); print $4; exit}')
|
||||
OPEN+=("${ssh_port:-22}/tcp")
|
||||
ssh_port=$(ssh_ports)
|
||||
for o in ${ssh_port:-22}; do OPEN+=("$o/tcp"); done
|
||||
say "Настраиваю ufw: ${OPEN[*]}"
|
||||
for o in "${OPEN[@]}"; do ufw allow "$o" >/dev/null; done
|
||||
ufw --force enable >/dev/null || warn "ufw не включился (так бывает в контейнерах) – откройте порты у хостера вручную."
|
||||
@@ -663,7 +703,9 @@ setup_tls_cert() {
|
||||
elif [[ $PANEL_SSL == ip && -s /root/cert/ip/fullchain.pem ]]; then
|
||||
CERT=/root/cert/ip/fullchain.pem; KEY=/root/cert/ip/privkey.pem
|
||||
# Самоподписанный сертификат на IP: отпечаток уходит в ссылки, чтобы клиенты доверяли именно ему.
|
||||
[[ $SELF_IP_CERT == yes ]] && PIN=$(openssl x509 -in "$CERT" -noout -fingerprint -sha256 | cut -d= -f2 | tr -d ':' | tr 'A-F' 'a-f')
|
||||
if [[ $SELF_IP_CERT == yes ]]; then
|
||||
PIN=$(openssl x509 -in "$CERT" -noout -fingerprint -sha256 | cut -d= -f2 | tr -d ':' | tr 'A-F' 'a-f')
|
||||
fi
|
||||
else
|
||||
# Без Let's Encrypt – свой сертификат, а его отпечаток уходит в ссылки (pcs),
|
||||
# чтобы клиенты доверяли именно ему.
|
||||
@@ -1195,7 +1237,13 @@ server {
|
||||
NGX
|
||||
fi
|
||||
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 не принял конфиг – лог выше."; }
|
||||
if ! nginx -t >/tmp/nginx-test.log 2>&1; then
|
||||
cat /tmp/nginx-test.log >&2
|
||||
# Наш include убираем, чтобы не оставить чужой nginx сломанным.
|
||||
sed -i '/kit-stream\.conf/d' /etc/nginx/nginx.conf
|
||||
rm -f /etc/nginx/kit-stream.conf
|
||||
die "nginx не принял конфиг – лог выше. Если в вашем nginx уже есть блок stream, запустите установку с --multi-port."
|
||||
fi
|
||||
systemctl enable nginx >/dev/null 2>&1
|
||||
systemctl restart nginx
|
||||
# Let's Encrypt на IP продлевается каждые несколько дней – nginx раз в сутки перечитывает сертификат.
|
||||
|
||||
+40
-10
@@ -8,6 +8,9 @@
|
||||
# сумма проверяется), настраивает сертификат, сайт-заглушку, ufw и выдаёт
|
||||
# ссылку hy2:// с QR-кодом.
|
||||
|
||||
# Запуск через sh (dash) ломается на непонятной ошибке синтаксиса – подскажем сразу.
|
||||
[ -n "${BASH_VERSION:-}" ] || { echo "Запустите через bash, а не через sh." >&2; exit 1; }
|
||||
|
||||
set -Eeuo pipefail
|
||||
|
||||
# На свежем VPS в фоне идут автообновления системы и держат замок dpkg: ждём его, а не падаем.
|
||||
@@ -21,7 +24,7 @@ declare -A HY_SHA256=(
|
||||
[amd64]=8c7a68a906998b747a0db87586e364f995fbfddb95693ae6e2fdb68a6e920d3e
|
||||
[arm64]=c8dc653c3ba0a28d29a26b8fa52d2086f27c0927afddce95c09965e7174e78b0
|
||||
)
|
||||
KIT_VERSION="1.1.1"
|
||||
KIT_VERSION="1.1.2"
|
||||
KIT_REPO_RAW="https://raw.githubusercontent.com/itsnotkubrick/3X-UI_KIT"
|
||||
# Скрипт берём из тега релиза, а не из меняющейся ветки main.
|
||||
SELF_URL="$KIT_REPO_RAW/v$KIT_VERSION/scripts/hysteria2.sh"
|
||||
@@ -220,14 +223,23 @@ tune_sysctl() {
|
||||
sysctl -q -p "$SYSCTL" 2>/dev/null || warn "Не удалось применить sysctl (так бывает в контейнерах) – на работу не влияет."
|
||||
}
|
||||
|
||||
# Порты SSH, на которых сервер слушает сейчас (sshd, ss и порт текущего подключения):
|
||||
# ufw не должен запереть вас, в том числе на Ubuntu 24.04, где порт держит systemd.
|
||||
ssh_ports() {
|
||||
{ sshd -T 2>/dev/null | awk '$1 == "port" {print $2}'
|
||||
ss -H -ltnp 2>/dev/null | awk '/"sshd"/ {n = split($4, a, ":"); print a[n]}'
|
||||
awk '{print $4}' <<<"${SSH_CONNECTION:-}"
|
||||
} | grep -E '^[0-9]{1,5}$' | sort -un || true
|
||||
}
|
||||
|
||||
setup_ufw() {
|
||||
. "$STATE"
|
||||
[[ ${UFW:-yes} == yes ]] || return 0
|
||||
local ssh_port
|
||||
ssh_port=$(ss -H -ltnp 2>/dev/null | awk '/sshd/ {sub(/.*:/,"",$4); print $4; exit}')
|
||||
local ssh_port p
|
||||
ssh_port=$(ssh_ports)
|
||||
ssh_port=${ssh_port:-22}
|
||||
say "Настраиваю ufw: SSH $ssh_port/tcp, Hysteria $PORT/udp${DOMAIN:+, сайт 80/tcp и $PORT/tcp}"
|
||||
ufw allow "$ssh_port/tcp" >/dev/null
|
||||
say "Настраиваю ufw: SSH $(tr '\n' ' ' <<<"$ssh_port")/tcp, Hysteria $PORT/udp${DOMAIN:+, сайт 80/tcp и $PORT/tcp}"
|
||||
for p in $ssh_port; do ufw allow "$p/tcp" >/dev/null; done
|
||||
ufw allow "$PORT/udp" >/dev/null
|
||||
if [[ -n ${DOMAIN:-} ]]; then
|
||||
ufw allow 80/tcp >/dev/null
|
||||
@@ -265,8 +277,17 @@ cmd_install() {
|
||||
check_os
|
||||
[[ -f $STATE ]] && die "Hysteria уже установлена этим скриптом. Команды управления: hy2 help"
|
||||
|
||||
local DOMAIN="" EMAIL="" PORT=443 USERNAME="admin" HOST="" UFW=yes SNI="" yes=no
|
||||
local DOMAIN="" EMAIL="" PORT=443 USERNAME="admin" HOST="" UFW=yes SNI="" yes=no a args=()
|
||||
# --port=8443 понимаем так же, как --port 8443
|
||||
for a in "$@"; do
|
||||
if [[ $a == --*=* ]]; then args+=("${a%%=*}" "${a#*=}"); else args+=("$a"); fi
|
||||
done
|
||||
set -- ${args[@]+"${args[@]}"}
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--domain | --email | --port | --user | --host | --sni)
|
||||
[[ -n ${2-} ]] || die "У параметра $1 нет значения (см. hy2 help)" ;;
|
||||
esac
|
||||
case $1 in
|
||||
--domain) DOMAIN=$2; shift 2 ;;
|
||||
--email) EMAIL=$2; shift 2 ;;
|
||||
@@ -289,14 +310,23 @@ cmd_install() {
|
||||
if [[ -n $DOMAIN ]]; then read -rp "Почта для Let's Encrypt: " EMAIL; fi
|
||||
fi
|
||||
# Значения попадают в install.env и конфиг Hysteria – пропускаем только допустимые символы.
|
||||
local re_host='^([A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?\.)+[A-Za-z]{2,63}$'
|
||||
# Регистр и точка в конце не важны: Vpn.Example.com. – это vpn.example.com
|
||||
local k
|
||||
for k in DOMAIN SNI HOST; do
|
||||
[[ -z ${!k} ]] || { local v=${!k//[[:space:]]/}; v=${v,,}; v=${v%.}; printf -v "$k" '%s' "$v"; }
|
||||
[[ ${!k} != *://* && ${!k} != */* ]] || die "Нужно только имя, без https:// и без «/»: например vpn.example.com"
|
||||
[[ ${!k} =~ ^[A-Za-z0-9.-]*$ ]] || die "Имя «${!k}» с не латинскими буквами не подойдёт: запишите его в виде punycode (xn--…)."
|
||||
done
|
||||
EMAIL=${EMAIL//[[:space:]]/}
|
||||
local re_host='^([A-Za-z0-9]([A-Za-z0-9-]{0,61}[A-Za-z0-9])?\.)+([A-Za-z]{2,63}|xn--[A-Za-z0-9-]{1,59})$'
|
||||
if [[ -n $DOMAIN ]]; then
|
||||
[[ $DOMAIN =~ $re_host ]] || die "Похоже, это не домен: $DOMAIN"
|
||||
[[ $EMAIL =~ ^[A-Za-z0-9._%+-]+@([A-Za-z0-9-]+\.)+[A-Za-z]{2,63}$ ]] || die "Для Let's Encrypt нужна почта: --email you@example.com"
|
||||
fi
|
||||
[[ -z $SNI || $SNI =~ $re_host ]] || die "--sni: нужно имя сайта, например www.bing.com"
|
||||
[[ -z $HOST || $HOST =~ $re_host || $HOST =~ ^[0-9]{1,3}(\.[0-9]{1,3}){3}$ ]] || die "--host: нужен IP или домен"
|
||||
[[ $PORT =~ ^[0-9]+$ ]] && ((PORT > 0 && PORT < 65536)) || die "Неверный порт: $PORT"
|
||||
[[ -z $HOST || $HOST =~ $re_host || $HOST =~ ^(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])(\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}$ ]] || die "--host: нужен IP (например 1.2.3.4) или домен"
|
||||
[[ $PORT =~ ^[0-9]{1,5}$ ]] && ((10#$PORT > 0 && 10#$PORT < 65536)) || die "Неверный порт: $PORT"
|
||||
PORT=$((10#$PORT))
|
||||
[[ $USERNAME =~ ^[A-Za-z0-9_.-]{1,32}$ ]] || die "Имя пользователя: латиница, цифры, _ . - (до 32 символов)."
|
||||
|
||||
port_busy "$PORT" udp && die "UDP-порт $PORT уже занят. Выберите другой: --port 8443"
|
||||
@@ -378,7 +408,7 @@ show_link() {
|
||||
echo
|
||||
echo "$link"
|
||||
echo
|
||||
command -v qrencode >/dev/null && qrencode -t ANSIUTF8 -m 1 "$link"
|
||||
if command -v qrencode >/dev/null; then qrencode -t ANSIUTF8 -m 1 "$link"; fi
|
||||
}
|
||||
|
||||
reload_service() {
|
||||
|
||||
+12
-1
@@ -14,6 +14,7 @@ https://github.com/itsnotkubrick/3X-UI_KIT
|
||||
"""
|
||||
|
||||
import base64
|
||||
import http.client
|
||||
import http.server
|
||||
import json
|
||||
import os
|
||||
@@ -65,7 +66,7 @@ def upstream(sub_id, ua, host, accept):
|
||||
return r.status, {k.lower(): v for k, v in r.getheaders()}, r.read()
|
||||
except urllib.error.HTTPError as e:
|
||||
return e.code, {k.lower(): v for k, v in e.headers.items()}, e.read()
|
||||
except (urllib.error.URLError, OSError, socket.timeout) as e:
|
||||
except (urllib.error.URLError, http.client.HTTPException, OSError, socket.timeout) as e:
|
||||
log(f"upstream недоступен: {e}")
|
||||
return None, {}, b""
|
||||
|
||||
@@ -165,6 +166,16 @@ class Handler(http.server.BaseHTTPRequestHandler):
|
||||
if self.command != "HEAD":
|
||||
self.wfile.write(body)
|
||||
|
||||
def send_error(self, code, message=None, explain=None):
|
||||
# Свои короткие ответы вместо страницы ошибок Python: сканеру не видно, чем отвечает сервер.
|
||||
self.close_connection = True
|
||||
self.send_plain(code, f"{code} {self.responses.get(code, ('error',))[0].lower()}")
|
||||
|
||||
def do_other(self):
|
||||
self.send_plain(404, "404 page not found")
|
||||
|
||||
do_POST = do_PUT = do_DELETE = do_PATCH = do_OPTIONS = do_other
|
||||
|
||||
def do_HEAD(self):
|
||||
self.do_GET()
|
||||
|
||||
|
||||
+35
-18
@@ -6,10 +6,13 @@
|
||||
# kit user list | link имя | limit имя [--gb N] [--days N] | off имя | on имя | del имя
|
||||
# kit update [--auto | --manual] | kit backup | kit check | kit fix | kit version
|
||||
|
||||
# Запуск через sh (dash) ломается на непонятной ошибке синтаксиса – подскажем сразу.
|
||||
[ -n "${BASH_VERSION:-}" ] || { echo "Запустите через bash, а не через sh." >&2; exit 1; }
|
||||
|
||||
set -Eeuo pipefail
|
||||
export LC_ALL=C.UTF-8 # ширина колонок по символам, а не байтам
|
||||
|
||||
KIT_VERSION="1.1.1"
|
||||
KIT_VERSION="1.1.2"
|
||||
KIT_RAW="https://raw.githubusercontent.com/itsnotkubrick/3X-UI_KIT/main"
|
||||
# Файлы новой версии берём из её тега, а не из меняющейся ветки main.
|
||||
kit_ref_raw() { echo "https://raw.githubusercontent.com/itsnotkubrick/3X-UI_KIT/v$1"; }
|
||||
@@ -80,12 +83,19 @@ awg_attach() { # имя subId [лимит-байт] [срок-мс] [устро
|
||||
clients() { api GET clients/list | jq -c 'if type == "array" then . else .clients end'; }
|
||||
client() { clients | jq -c --arg e "$1" 'map(select(.email == $e))[0] // empty'; }
|
||||
# Все записи пользователя: основная и «двойники» для AmneziaWG («имя-awgN»).
|
||||
emails_of() { clients | jq -r --arg e "$1" '.[] | select(.email == $e or (.email | test("^" + $e + "-awg[0-9]*$"))) | .email'; }
|
||||
emails_of() { clients | jq -r --arg e "$1" '.[] | select(.email == $e or ((.email | startswith($e + "-awg")) and (.email[($e | length) + 4:] | test("^[0-9]*$")))) | .email'; }
|
||||
valid_name() { [[ $1 =~ ^[A-Za-z0-9_.-]{1,32}$ ]] || die "Имя: латиница, цифры, _ . - (до 32 символов)."; }
|
||||
rand_id() { openssl rand -base64 48 | tr -dc 'a-z0-9' | head -c 16; }
|
||||
|
||||
gb_bytes() { [[ $1 =~ ^[0-9]+$ ]] || die "--gb: целое число гигабайт"; echo $(($1 * 1073741824)); }
|
||||
days_ms() { [[ $1 =~ ^[0-9]+$ ]] || die "--days: целое число дней"; ((${1} == 0)) && { echo 0; return; }; echo $((($(date +%s) + $1 * 86400) * 1000)); }
|
||||
# Числа из командной строки: «08» не должно читаться как восьмеричное, а 30 цифр – переполнять счёт.
|
||||
gb_bytes() { [[ ${1-} =~ ^[0-9]{1,6}$ ]] || die "--gb: целое число гигабайт (до 999999), например --gb 50"; echo $((10#$1 * 1073741824)); }
|
||||
days_ms() { [[ ${1-} =~ ^[0-9]{1,5}$ ]] || die "--days: целое число дней (до 99999), например --days 30"; ((10#$1 == 0)) && { echo 0; return; }; echo $((($(date +%s) + 10#$1 * 86400) * 1000)); }
|
||||
# Флаги вида --gb=50 превращаем в «--gb 50»; у флага без значения – понятная ошибка.
|
||||
eq_args() { # печатает аргументы по одному
|
||||
local a
|
||||
for a in "$@"; do if [[ $a == --*=* ]]; then printf '%s\n%s\n' "${a%%=*}" "${a#*=}"; else printf '%s\n' "$a"; fi; done
|
||||
}
|
||||
need_val() { [[ -n ${2-} ]] || die "У параметра $1 нет значения."; }
|
||||
|
||||
human() { # байты → «1.2 ГБ»
|
||||
awk -v b="$1" 'BEGIN { split("Б КБ МБ ГБ ТБ", u, " "); i = 1; while (b >= 1024 && i < 5) { b /= 1024; i++ }
|
||||
@@ -110,18 +120,23 @@ show_link() { # имя subId
|
||||
cmd_add() {
|
||||
local name=${1:-} gb=0 days=0 devices=0
|
||||
valid_name "$name"; shift
|
||||
mapfile -t _args < <(eq_args "$@"); set -- ${_args[@]+"${_args[@]}"}
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--gb) gb=$2; shift 2 ;;
|
||||
--days) days=$2; shift 2 ;;
|
||||
--devices) devices=$2; shift 2 ;;
|
||||
--gb) need_val "$@"; gb=$2; shift 2 ;;
|
||||
--days) need_val "$@"; days=$2; shift 2 ;;
|
||||
--devices) need_val "$@"; devices=$2; shift 2 ;;
|
||||
*) die "Неизвестный параметр: $1" ;;
|
||||
esac
|
||||
done
|
||||
[[ $devices =~ ^[0-9]{1,4}$ ]] || die "--devices: целое число"
|
||||
devices=$((10#$devices))
|
||||
[[ -z $(client "$name") ]] || die "Пользователь $name уже есть. Ссылка: kit user link $name"
|
||||
local ids sid body
|
||||
ids=$(non_awg_ids)
|
||||
[[ $ids != "[]" ]] || die "На сервере нет подключений."
|
||||
gb_bytes "$gb" >/dev/null; days_ms "$days" >/dev/null
|
||||
gb=$((10#$gb)); days=$((10#$days))
|
||||
sid=$(rand_id)
|
||||
body=$(jq -nc --arg e "$name" --arg s "$sid" --argjson t "$(gb_bytes "$gb")" --argjson x "$(days_ms "$days")" \
|
||||
--argjson ip "$devices" --argjson ids "$ids" '{client: {email: $e, subId: $s, totalGB: $t, expiryTime: $x,
|
||||
@@ -188,11 +203,12 @@ cmd_limit() {
|
||||
local name=${1:-} f="." gb="" days="" dev=""
|
||||
valid_name "$name"; shift
|
||||
[[ -n $(client "$name") ]] || die "Нет пользователя $name"
|
||||
mapfile -t _args < <(eq_args "$@"); set -- ${_args[@]+"${_args[@]}"}
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--gb) gb=$(gb_bytes "$2"); f+=" | .totalGB = \$gb"; shift 2 ;;
|
||||
--days) days=$(days_ms "$2"); f+=" | .expiryTime = \$days"; shift 2 ;;
|
||||
--devices) [[ $2 =~ ^[0-9]+$ ]] || die "--devices: целое число"; dev=$2; f+=" | .limitIp = \$dev"; shift 2 ;;
|
||||
--gb) need_val "$@"; gb=$(gb_bytes "$2"); f+=" | .totalGB = \$gb"; shift 2 ;;
|
||||
--days) need_val "$@"; days=$(days_ms "$2"); f+=" | .expiryTime = \$days"; shift 2 ;;
|
||||
--devices) need_val "$@"; [[ $2 =~ ^[0-9]{1,4}$ ]] || die "--devices: целое число"; dev=$((10#$2)); f+=" | .limitIp = \$dev"; shift 2 ;;
|
||||
*) die "Неизвестный параметр: $1" ;;
|
||||
esac
|
||||
done
|
||||
@@ -271,7 +287,7 @@ cmd_version() {
|
||||
# Что нового в версии $1 – из CHANGELOG.md, без разметки.
|
||||
changelog_of() {
|
||||
curl -fsS -m 5 "$KIT_RAW/CHANGELOG.md" 2>/dev/null | awk -v v="## v$1" '
|
||||
index($0, v) == 1 { on = 1; t = substr($0, length(v) + 1); sub(/^[: ]+/, "", t); if (t != "") print t; next } on && /^## / { exit } on' | sed -E 's/ ?Спасибо \[[^]]*\]\([^)]*\)[^.]*\.//g; s/\[([^]]*)\]\([^)]*\)/\1/g; s/\*\*//g; s/`//g' | grep -v '^[[:space:]]*$' | head -40 || true
|
||||
index($0, v) == 1 { on = 1; t = substr($0, length(v) + 1); sub(/^[: ]+/, "", t); if (t != "") print t; next } on && /^## / { exit } on' | sed -E 's/ ?Спасибо .*$//; s/\[([^]]*)\]\([^)]*\)/\1/g; s/\*\*//g; s/`//g' | grep -v '^[[:space:]]*$' | head -40 || true
|
||||
}
|
||||
|
||||
# Скачивает релиз $1 в каталог $2 и проверяет: подпись SHA256SUMS нашим ключом, версию
|
||||
@@ -446,16 +462,14 @@ cmd_update() {
|
||||
kit_sub_unit "$c" "$k" >/etc/systemd/system/kit-sub.service
|
||||
[[ -n $c ]] && echo '19 4 * * * root systemctl restart kit-sub >/dev/null 2>&1' >/etc/cron.d/kit-sub-cert
|
||||
systemctl daemon-reload
|
||||
systemctl restart kit-sub
|
||||
sleep 2
|
||||
if sub_ok; then
|
||||
if systemctl restart kit-sub && sleep 2 && sub_ok; then
|
||||
say "Подписка kit-sub обновлена и отвечает"
|
||||
else
|
||||
install -m 644 "$tmp/kit_sub.old" /usr/local/lib/kit-sub/kit_sub.py
|
||||
install -m 644 "$tmp/kit-sub.service.old" /etc/systemd/system/kit-sub.service
|
||||
grep -q '^LoadCredential=cert.pem' "$tmp/kit-sub.service.old" || rm -f /etc/cron.d/kit-sub-cert
|
||||
systemctl daemon-reload
|
||||
systemctl restart kit-sub
|
||||
systemctl restart kit-sub || true
|
||||
die "Новая подписка не ответила – вернул прежнюю, kit остался версии $KIT_VERSION. Лог: journalctl -u kit-sub -n 30"
|
||||
fi
|
||||
fi
|
||||
@@ -532,7 +546,8 @@ check_cert() {
|
||||
local cert end left
|
||||
cert=$(awk '$1 == "ssl_certificate" {sub(/;$/, "", $2); print $2; exit}' /etc/nginx/conf.d/kit.conf 2>/dev/null || true)
|
||||
[[ -n $cert ]] || cert=$(jq -r '.cert // empty' /etc/kit-sub/config.json 2>/dev/null || true)
|
||||
[[ -n $cert && -f $cert ]] || { c_info "сертификат для проверки не найден (режим без TLS)"; return 0; }
|
||||
[[ -n $cert ]] || { c_info "сертификат не настроен (режим без TLS)"; return 0; }
|
||||
[[ -f $cert ]] || { c_bad cert "файл сертификата $cert из настроек не найден"; return 0; }
|
||||
if ! openssl x509 -in "$cert" -noout -checkend 0 >/dev/null 2>&1; then
|
||||
c_bad cert "сертификат $cert просрочен"
|
||||
else
|
||||
@@ -660,7 +675,7 @@ fix_action() { # код
|
||||
say "Возвращаю права 600 на файлы с паролями и ключами"
|
||||
local f
|
||||
for f in /etc/x-ui/install-result.env /etc/kit/kit.env /etc/kit-sub/config.json /root/3x-ui.txt /root/cert/*/privkey.pem; do
|
||||
[[ -f $f ]] && chmod 600 "$f"
|
||||
if [[ -f $f ]]; then chmod 600 "$f"; fi
|
||||
done ;;
|
||||
cert)
|
||||
if nginx -t >/dev/null 2>&1; then say "Перечитываю сертификат в nginx"; systemctl reload nginx
|
||||
@@ -775,7 +790,9 @@ ${B}kit${N} $KIT_VERSION – управление сервером 3X-UI KIT
|
||||
EOF
|
||||
}
|
||||
|
||||
case "${1:-} ${2:-}" in
|
||||
# Регистр и «users» вместо «user» не должны ломать команду; имя пользователя (третье слово) не трогаем.
|
||||
cmd_key="${1:-} ${2:-}"; cmd_key=${cmd_key,,}; cmd_key=${cmd_key/users /user }
|
||||
case "$cmd_key" in
|
||||
"user add") shift 2; cmd_add "$@" ;;
|
||||
"user list") cmd_list; update_hint ;;
|
||||
"user link") shift 2; cmd_link "$@" ;;
|
||||
|
||||
Reference in New Issue
Block a user