Пользователи: kit — один пользователь сразу на всех протоколах
- scripts/kit.sh (команда kit на сервере): user add/list/link/limit/off/on/del; общие трафик, лимит, срок и число устройств на все протоколы - Установщик создаёт подключения без клиентов и первого пользователя сразу во все, имя подписки случайное (чужую подписку не угадать по имени) - AmneziaWG: у записи 3X-UI одна пара ключей WireGuard, поэтому ко второму AmneziaWG подключается запись-«двойник» с подпиской «-awg»; kit работает с обеими как с одной - kit-sub фильтрует по содержимому: Clash-приложениям без AmneziaWG он вырезается, приложениям со ссылками не отдаются vpn:// и tg:// Проверено: три пользователя — Mihomo по подписке 10/10 у каждого, официальный клиент AmneziaWG — оба варианта у каждого; выключение, лимиты и удаление действуют на все записи.
This commit is contained in:
+236
@@ -0,0 +1,236 @@
|
||||
#!/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
|
||||
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 удалён, его подписка больше не работает."
|
||||
}
|
||||
|
||||
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 имя удалить
|
||||
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 "$@" ;;
|
||||
*) usage ;;
|
||||
esac
|
||||
Reference in New Issue
Block a user