kit 1.2: меню как у x-ui (kit без аргументов), kit net (протоколы, порты, сайт, отпечаток, вкл/выкл), блок ссылок по группам, kit check --fix, kit update --panel; версия 1.2

This commit is contained in:
maxdmitrov
2026-10-04 20:20:20 +03:00
parent 4246031ff9
commit 0a9cce18b6
4 changed files with 424 additions and 39 deletions
+1 -1
View File
@@ -1 +1 @@
1.1.2 1.2
+1 -1
View File
@@ -31,7 +31,7 @@ wait_apt_idle() {
XUI_VERSION="v3.9.0" XUI_VERSION="v3.9.0"
# SHA256 установщика 3X-UI этой версии: тег могут передвинуть, а хеш – нет (проверено 2026-09-30). # SHA256 установщика 3X-UI этой версии: тег могут передвинуть, а хеш – нет (проверено 2026-09-30).
XUI_INSTALL_SHA256="18616fe26c8f6c92db6daa2dcd7cd53c5143ee69ecfd26d8ea6dc9b2c78607a6" XUI_INSTALL_SHA256="18616fe26c8f6c92db6daa2dcd7cd53c5143ee69ecfd26d8ea6dc9b2c78607a6"
KIT_VERSION="1.1.2" KIT_VERSION="1.2"
# kit и kit-sub берём из того же релиза, что и этот скрипт, а не из меняющейся ветки main. # kit и kit-sub берём из того же релиза, что и этот скрипт, а не из меняющейся ветки main.
KIT_RAW="https://raw.githubusercontent.com/itsnotkubrick/3X-UI_KIT/v$KIT_VERSION" KIT_RAW="https://raw.githubusercontent.com/itsnotkubrick/3X-UI_KIT/v$KIT_VERSION"
# Ядро Xray для панели. С 26.7.x клиенты на Mihomo и sing-box (Hiddify, FlClash, # Ядро Xray для панели. С 26.7.x клиенты на Mihomo и sing-box (Hiddify, FlClash,
+1 -1
View File
@@ -24,7 +24,7 @@ declare -A HY_SHA256=(
[amd64]=8c7a68a906998b747a0db87586e364f995fbfddb95693ae6e2fdb68a6e920d3e [amd64]=8c7a68a906998b747a0db87586e364f995fbfddb95693ae6e2fdb68a6e920d3e
[arm64]=c8dc653c3ba0a28d29a26b8fa52d2086f27c0927afddce95c09965e7174e78b0 [arm64]=c8dc653c3ba0a28d29a26b8fa52d2086f27c0927afddce95c09965e7174e78b0
) )
KIT_VERSION="1.1.2" KIT_VERSION="1.2"
KIT_REPO_RAW="https://raw.githubusercontent.com/itsnotkubrick/3X-UI_KIT" KIT_REPO_RAW="https://raw.githubusercontent.com/itsnotkubrick/3X-UI_KIT"
# Скрипт берём из тега релиза, а не из меняющейся ветки main. # Скрипт берём из тега релиза, а не из меняющейся ветки main.
SELF_URL="$KIT_REPO_RAW/v$KIT_VERSION/scripts/hysteria2.sh" SELF_URL="$KIT_REPO_RAW/v$KIT_VERSION/scripts/hysteria2.sh"
+421 -36
View File
@@ -12,7 +12,7 @@
set -Eeuo pipefail set -Eeuo pipefail
export LC_ALL=C.UTF-8 # ширина колонок по символам, а не байтам export LC_ALL=C.UTF-8 # ширина колонок по символам, а не байтам
KIT_VERSION="1.1.2" KIT_VERSION="1.2"
KIT_RAW="https://raw.githubusercontent.com/itsnotkubrick/3X-UI_KIT/main" KIT_RAW="https://raw.githubusercontent.com/itsnotkubrick/3X-UI_KIT/main"
# Файлы новой версии берём из её тега, а не из меняющейся ветки main. # Файлы новой версии берём из её тега, а не из меняющейся ветки main.
kit_ref_raw() { echo "https://raw.githubusercontent.com/itsnotkubrick/3X-UI_KIT/v$1"; } kit_ref_raw() { echo "https://raw.githubusercontent.com/itsnotkubrick/3X-UI_KIT/v$1"; }
@@ -147,9 +147,10 @@ show_link() { # имя subId
url=$(sub_url "$2") url=$(sub_url "$2")
echo echo
if local_only_sub; then if local_only_sub; then
echo "Подписка ${B}$1${N} в этом режиме (панель через SSH-туннель) открывается только на самом сервере," echo "В режиме «панель через SSH-туннель» подписка открывается только на самом сервере,"
echo "с телефона по ней не зайти. Подключайтесь отдельными ссылками на протоколы:" echo "поэтому подключайтесь ссылками на протоколы ниже."
direct_links "$2" '^[a-z0-9]+://' echo
links_block "$1" "$2"
return 0 return 0
fi fi
echo "Подписка ${B}$1${N} – все протоколы одной ссылкой. Вставьте в Happ, Hiddify, Karing," echo "Подписка ${B}$1${N} – все протоколы одной ссылкой. Вставьте в Happ, Hiddify, Karing,"
@@ -158,7 +159,7 @@ show_link() { # имя subId
echo "$url" echo "$url"
echo echo
command -v qrencode >/dev/null && qrencode -t ANSIUTF8 -m 1 "$url" command -v qrencode >/dev/null && qrencode -t ANSIUTF8 -m 1 "$url"
echo "${D}AmneziaVPN: kit user link $1 --amnezia · Telegram: kit user link $1 --telegram${N}" echo "${D}Отдельные ссылки на каждый протокол: kit user link $1 --all${N}"
} }
cmd_add() { cmd_add() {
@@ -218,7 +219,7 @@ cmd_link() {
"" | --all) "" | --all)
show_link "$name" "$sid" show_link "$name" "$sid"
# В режиме «только на сервере» показ уже содержит все ссылки. # В режиме «только на сервере» показ уже содержит все ссылки.
if [[ $flag == --all ]] && ! local_only_sub; then direct_links "$sid" '^(vpn|tg)://'; fi ;; if [[ $flag == --all ]] && ! local_only_sub; then echo; links_block "$name" "$sid"; fi ;;
*) die "kit user link имя [--all | --amnezia | --telegram]" ;; *) die "kit user link имя [--all | --amnezia | --telegram]" ;;
esac esac
} }
@@ -580,17 +581,19 @@ auto_off() {
} }
cmd_update() { cmd_update() {
local force="" unattended=no latest tmp local force="" unattended=no latest tmp panel=no
while [[ $# -gt 0 ]]; do while [[ $# -gt 0 ]]; do
case $1 in case $1 in
--force) force=yes ;; --force) force=yes ;;
--panel) panel=yes ;;
--auto) auto_on; say "Автообновление включено: раз в сутки ночью, только подписанные релизы. Журнал: $KIT_UPDATE_LOG"; return ;; --auto) auto_on; say "Автообновление включено: раз в сутки ночью, только подписанные релизы. Журнал: $KIT_UPDATE_LOG"; return ;;
--manual) auto_off; say "Автообновление выключено. Обновляться вручную: kit update, включить снова: kit update --auto"; return ;; --manual) auto_off; say "Автообновление выключено. Обновляться вручную: kit update, включить снова: kit update --auto"; return ;;
--unattended) unattended=yes ;; --unattended) unattended=yes ;;
*) die "Неизвестный параметр: $1 (kit update [--force | --auto | --manual])" ;; *) die "Неизвестный параметр: $1 (kit update [--panel | --force | --auto | --manual])" ;;
esac esac
shift shift
done done
[[ $panel == no ]] || { panel_update ${force:+--force}; return; }
# Ночной запуск и ручной не должны встретиться. # Ночной запуск и ручной не должны встретиться.
exec 9>/run/kit-update.lock exec 9>/run/kit-update.lock
flock -n 9 || die "Обновление уже идёт." flock -n 9 || die "Обновление уже идёт."
@@ -892,8 +895,9 @@ deep_clients() {
cmd_check() { cmd_check() {
local deep=no local deep=no
[[ ${1:-} != --fix ]] || { shift; cmd_fix "$@"; return; }
[[ ${1:-} != --deep ]] || deep=yes [[ ${1:-} != --deep ]] || deep=yes
[[ -z ${1:-} || $deep == yes ]] || die "Команда: kit check [--deep]" [[ -z ${1:-} || $deep == yes ]] || die "Команда: kit check [--deep | --fix]"
echo "${B}kit check${N} – проверка сервера (ничего не меняет)" echo "${B}kit check${N} – проверка сервера (ничего не меняет)"
echo echo
run_checks run_checks
@@ -1456,37 +1460,416 @@ reality_add() { # [порт]
echo "Проверить: ${B}kit check --deep${N}. Ссылки: ${B}kit user link имя --all${N}" echo "Проверить: ${B}kit check --deep${N}. Ссылки: ${B}kit user link имя --all${N}"
} }
# ---------- вид: блок ссылок, kit net, меню ----------
# Блок ссылок: основные и запасные по группам, у каждой название, порт и сама ссылка. Печатает и итог установки, и kit user link.
links_block() { # имя subId
local name=$1 sid=$2 raw
raw=$(collect_links "$sid" '^[a-z0-9]+://')
[[ -n $raw ]] || { echo "Отдельных ссылок нет."; return 0; }
local script
IFS= read -r -d '' script <<'PY' || true
import base64, json, re, subprocess, shutil, sys
from urllib.parse import urlparse, parse_qs, unquote
name = sys.argv[1]
MAIN = ["REALITY", "XHTTP", "Hysteria2"]
items, vpn = [], 0
for line in sys.stdin.read().split():
sch = line.split("://", 1)[0]
label, port, net, hint = "", "", "tcp", ""
try:
if sch == "vmess":
d = json.loads(base64.b64decode(line[8:] + "=" * (-len(line[8:]) % 4)))
label, port = d.get("ps", "VMess"), str(d.get("port", ""))
elif sch == "vpn":
vpn += 1
label, net, hint = ("AmneziaWG" if vpn == 1 else "AmneziaWG 3.1"), "udp", "вставьте в AmneziaVPN"
elif sch == "tg":
label, port, hint = "MTProto", parse_qs(urlparse(line).query).get("port", [""])[0], "для Telegram"
else:
u = urlparse(line)
label, port = unquote(u.fragment), str(u.port or "")
if sch in ("hysteria2", "tuic"):
net = "udp"
if sch == "tuic":
hint = "если приложение ругается на сертификат, включите «Разрешить небезопасный»"
except Exception:
continue
label = re.sub(r"-" + re.escape(name) + r"$", "", label) or sch
items.append((label, port, net, hint, line))
def show(label, port, net, hint, line):
where = (port + "/" + net) if port and sch_net_ok(label, net) else (port or net)
print(" %s · %s%s" % (label, where, (" → " + hint) if hint else ""))
print(" " + line)
print()
def sch_net_ok(label, net):
return label not in ("Shadowsocks",)
print("ПОДКЛЮЧЕНИЕ · %s" % name)
print()
main = [i for m in MAIN for i in items if i[0] == m]
ORDER = ["REALITY-2", "VLESS-WS", "Trojan-gRPC", "VMess-WS", "Shadowsocks", "TUIC", "AmneziaWG", "AmneziaWG 3.1", "MTProto"]
spare = [i for i in items if i[0] not in MAIN]
spare.sort(key=lambda i: ORDER.index(i[0]) if i[0] in ORDER else len(ORDER))
qr_done = False
for title, group in (("Основные", main), ("Запасные", spare)):
if not group:
continue
print(title)
for it in group:
show(*it)
if not qr_done and it[0] == "REALITY" and shutil.which("qrencode"):
sys.stdout.flush()
subprocess.run(["qrencode", "-t", "ANSIUTF8", "-m", "1", it[4]])
print()
qr_done = True
PY
python3 -c "$script" "$name" <<<"$raw"
}
# ---------- kit net: протоколы, порты, сайт маскировки, отпечаток ----------
net_main_name() { case $1 in REALITY | XHTTP | Hysteria2) return 0 ;; *) return 1 ;; esac; }
net_resolve() { # короткое имя → название подключения
case ${1,,} in
reality) echo REALITY ;; reality2 | reality-2) echo REALITY-2 ;; xhttp) echo XHTTP ;; hy2 | hysteria2 | hysteria) echo Hysteria2 ;;
ws | vless-ws) echo VLESS-WS ;; grpc | trojan | trojan-grpc) echo Trojan-gRPC ;; vmess | vmess-ws) echo VMess-WS ;;
ss | shadowsocks) echo Shadowsocks ;; tuic) echo TUIC ;; wg | wireguard) echo WireGuard ;;
awg | amneziawg) echo AmneziaWG ;; awg3 | amneziawg-3.1) echo AmneziaWG-3.1 ;; mtproto) echo MTProto ;;
*) echo "$1" ;;
esac
}
inbound_body_of() { # строка подключения (json) → тело для inbounds/update
jq -c '{id, remark, enable, listen, port, protocol, expiryTime, total,
settings: (.settings | if type == "string" then . else tojson end),
streamSettings: (.streamSettings | if type == "string" then . else tojson end),
sniffing: (.sniffing | if type == "string" then . else tojson end)}' <<<"$1"
}
inbound_patch() { # id фильтр-jq [аргументы jq...]: меняет подключение, ошибка – код возврата, не выход
local id=$1 filter=$2 row new
shift 2
row=$(api GET inbounds/list | jq -c --argjson id "$id" '.[] | select(.id == $id)')
[[ -n $row ]] || return 1
new=$(jq -c "$@" "$filter" <<<"$row") || return 1
(api POST "inbounds/update/$id" "$(inbound_body_of "$new")" >/dev/null) || return 1
}
net_show() {
local list name port proto listen en sni fp shown_reality2=no mark note group fpl=""
list=$(api GET inbounds/list)
printf '%s %s %s %s\n' "$(padr Протоколы 22)" "$(padr Порт 12)" "$(padr Статус 8)" "Заметка"
for group in main spare; do
if [[ $group == main ]]; then echo "Основные"; else echo "Запасные"; fi
while IFS='|' read -r name port proto listen en sni fp; do
[[ -n $name ]] || continue
if net_main_name "$name"; then [[ $group == main ]] || continue; else [[ $group == spare ]] || continue; fi
[[ $name == REALITY-2 ]] && shown_reality2=yes
if [[ $listen == 127.0.0.1 ]]; then mark="443/tcp"; else mark="$port/$(port_net "$proto")"; fi
[[ $(port_net "$proto") == both ]] && mark=$port
note=""
[[ -z $sni ]] || note="сайт: $sni"
[[ -n $fp && -z $fpl ]] && fpl=$fp
printf ' %s %s %s %s\n' "$(padr "$name" 20)" "$(padr "$mark" 12)" "$(padr "$([[ $en == true ]] && echo вкл || echo выкл)" 8)" "$note"
done < <(jq -r '.[] | (.streamSettings | if type == "string" then fromjson else . end) as $s
| [.remark, .port, .protocol, .listen, .enable, ($s.realitySettings.serverNames[0] // ""),
($s.realitySettings.settings.fingerprint // $s.tlsSettings.settings.fingerprint // "")] | map(tostring) | join("|")' <<<"$list")
done
[[ $shown_reality2 == yes ]] || printf ' %s %s %s %s\n' "$(padr REALITY-2 20)" "$(padr – 12)" "$(padr выкл 8)" "включить: kit net on reality2"
echo
echo "Отпечаток клиента: ${fpl:-?} (сменить: kit net fp firefox)"
}
net_toggle() { # on|off имя [-y]
local act=$1 name row id en listen proto port net yes=no
shift
[[ -n ${1:-} ]] || die "kit net $act имя (например: kit net $act vmess-ws). Список: kit net"
name=$(net_resolve "$1"); [[ ${2:-} == -y || ${2:-} == --yes ]] && yes=yes
row=$(api GET inbounds/list | jq -c --arg n "${name,,}" '[.[] | select((.remark | ascii_downcase) == $n)][0] // empty')
if [[ -z $row ]]; then
if [[ $name == REALITY-2 && $act == on ]]; then reality_add; return; fi
die "Нет подключения «$name». Список: kit net"
fi
id=$(jq -r '.id' <<<"$row"); en=$(jq -r '.enable' <<<"$row"); listen=$(jq -r '.listen' <<<"$row")
proto=$(jq -r '.protocol' <<<"$row"); port=$(jq -r '.port' <<<"$row"); net=$(port_net "$proto")
if [[ $act == off ]]; then
[[ $en == true ]] || { say "$name уже выключен."; return 0; }
if net_main_name "$name" && [[ $yes == no ]]; then
if [[ -t 0 ]]; then
local ans; read -r -p "$name – основной протокол. Выключить? [y/N] " ans
[[ $ans =~ ^[yYдД]$ ]] || { echo "Отменено."; return 0; }
else die "$name – основной протокол. Выключить: kit net off $1 -y"; fi
fi
(api POST "inbounds/setEnable/$id" '{"enable":false}' >/dev/null) || die "Панель не приняла изменение. Ничего не изменилось."
if [[ $listen != 127.0.0.1 ]] && command -v ufw >/dev/null && ufw status 2>/dev/null | grep -q '^Status: active'; then port_ufw delete "$port" "$net"; fi
if [[ $listen != 127.0.0.1 ]]; then say "$name выключен, порт $port/$net закрыт. Вернуть: kit net on $1"; else say "$name выключен. Вернуть: kit net on $1"; fi
echo "Из подписки пропадёт при её обновлении в приложении."
else
[[ $en == true ]] && { say "$name уже включён."; return 0; }
if [[ $listen != 127.0.0.1 ]] && command -v ufw >/dev/null && ufw status 2>/dev/null | grep -q '^Status: active'; then port_ufw allow "$port" "$net"; fi
(api POST "inbounds/setEnable/$id" '{"enable":true}' >/dev/null) || die "Панель не приняла изменение. Ничего не изменилось."
say "$name включён."
fi
}
net_fp() { # отпечаток
local f=${1:-} n=0 id old
[[ -n $f ]] || die "kit net fp отпечаток (chrome, firefox, safari, edge, ios, android, qq, 360, random, randomized)"
f=${f,,}
[[ $f =~ ^(chrome|firefox|safari|edge|ios|android|qq|360|random|randomized)$ ]] || die "Неизвестный отпечаток «$f». Доступны: chrome, firefox, safari, edge, ios, android, qq, 360, random, randomized."
old=$(api GET inbounds/list | jq -r '[.[] | (.streamSettings | if type == "string" then fromjson else . end) | (.realitySettings.settings.fingerprint // .tlsSettings.settings.fingerprint // empty)][0] // "?"')
while read -r id; do
inbound_patch "$id" '.streamSettings |= ((if type == "string" then fromjson else . end)
| (if .realitySettings.settings then .realitySettings.settings.fingerprint = $f else . end)
| (if .tlsSettings.settings then .tlsSettings.settings.fingerprint = $f else . end)
| (if .externalProxy then .externalProxy |= map(if has("fingerprint") then .fingerprint = $f else . end) else . end))' --arg f "$f" \
&& n=$((n + 1))
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."
echo "Подписка обновится сама; вручную сохранённые ссылки замените (kit user link имя)."
echo "Меняйте, если связь пропала сразу у многих, а не из-за одного неудачного раза."
}
cmd_net() {
local sub=${1:-}
[[ -z $sub ]] || shift
case ${sub,,} in
"") net_show ;;
site) [[ $# -gt 0 ]] || set -- --nearby; sni_rotate "$@" ;;
port) port_set "$@" ;;
off | on) net_toggle "${sub,,}" "$@" ;;
fp) net_fp "$@" ;;
vision) case ${1:-} in on) cmd_vision --all ;; off) cmd_vision --all off ;; *) die "kit net vision on|off" ;; esac ;;
*) die "kit net [site | port имя порт | off имя | on имя | fp отпечаток | vision on|off]" ;;
esac
}
# ---------- меню (как у x-ui) ----------
BOX_W=48
box_rule() { local i; for ((i = 0; i < BOX_W; i++)); do printf '─'; done; }
box_top() { printf '╔'; box_rule; printf '╗\n'; }
box_bot() { printf '╚'; box_rule; printf '╝\n'; }
box_sep() { printf '│'; box_rule; printf '│\n'; }
padr() { local n=$(($2 - ${#1})); ((n < 0)) && n=0; printf '%s%*s' "$1" "$n" ''; } # по символам: printf %-Ns считает байты
box_line() { printf '│ %s│\n' "$(padr "$1" $((BOX_W - 2)))"; }
box() { # заголовок, затем пункты; пустой пункт – разделитель
local t=$1 l; shift
box_top; box_line "$t"; box_sep
for l in "$@"; do if [[ -z $l ]]; then box_sep; else box_line "$l"; fi; done
box_bot
}
ask_tty() { read -r -p "$1" REPLY </dev/tty || exit 0; }
ask_num() { ask_tty "$1"; REPLY=${REPLY//[[:space:]]/}; }
pause() { read -r -p "${D}Enter – в меню${N} " _ </dev/tty || exit 0; }
run_action() { ( "$@" ) || true; }
menu_status() {
local sni users st auto
sni=$(sni_targets 2>/dev/null | awk -F'\t' '$2 == "reality" || $2 == "self" {print $4; exit}' || true)
users=$(clients 2>/dev/null | jq '[.[] | select(.email | test("-awg[0-9]*$") | not)] | length' 2>/dev/null || echo "?")
if systemctl is-active -q x-ui; then st="панель работает"; else st="панель НЕ работает"; fi
if auto_enabled; then auto=вкл; else auto=выкл; fi
echo "Сервер: ${HOST:-?} · Сайт маскировки: ${sni:--}"
echo "Состояние: $st · Xray $(xray_version 2>/dev/null || echo '?') · пользователей $users · автообновление $auto"
}
pick_user() {
cmd_list
ask_tty "Имя пользователя: "
PICK=${REPLY//[[:space:]]/}
[[ -n $PICK ]] || return 1
valid_name "$PICK"
}
u_add() {
local name gb days dev a=()
ask_tty "Имя пользователя: "; name=${REPLY//[[:space:]]/}
ask_tty "Лимит трафика, ГБ (Enter – без лимита): "; gb=${REPLY//[[:space:]]/}
ask_tty "Срок, дней (Enter – без срока): "; days=${REPLY//[[:space:]]/}
ask_tty "Устройств (Enter – без ограничения): "; dev=${REPLY//[[:space:]]/}
[[ -z $gb ]] || a+=(--gb "$gb"); [[ -z $days ]] || a+=(--days "$days"); [[ -z $dev ]] || a+=(--devices "$dev")
cmd_add "$name" "${a[@]}"
}
u_link() {
pick_user || return 0
ask_num "1 – ссылки, 2 – AmneziaVPN, 3 – Telegram [1]: "
case ${REPLY:-1} in 2) cmd_link "$PICK" --amnezia ;; 3) cmd_link "$PICK" --telegram ;; *) cmd_link "$PICK" ;; esac
}
u_limit() {
local gb days dev a=()
pick_user || return 0
echo "Enter – не менять, 0 – без ограничения."
ask_tty "Лимит трафика, ГБ: "; gb=${REPLY//[[:space:]]/}
ask_tty "Срок, дней: "; days=${REPLY//[[:space:]]/}
ask_tty "Устройств: "; dev=${REPLY//[[:space:]]/}
[[ -z $gb ]] || a+=(--gb "$gb"); [[ -z $days ]] || a+=(--days "$days"); [[ -z $dev ]] || a+=(--devices "$dev")
((${#a[@]})) || { echo "Ничего не изменено."; return 0; }
cmd_limit "$PICK" "${a[@]}"
}
u_toggle() {
pick_user || return 0
ask_num "1 – выключить, 2 – включить: "
case $REPLY in 1) cmd_toggle "$PICK" false ;; 2) cmd_toggle "$PICK" true ;; *) echo "Отменено." ;; esac
}
u_del() { pick_user || return 0; cmd_del "$PICK"; }
menu_users() {
local c
while :; do
echo
box "Пользователи" "1. Список (трафик, срок, статус)" "2. Добавить" "3. Показать ссылки" "4. Изменить лимит" "5. Выключить или включить" "6. Удалить" "" "0. Назад"
ask_num "Выбор [0-6]: "; c=$REPLY
case $c in
1) run_action cmd_list; pause ;;
2) run_action u_add; pause ;;
3) run_action u_link; pause ;;
4) run_action u_limit; pause ;;
5) run_action u_toggle; pause ;;
6) run_action u_del; pause ;;
0 | "") return 0 ;;
*) echo "Нет такого пункта." ;;
esac
done
}
pick_proto() { # PICK = название подключения (в том числе ещё не созданный REALITY-2)
local -a names=(); local i n en
mapfile -t names < <(api GET inbounds/list | jq -r '.[] | [.remark, (if .enable then "вкл" else "выкл" end)] | @tsv')
names+=("REALITY-2"$'\t'"создать")
i=0
for n in "${names[@]}"; do
i=$((i + 1))
if [[ $n == REALITY-2$'\t'* ]] && api GET inbounds/list | jq -e 'any(.[]; .remark == "REALITY-2")' >/dev/null; then names[i-1]=""; i=$((i)); continue; fi
printf ' %2d %-16s %s\n' "$i" "${n%%$'\t'*}" "${n#*$'\t'}"
done
ask_num "Номер: "
[[ $REPLY =~ ^[0-9]+$ ]] && ((REPLY >= 1 && REPLY <= ${#names[@]})) && [[ -n ${names[REPLY-1]} ]] || { echo "Отменено."; return 1; }
PICK=${names[REPLY-1]%%$'\t'*}
}
n_site() {
ask_num "1 – подобрать автоматически (сначала соседи по подсети), 2 – свой сайт [1]: "
if [[ ${REPLY:-1} == 2 ]]; then
ask_tty "Адрес сайта (например example.com): "
[[ -n ${REPLY//[[:space:]]/} ]] || { echo "Отменено."; return 0; }
sni_rotate "${REPLY//[[:space:]]/}"
else
sni_rotate --nearby
fi
}
n_port() { local p; pick_proto || return 0; p=$PICK; ask_num "Новый порт: "; port_set "$p" "$REPLY"; }
n_toggle() {
local en
pick_proto || return 0
en=$(api GET inbounds/list | jq -r --arg n "$PICK" '[.[] | select(.remark == $n)][0].enable // "none"')
case $en in
true) net_toggle off "$PICK" ;;
false) net_toggle on "$PICK" ;;
*) net_toggle on "$PICK" ;;
esac
}
n_fp() {
local -a fps=(chrome firefox safari edge ios android qq 360 random randomized); local i
for i in "${!fps[@]}"; do printf ' %2d %s\n' "$((i + 1))" "${fps[$i]}"; done
ask_num "Номер: "
[[ $REPLY =~ ^[0-9]+$ ]] && ((REPLY >= 1 && REPLY <= ${#fps[@]})) || { echo "Отменено."; return 0; }
net_fp "${fps[REPLY-1]}"
}
menu_net() {
local c
while :; do
echo
box "Протоколы, порты и маскировка" "1. Показать протоколы и порты" "2. Сменить сайт маскировки" "3. Сменить порт протокола" "4. Выключить или включить протокол" "5. Сменить отпечаток клиента" "6. Второй REALITY на высоком порту" "" "0. Назад"
ask_num "Выбор [0-6]: "; c=$REPLY
case $c in
1) run_action net_show; pause ;;
2) run_action n_site; pause ;;
3) run_action n_port; pause ;;
4) run_action n_toggle; pause ;;
5) run_action n_fp; pause ;;
6) run_action net_toggle on reality2; pause ;;
0 | "") return 0 ;;
*) echo "Нет такого пункта." ;;
esac
done
}
menu_check() {
local c
while :; do
echo
box "Проверка сервера" "1. Быстрая проверка" "2. Глубокая (подключиться клиентом)" "3. Исправить безопасное" "" "0. Назад"
ask_num "Выбор [0-3]: "; c=$REPLY
case $c in
1) run_action cmd_check; pause ;;
2) run_action cmd_check --deep; pause ;;
3) run_action cmd_fix; pause ;;
0 | "") return 0 ;;
*) echo "Нет такого пункта." ;;
esac
done
}
menu_auto() {
local ans
if auto_enabled; then
ask_tty "Автообновление сейчас включено (ночью, только подписанные релизы). Выключить? [y/N] "
[[ $REPLY =~ ^[yYдД]$ ]] && auto_off && say "Автообновление выключено. Обновляться вручную: пункт 4."
else
ask_tty "Автообновление сейчас выключено. Включить? [y/N] "
[[ $REPLY =~ ^[yYдД]$ ]] && auto_on && say "Автообновление включено."
fi
return 0
}
menu_main() {
local c
while :; do
echo
box "3X-UI KIT $KIT_VERSION – управление сервером" "1. Пользователи" "2. Протоколы, порты и маскировка" "3. Проверка сервера" "" \
"4. Обновить kit и подписку" "5. Обновить панель 3X-UI" "6. Автообновление: включить или выключить" "" \
"7. Резервная копия" "8. Версии" "" "9. Панель 3X-UI (меню x-ui)" "0. Выход"
echo
menu_status
echo
ask_num "Выбор [0-9]: "; c=$REPLY
case $c in
1) menu_users ;;
2) menu_net ;;
3) menu_check ;;
4) run_action cmd_update; pause ;;
5) run_action panel_update; pause ;;
6) run_action menu_auto; pause ;;
7) run_action cmd_backup; pause ;;
8) run_action cmd_version; pause ;;
9) command -v x-ui >/dev/null && x-ui || echo "Меню x-ui не найдено." ;;
0 | "") return 0 ;;
*) echo "Нет такого пункта." ;;
esac
done
}
usage() { usage() {
cat <<EOF cat <<EOF
${B}kit${N} $KIT_VERSION – управление сервером 3X-UI KIT ${B}3X-UI KIT $KIT_VERSION${N} – команды (то же самое есть в меню: просто ${B}kit${N})
Пользователи (один пользователь сразу на всех протоколах): kit меню
kit user add имя [--gb 50] [--days 30] [--devices 3] добавить и показать подписку kit user … add, list, link, limit, on, off, del
kit user list трафик, срок, статус kit net … протоколы, порты, сайт маскировки, отпечаток
kit user link имя [--all|--amnezia|--telegram] подписка и QR; --all, --amnezia, --telegram – отдельные ссылки kit check [--deep|--fix] проверка сервера
kit user limit имя [--gb N] [--days N] [--devices N] изменить лимиты (0 – без ограничений) kit update [--panel] обновление (--auto и --manual – автообновление)
kit user off имя / kit user on имя выключить и включить kit backup копия сервера
kit user del имя удалить kit version версии
kit panel update [--force] обновить панель 3X-UI до проверенной версии (ядро и настройки сохраняются)
kit user vision имя|--all [off] включить xtls-rprx-vision для REALITY (у новых пользователей включён сам)
kit user enforce [--dry-run] применить общий лимит сейчас (обычно само, раз в 5 минут)
Сервер:
kit update обновить kit и подписку kit-sub сейчас (пользователи и ссылки не меняются)
kit update --manual выключить автообновление (--auto – включить обратно)
kit backup резервная копия сервера (подключения, ключи, пользователи)
kit check [--deep] проверить сервер: службы, сертификат, подписка, сайт маскировки, права; --deep – ещё и подключиться клиентом
kit fix [--dry-run] исправить безопасное: перезапустить службы, права, автообновление, сертификат
kit port порты подключений; kit port set имя порт – сменить (ufw и ссылки обновятся)
kit reality add [порт] добавить второй REALITY на высоком порту (для сетей, где привычные порты проходят хуже)
kit sni сайты маскировки; kit sni rotate [--nearby] [сайт] – сменить (--nearby – искать в подсети сервера)
kit version версия kit, панели и ядра
EOF EOF
} }
# Регистр и «users» вместо «user» не должны ломать команду; имя пользователя (третье слово) не трогаем. # Регистр и «users» вместо «user» не должны ломать команду; имя пользователя (третье слово) не трогаем.
cmd_key="${1:-} ${2:-}"; cmd_key=${cmd_key,,}; cmd_key=${cmd_key/users /user } cmd_key="${1:-} ${2:-}"; cmd_key=${cmd_key,,}; cmd_key=${cmd_key/users /user }
case "$cmd_key" in case "$cmd_key" in
" ") if [[ -t 0 && -t 1 ]]; then menu_main; else usage; fi ;;
"user add") shift 2; cmd_add "$@" ;; "user add") shift 2; cmd_add "$@" ;;
"user list") cmd_list; update_hint ;; "user list") cmd_list; update_hint ;;
"user link") shift 2; cmd_link "$@" ;; "user link") shift 2; cmd_link "$@" ;;
@@ -1496,14 +1879,16 @@ case "$cmd_key" in
"user del") shift 2; cmd_del "$@" ;; "user del") shift 2; cmd_del "$@" ;;
"user enforce") shift 2; cmd_enforce "$@" ;; "user enforce") shift 2; cmd_enforce "$@" ;;
"user vision") shift 2; cmd_vision "$@" ;; "user vision") shift 2; cmd_vision "$@" ;;
"net "*) shift; cmd_net "$@" ;;
"sni rotate") shift 2; sni_rotate "$@" ;;
"sni "*) [[ -z ${2:-} ]] || die "Команда: kit net site"; sni_show ;;
"port set") shift 2; port_set "$@" ;;
"port "*) [[ -z ${2:-} ]] || die "Команда: kit net port имя порт"; net_show ;;
"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 ;;
"__links "*) links_block "${2:-}" "${3:-}" ;;
"update "*) shift; cmd_update "$@" ;; "update "*) shift; cmd_update "$@" ;;
"reality add") shift 2; reality_add "$@" ;;
"port set") shift 2; port_set "$@" ;;
"port "*) [[ -z ${2:-} ]] || die "Команда: kit port или kit port set имя порт"; port_show ;;
"sni rotate") shift 2; sni_rotate "$@" ;;
"sni "*) [[ -z ${2:-} ]] || die "Команда: kit sni или kit sni rotate [--nearby] [--dry-run] [сайт]"; sni_show ;;
"backup "*) cmd_backup ;; "backup "*) cmd_backup ;;
"check "*) shift; cmd_check "$@" ;; "check "*) shift; cmd_check "$@" ;;
"fix "*) shift; cmd_fix "$@" ;; "fix "*) shift; cmd_fix "$@" ;;