Files
3X-UI_autoVPS/scripts/kit.sh
T

257 lines
13 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
# kit — пользователи 3X-UI + Hysteria2 Kit: один пользователь сразу на всех протоколах.
# https://github.com/itsnotkubrick/Reality_Hysteria2
#
# kit user add имя [--gb 50] [--days 30] [--devices 3]
# kit user list | link имя | limit имя [--gb N] [--days N] | off имя | on имя | del имя
set -Eeuo pipefail
export LC_ALL=C.UTF-8 # ширина колонок по символам, а не байтам
XUI_ENV=/etc/x-ui/install-result.env
KIT_ENV=/etc/kit/kit.env
if [[ -t 1 ]]; then
G=$'\e[32m'; Y=$'\e[33m'; R=$'\e[31m'; B=$'\e[1m'; D=$'\e[2m'; N=$'\e[0m'
else
G=; Y=; R=; B=; D=; N=
fi
say() { printf '%s\n' "${G}==>${N} $*"; }
die() { printf '%s\n' "${R}✗${N} $*" >&2; exit 1; }
[[ $EUID -eq 0 ]] || die "Запустите от root: sudo -i, затем команду ещё раз."
[[ -f $XUI_ENV && -f $KIT_ENV ]] || die "Не найдена установка — сначала поставьте сервер скриптом 3x-ui.sh."
# shellcheck disable=SC1090
. "$XUI_ENV"; . "$KIT_ENV"
API=""
for scheme in https http; do
API="$scheme://127.0.0.1:$XUI_PANEL_PORT/$XUI_WEB_BASE_PATH/panel/api"
curl -fsk -m 5 -o /dev/null -H "Authorization: Bearer $XUI_API_TOKEN" "$API/server/getNewUUID" 2>/dev/null && break
done
api() { # METHOD path [json]
local out
if [[ $1 == GET ]]; then
out=$(curl -sSk -m 20 -H "Authorization: Bearer $XUI_API_TOKEN" "$API/$2")
else
out=$(curl -sSk -m 20 -H "Authorization: Bearer $XUI_API_TOKEN" -H 'Content-Type: application/json' -X "$1" -d "${3:-{\}}" "$API/$2")
fi
[[ $(jq -r '.success' <<<"$out" 2>/dev/null) == true ]] || die "Панель ответила ошибкой: $(jq -r '.msg // .' <<<"$out" 2>/dev/null | head -c 300)"
jq -c '.obj' <<<"$out"
}
# В 3X-UI 3.x у клиента одна запись и в ней одна пара ключей WireGuard и один адрес. Если
# клиент подключён к двум AmneziaWG, в подписку для обоих уходят ключ и адрес одного из них,
# и второй сервер клиента не узнаёт (проверено 2026-09-27). Поэтому к первому AmneziaWG
# подключаем основную запись, а ко второму — запись-«двойник» «имя-awg» с подпиской «<id>-awg»
# (subId в 3X-UI обязан быть уникальным) и теми же лимитами; kit-sub подмешивает её в Clash.
awg_ids() { api GET inbounds/list | jq -r '[.[] | select(.protocol == "amneziawg") | .id] | sort | .[]'; }
non_awg_ids() { api GET inbounds/list | jq -c '[.[] | select(.protocol != "amneziawg") | .id]'; }
awg_attach() { # имя subId [лимит-байт] [срок-мс] [устройств]
local name=$1 sid=$2 total=${3:-0} exp=${4:-0} lim=${5:-0} n=1 id email have
have=$(api GET clients/list | jq -c 'if type == "array" then . else .clients end')
for id in $(awg_ids); do
local esid=$sid
if ((n == 1)); then email=$name
elif ((n == 2)); then email="$name-awg"; esid="$sid-awg"
else email="$name-awg$n"; esid="$sid-awg$n"; fi
n=$((n + 1))
if jq -e --arg e "$email" --argjson i "$id" 'any(.[]; .email == $e and ((.inboundIds // []) | index($i)))' <<<"$have" >/dev/null; then
continue
elif jq -e --arg e "$email" 'any(.[]; .email == $e)' <<<"$have" >/dev/null; then
api POST "clients/$email/attach" "$(jq -nc --argjson i "$id" '{inboundIds: [$i]}')" >/dev/null
else
api POST clients/add "$(jq -nc --arg e "$email" --arg s "$esid" --argjson t "$total" --argjson x "$exp" --argjson l "$lim" --argjson i "$id" \
'{client: {email: $e, subId: $s, totalGB: $t, expiryTime: $x, limitIp: $l, enable: true, comment: "kit"}, inboundIds: [$i]}')" >/dev/null
fi
done
}
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'; }
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)); }
human() { # байты → «1.2 ГБ»
awk -v b="$1" 'BEGIN { split("Б КБ МБ ГБ ТБ", u, " "); i = 1; while (b >= 1024 && i < 5) { b /= 1024; i++ }
printf (i == 1 ? "%d %s" : "%.1f %s"), b, u[i] }'
}
sub_url() { echo "${SUB_BASE}$1"; }
show_link() { # имя subId
local url
url=$(sub_url "$2")
echo
echo "Подписка ${B}$1${N} — все протоколы одной ссылкой. Вставьте в Happ, Hiddify, Karing,"
echo "v2rayN, Clash Verge или FlClash:"
echo
echo "$url"
echo
command -v qrencode >/dev/null && qrencode -t ANSIUTF8 -m 1 "$url"
echo "${D}AmneziaVPN и Telegram: kit user link $1 --all — отдельные ссылки vpn:// и tg://${N}"
}
cmd_add() {
local name=${1:-} gb=0 days=0 devices=0
valid_name "$name"; shift
while [[ $# -gt 0 ]]; do
case $1 in
--gb) gb=$2; shift 2 ;;
--days) days=$2; shift 2 ;;
--devices) devices=$2; shift 2 ;;
*) die "Неизвестный параметр: $1" ;;
esac
done
[[ -z $(client "$name") ]] || die "Пользователь $name уже есть. Ссылка: kit user link $name"
local ids sid body
ids=$(non_awg_ids)
[[ $ids != "[]" ]] || die "На сервере нет подключений."
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,
limitIp: $ip, enable: true, comment: "kit"}, inboundIds: $ids}')
api POST clients/add "$body" >/dev/null
awg_attach "$name" "$sid" "$(gb_bytes "$gb")" "$(days_ms "$days")" "$devices"
say "Пользователь $name добавлен во все протоколы ($(api GET inbounds/list | jq length))$( ((gb)) && echo ", лимит $gb ГБ")$( ((days)) && echo ", на $days дн")."
show_link "$name" "$sid"
}
cmd_link() {
local name=${1:-} all=${2:-} c
valid_name "$name"
c=$(client "$name"); [[ -n $c ]] || die "Нет пользователя $name"
show_link "$name" "$(jq -r '.subId' <<<"$c")"
if [[ $all == --all ]]; then
local sid raw out="" suffix
sid=$(jq -r '.subId' <<<"$c")
for suffix in "" -awg; do
raw=$(curl -fsSk -m 10 -A "v2rayN/7" -H "Host: $HOST" "http://127.0.0.1:$SUB_INTERNAL$SUB_PATH$sid$suffix" 2>/dev/null || true)
grep -q '://' <<<"$raw" || raw=$(base64 -d <<<"$raw" 2>/dev/null || true)
out+=$(grep -E '^(vpn|tg)://' <<<"$raw" || true)$'\n'
done
[[ ${SINGLE:-no} == yes ]] && out=$(sed "s/^\(tg:\/\/proxy?\)\(.*\)port=${MTPROTO_INNER:-10445}/\1\2port=443/" <<<"$out")
echo; grep . <<<"$out" || echo "Отдельных ссылок нет."
fi
}
cmd_list() {
local now
now=$(($(date +%s) * 1000))
clients | jq -r --argjson now "$now" '
map(select(.subId != null)) | group_by(.subId | sub("-awg[0-9]*$"; "")) | map(sort_by(.email | length) as $g | $g[0] + {used: ([$g[] | (.traffic.up // 0) + (.traffic.down // 0)] | add),
seen: ([$g[] | .traffic.lastOnline // 0] | max)}) | sort_by(.email)[]
| [.email, .used, (.totalGB // 0), (.expiryTime // 0), .enable, .seen] | @tsv' |
{
printf "${B}%-18s %-22s %-14s %-10s %s${N}\n" "Пользователь" "Трафик" "До" "Статус" "Был в сети"
while IFS=$'\t' read -r email used total exp en last; do
local tr till st seen
tr="$(human "$used")"; ((total > 0)) && tr="$tr / $(human "$total")"
if ((exp > 0)); then till=$(date -d "@$((exp / 1000))" +%d.%m.%Y); else till="бессрочно"; fi
if [[ $en != true ]]; then st="${R}выключен${N}"
elif ((exp > 0 && exp < $(date +%s) * 1000)); then st="${Y}истёк${N}"
elif ((total > 0 && used >= total)); then st="${Y}лимит${N}"
else st="${G}активен${N}"; fi
if ((last > 0)); then seen=$(date -d "@$((last / 1000))" '+%d.%m %H:%M'); else seen="—"; fi
printf "%-18s %-22s %-14s %-19s %s\n" "$email" "$tr" "$till" "$st" "$seen"
done
}
}
# Меняет все записи пользователя (основную и «двойников» AmneziaWG), каждую — от её собственных данных.
update_user() { # имя jq-фильтр [аргументы jq...]
local name=$1 filter=$2 e rec body
shift 2
for e in $(emails_of "$name"); do
rec=$(client "$e")
body=$(jq -c "$@" "{email, subId, totalGB, expiryTime, limitIp, enable, comment} | $filter" <<<"$rec")
api POST "clients/update/$e" "$body" >/dev/null
done
}
cmd_limit() {
local name=${1:-} f="." gb="" days="" dev=""
valid_name "$name"; shift
[[ -n $(client "$name") ]] || die "Нет пользователя $name"
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 ;;
*) die "Неизвестный параметр: $1" ;;
esac
done
update_user "$name" "$f" --argjson gb "${gb:-0}" --argjson days "${days:-0}" --argjson dev "${dev:-0}"
say "Лимиты $name обновлены (0 — без ограничений)."
}
cmd_toggle() { # имя true|false
valid_name "$1"
[[ -n $(client "$1") ]] || die "Нет пользователя $1"
update_user "$1" ".enable = \$v" --argjson v "$2"
if [[ $2 == true ]]; then say "Пользователь $1 включён."; else say "Пользователь $1 выключен — подписка и подключения не работают."; fi
}
cmd_del() {
local name=${1:-} ans=""
valid_name "$name"
[[ -n $(client "$name") ]] || die "Нет пользователя $name"
if [[ ${2:-} != -y && -t 0 ]]; then
read -rp "Удалить $name со всех протоколов? [y/N] " ans
[[ $ans =~ ^[yYдД]$ ]] || { echo "Отменено."; return; }
fi
local e
for e in $(emails_of "$name"); do api POST "clients/del/$e" >/dev/null; done
say "Пользователь $name удалён, его подписка больше не работает."
}
cmd_about() {
# Пасхалка: kit itsnotkubrick
cat <<'ART'
_ _ _ _ _ _ _
(_) |_ ___ _ __ ___ | |_| | ___ _| |__ _ __(_) ___| | __
| | __/ __| '_ \ / _ \| __| |/ / | | | '_ \| '__| |/ __| |/ /
| | |_\__ \ | | | (_) | |_| <| |_| | |_) | | | | (__| <
|_|\__|___/_| |_|\___/ \__|_|\_\\__,_|_.__/|_| |_|\___|_|\_\
ART
echo
echo " ${B}3X-UI + Hysteria2 Kit${N} — сделал ${B}itsnotkubrick${N}"
echo " на основе панели 3X-UI (MHSanaei/3x-ui) и ядра Xray (XTLS)"
echo
echo " ${D}https://github.com/itsnotkubrick/Reality_Hysteria2${N}"
echo " ${D}it's not Kubrick. it's just a VPN.${N}"
}
usage() {
cat <<EOF
${B}kit${N} — пользователи: один пользователь сразу на всех протоколах
kit user add имя [--gb 50] [--days 30] [--devices 3] добавить и показать подписку
kit user list трафик, срок, статус
kit user link имя [--all] подписка и QR; --all — ещё vpn:// и tg://
kit user limit имя [--gb N] [--days N] [--devices N] изменить лимиты (0 — без ограничений)
kit user off имя / kit user on имя выключить и включить
kit user del имя удалить
kit about об авторе
EOF
}
case "${1:-} ${2:-}" in
"user add") shift 2; cmd_add "$@" ;;
"user list") cmd_list ;;
"user link") shift 2; cmd_link "$@" ;;
"user limit") shift 2; cmd_limit "$@" ;;
"user off") cmd_toggle "${3:-}" false ;;
"user on") cmd_toggle "${3:-}" true ;;
"user del") shift 2; cmd_del "$@" ;;
"about "*|"itsnotkubrick "*|"--version "*) cmd_about ;;
*) usage ;;
esac