From 3d3f3e2d9de244224dee95f1c8797930234aa2e3 Mon Sep 17 00:00:00 2001 From: maxdmitrov <56165502+maxdmitrov@users.noreply.github.com> Date: Sat, 3 Oct 2026 21:36:22 +0300 Subject: [PATCH] =?UTF-8?q?1.1.2:=20=D0=B8=D1=81=D0=BF=D1=80=D0=B0=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20=D1=83=D1=81=D1=82=D0=B0=D0=BD?= =?UTF-8?q?=D0=BE=D0=B2=D0=BA=D0=B8=20=D1=81=20=D1=81=D0=B5=D1=80=D1=82?= =?UTF-8?q?=D0=B8=D1=84=D0=B8=D0=BA=D0=B0=D1=82=D0=BE=D0=BC=20Let's=20Encr?= =?UTF-8?q?ypt=20=D0=BD=D0=B0=20IP,=20ufw=20=D0=B8=20=D0=BF=D0=BE=D1=80?= =?UTF-8?q?=D1=82=D1=8B=20SSH,=20=D0=BF=D0=B0=D1=80=D0=B0=D0=BC=D0=B5?= =?UTF-8?q?=D1=82=D1=80=D1=8B,=20kit-sub,=20kit=20user?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 50 ++++++++++-------------------- README.md | 8 ++--- VERSION | 2 +- manuals/3x-ui.md | 2 +- scripts/3x-ui.sh | 74 ++++++++++++++++++++++++++++++++++++-------- scripts/hysteria2.sh | 50 ++++++++++++++++++++++++------ scripts/kit-sub.py | 13 +++++++- scripts/kit.sh | 53 ++++++++++++++++++++----------- 8 files changed, 171 insertions(+), 81 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index db239cd..66ece99 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index dad81de..6f4f545 100644 --- a/README.md +++ b/README.md @@ -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] diff --git a/VERSION b/VERSION index 524cb55..45a1b3f 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.1.1 +1.1.2 diff --git a/manuals/3x-ui.md b/manuals/3x-ui.md index 7d9d893..5cb8680 100644 --- a/manuals/3x-ui.md +++ b/manuals/3x-ui.md @@ -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 ``` Дальше обновления будут приходить сами. diff --git a/scripts/3x-ui.sh b/scripts/3x-ui.sh index c1b25bb..8d35b31 100755 --- a/scripts/3x-ui.sh +++ b/scripts/3x-ui.sh @@ -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/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 раз в сутки перечитывает сертификат. diff --git a/scripts/hysteria2.sh b/scripts/hysteria2.sh index 8742e08..b2918c7 100755 --- a/scripts/hysteria2.sh +++ b/scripts/hysteria2.sh @@ -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() { diff --git a/scripts/kit-sub.py b/scripts/kit-sub.py index 8082880..bb47aa3 100644 --- a/scripts/kit-sub.py +++ b/scripts/kit-sub.py @@ -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() diff --git a/scripts/kit.sh b/scripts/kit.sh index 69ecc36..f79ec0f 100644 --- a/scripts/kit.sh +++ b/scripts/kit.sh @@ -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 "$@" ;;