Установщик: шаги [1/6]–[6/6], выбор маскировки (домен, сосед по подсети, свой сайт), блок ссылок по группам, итог с блоком «Внимание», адрес в ссылках LINK_HOST (домен в ссылках и MTProto), REALITY-2 не ставится по умолчанию, новая заглушка вместо «Welcome to nginx»

This commit is contained in:
maxdmitrov
2026-10-04 20:28:24 +03:00
parent 346159210b
commit f7ac1ddf49
4 changed files with 238 additions and 87 deletions
+233 -83
View File
@@ -56,7 +56,7 @@ SNI_CANDIDATES=(dl.google.com www.amazon.com www.samsung.com www.yahoo.com)
ALL_PROTOS=(reality reality2 hy2 xhttp ws trojan vmess ss tuic wg awg awg3 mtproto) ALL_PROTOS=(reality reality2 hy2 xhttp ws trojan vmess ss tuic wg awg awg3 mtproto)
# Обычный WireGuard легко распознаётся сетевым оборудованием и работает нестабильно # Обычный WireGuard легко распознаётся сетевым оборудованием и работает нестабильно
# (проверено 2026-09-27: рукопожатие доходит до сервера, ответ – нет). По умолчанию не ставим. # (проверено 2026-09-27: рукопожатие доходит до сервера, ответ – нет). По умолчанию не ставим.
DEFAULT_PROTOS=(reality reality2 hy2 xhttp ws trojan vmess ss tuic awg awg3 mtproto) DEFAULT_PROTOS=(reality hy2 xhttp ws trojan vmess ss tuic awg awg3 mtproto)
declare -A PORTS=([xhttp]=8443 [ws]=2053 [trojan]=2083 [vmess]=2087 [ss]=8388 [tuic]=8444 [wg]=51820 [awg]=51821 [awg3]=51822 [mtproto]=8445) declare -A PORTS=([xhttp]=8443 [ws]=2053 [trojan]=2083 [vmess]=2087 [ss]=8388 [tuic]=8444 [wg]=51820 [awg]=51821 [awg3]=51822 [mtproto]=8445)
PROTOS=(); CREATED=(); OPEN=() PROTOS=(); CREATED=(); OPEN=()
# Режим «всё TCP на 443»: nginx разводит по SNI и путям, подключения слушают только localhost. # Режим «всё TCP на 443»: nginx разводит по SNI и путям, подключения слушают только localhost.
@@ -65,6 +65,7 @@ declare -A INNER=([reality]=10443 [xhttp]=10444 [mtproto]=10445 [web]=10446 [sel
SNI2=""; SNI3="" SNI2=""; SNI3=""
# Свой домен (self-steal): REALITY маскируется под сайт на этом же сервере, а не под чужой. # Свой домен (self-steal): REALITY маскируется под сайт на этом же сервере, а не под чужой.
DOMAIN="" DOMAIN=""
LINK_HOST="" # адрес в ссылках: свой домен, если он выбран, иначе IP
DOMAIN_CERT_DIR=/root/cert/domain DOMAIN_CERT_DIR=/root/cert/domain
SELF_IP_CERT=no # yes – сертификат на IP самоподписанный (Let's Encrypt отказал, пользователь согласился) SELF_IP_CERT=no # yes – сертификат на IP самоподписанный (Let's Encrypt отказал, пользователь согласился)
@@ -73,8 +74,12 @@ if [[ -t 1 ]]; then
else else
G=; Y=; R=; B=; D=; N= G=; Y=; R=; B=; D=; N=
fi fi
say() { printf '%s\n' "${G}==>${N} $*"; } WARNINGS=(); STEP=0; STEPS=6
warn() { printf '%s\n' "${Y}!${N} $*" >&2; } say() { printf ' %s\n' "$*"; } # подробность под шагом
ok() { printf ' %s\n' "${G}✓${N} $*"; }
step() { STEP=$((STEP + 1)); printf '\n%s\n' "${B}[$STEP/$STEPS]${N} $1"; }
warn() { printf '%s\n' "${Y}!${N} $*" >&2; } # сразу (диалоги, ошибки перед остановкой)
later() { WARNINGS+=("$*"); } # в блок «Внимание» в конце
die() { printf '%s\n' "${R}✗${N} $*" >&2; exit 1; } die() { printf '%s\n' "${R}✗${N} $*" >&2; exit 1; }
trap 'die "Ошибка в строке $LINENO. Исправьте причину и запустите скрипт ещё раз."' ERR trap 'die "Ошибка в строке $LINENO. Исправьте причину и запустите скрипт ещё раз."' ERR
@@ -105,6 +110,80 @@ sni_ok() {
| grep -q 'ALPN protocol: h2' | grep -q 'ALPN protocol: h2'
} }
sni_alive() { sni_ok "$1"; }
# --- поиск сайта-прикрытия среди соседей по подсети (общий код kit и установщика) ---
SNI_RE='^[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?(\.[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?)+$'
# Бесплатные и динамические имена (sslip.io, work.gd и т. п.) любят чужие прокси-серверы: под них маскироваться не стоит.
SNI_DYN_RE='(^|\.)(sslip\.io|nip\.io|xip\.io|traefik\.me|work\.gd|duckdns\.org|ddns\.net|hopto\.org|zapto\.org|myftp\.biz|dynu\.net|freeddns\.org|no-ip\.(org|biz|info)|nom\.za|tk|ml|ga|cf|gq)$'
# Известные сайты на чужом IP заметны: сайт-прикрытие должен быть «своим» для подсети сервера.
SNI_BRAND_RE='(^|\.)(google|googleapis|gstatic|youtube|microsoft|windows|apple|icloud|amazon|amazonaws|samsung|yahoo|cloudflare|facebook|instagram|netflix|github|telegram)\.[a-z.]+$'
# У сайта настоящий сертификат: цепочка проходит проверку, имя совпадает.
sni_trusted() {
echo | timeout 8 openssl s_client -connect "$1:443" -servername "$1" -verify_hostname "$1" -verify_return_error 2>/dev/null | grep -q 'Verification: OK'
}
# На «/» отвечает страница (2xx) или переход на этот же сайт; не за Cloudflare, не переход на чужой сайт.
sni_quality() {
local out code loc
out=$(curl -sS -m 10 -o /dev/null -D- "https://$1/" 2>/dev/null | tr -d '\r') || return 1
code=$(awk 'NR == 1 {print $2}' <<<"$out")
grep -qiE '^(server: cloudflare|cf-ray:)' <<<"$out" && return 1
case $code in
2??) return 0 ;;
3??) loc=$(awk 'tolower($1) == "location:" {print $2; exit}' <<<"$out"); [[ $loc == /* || $loc =~ ^https?://(www\.)?${1#www.}(/|:|$) ]] ;;
*) return 1 ;;
esac
}
host_ip() { # IPv4 сервера
local h=${HOST:-}
if [[ $h =~ ^[0-9]{1,3}(\.[0-9]{1,3}){3}$ ]]; then echo "$h"; else getent ahostsv4 "$h" 2>/dev/null | awk 'NR == 1 {print $1}'; fi
}
# Тот же блок /23 (две соседние /24), что и у сервера.
same_net() { # имя
local me ip a b c x y z
me=$(host_ip); [[ -n $me ]] || return 1
IFS=. read -r a b c _ <<<"$me"
while read -r ip; do
[[ -n $ip ]] || continue
IFS=. read -r x y z _ <<<"$ip"
[[ $x == "$a" && $y == "$b" && $((z >> 1)) == $((c >> 1)) ]] && return 0
done < <(getent ahostsv4 "$1" 2>/dev/null | awk '{print $1}' | sort -u)
return 1
}
# Имена из сертификатов соседних адресов /24: заходим на 443 без имени и читаем subjectAltName.
nearby_scan() { # основа "a.b.c"
local me; me=$(host_ip)
seq 1 254 | xargs -P 24 -I{} bash -c '
ip=$1; [ "$ip" = "$2" ] && exit 0
echo | timeout 4 openssl s_client -connect "$ip:443" -tls1_3 -alpn h2 2>/dev/null | openssl x509 -noout -ext subjectAltName 2>/dev/null \
| tr "," "\n" | sed -n "s/^ *DNS://p"' _ "$1.{}" "$me" 2>/dev/null | sort -u | grep -E "$SNI_RE" | head -40 || true
}
# Подходящие сайты: сначала в /24 сервера, если пусто – во втором /24 того же /23. До 8 имён.
nearby_sites() {
local me a b c base n names found=0
set +e; trap - ERR # выполняется в подпроцессе: сбой одной проверки не должен обрывать поиск
me=$(host_ip)
[[ $me =~ ^[0-9]{1,3}(\.[0-9]{1,3}){3}$ ]] || return 0
IFS=. read -r a b c _ <<<"$me"
for base in "$a.$b.$c" "$a.$b.$((c ^ 1))"; do
names=$(nearby_scan "$base")
for n in $names; do
[[ $n == "$me" || $n =~ $SNI_DYN_RE || $n =~ $SNI_BRAND_RE ]] && continue
if same_net "$n" && sni_alive "$n" && sni_trusted "$n" && sni_quality "$n"; then echo "$n"; found=$((found + 1)); ((found >= 8)) && return 0; fi
done
((found > 0)) && return 0
done
return 0
}
# ---------- свой домен (self-steal) ---------- # ---------- свой домен (self-steal) ----------
# Домен должен смотреть на этот сервер: иначе Let's Encrypt не выдаст сертификат, # Домен должен смотреть на этот сервер: иначе Let's Encrypt не выдаст сертификат,
@@ -128,28 +207,30 @@ ask_tty() { # приглашение; ответ – в REPLY; не 0, если
read -r -p "$1" REPLY </dev/tty || { REPLY=""; return 1; } read -r -p "$1" REPLY </dev/tty || { REPLY=""; return 1; }
} }
# Вопрос пользователю: чужой сайт по умолчанию или свой домен. Без ответа – стандартный. # Вопрос пользователю: свой домен, сосед по подсети (по умолчанию) или свой сайт.
choose_masking() { choose_masking() {
echo echo
echo "${B}Под какой сайт маскировать сервер?${N}" echo "${B}Под какой сайт маскировать сервер?${N}"
echo "Чтобы сервер не выделялся, он притворяется обычным сайтом. Имя этого сайта (SNI) видно" echo "Со стороны он будет выглядеть как обычный сайт."
echo "всем по пути, поэтому от выбора зависит, насколько трудно вас заметить."
echo echo
echo " ${B}1)${N} Свой домен ${D}(рекомендуем, если он у вас есть)${N}" echo " ${B}1${N} Свой домен самый надёжный: нужен домен и свободный порт 80"
echo " Сервер притворяется вашим собственным сайтом: на нём настоящая страница и сертификат" echo " ${B}2${N} Сосед по подсети подберу сам, домен не нужен ${G}[рекомендуем]${N}"
echo " Let's Encrypt, а IP и имя совпадают. Нужно заранее: свой домен, его A-запись на IP этого" echo " ${B}3${N} Свой сайт вы вводите адрес сами"
echo " сервера и свободный порт 80. Сертификат установщик получит сам." echo " ${D}Сертификат своего домена виден в публичных журналах сертификатов: связь «домен – сервер» не скрыта.${N}"
echo " ${D}Честно: сертификат домена попадает в публичные журналы сертификатов, поэтому связь${N}"
echo " ${D}«домен – сервер» не скрыта. «Надёжнее» не значит «невидимо».${N}"
echo
echo " ${B}2)${N} Стандартный сайт ${D}(если домена нет)${N}"
echo " Сервер притворяется популярным сайтом (по умолчанию ${SNI_CANDIDATES[0]}). Ничего готовить"
echo " не нужно, работает сразу. Минус: IP вашего сервера не принадлежит этому сайту,"
echo " это можно заметить, и такой сервер может прожить недолго. Сменить сайт позже: kit sni rotate."
echo echo
local d="" prev="" local d="" prev=""
ask_tty "Ваш выбор [2]: " || return 0 ask_tty "Выбор [2]: " || return 0
REPLY=${REPLY//[[:space:]]/} REPLY=${REPLY//[[:space:]]/}
if [[ ${REPLY%.} == 3 ]]; then
while :; do
ask_tty "Адрес сайта, например example.com (пусто – подобрать самому): " || return 0
d=${REPLY,,}; d=${d//[[:space:]]/}
[[ -n $d ]] || return 0
if [[ ! $d =~ $re_host ]]; then warn "Нужно только имя, без https://. Пример: example.com"; continue; fi
if sni_ok "$d"; then SNI=$d; return 0; fi
warn "$d не отвечает по TLS 1.3 и HTTP/2 – REALITY с ним работать не будет. Выберите другой."
done
fi
[[ ${REPLY%.} == 1 ]] || { echo; return 0; } [[ ${REPLY%.} == 1 ]] || { echo; return 0; }
while :; do while :; do
if [[ -n $prev ]]; then ask_tty "Ваш домен [$prev]: " || return 0 if [[ -n $prev ]]; then ask_tty "Ваш домен [$prev]: " || return 0
@@ -274,12 +355,8 @@ kit_banner() {
ART ART
printf '%s' "$N" printf '%s' "$N"
echo echo
echo "${B}3X-UI KIT${N} на основе панели 3X-UI (MHSanaei/3x-ui), ядра Xray и mihomo" echo "${B}3X-UI KIT $KIT_VERSION${N} · it's not Kubrick. it's just a VPN."
echo echo "${D}на основе панели 3X-UI (MHSanaei/3x-ui), ядра Xray и mihomo · github.com/itsnotkubrick/3X-UI_KIT${N}"
echo " https://github.com/itsnotkubrick/3X-UI_KIT"
echo " ${D}it's not Kubrick. it's just a VPN.${N}"
echo
echo "Ниже – данные для входа в панель и подключения."
} }
main() { main() {
@@ -384,7 +461,7 @@ main() {
if [[ $PANEL_SSL == auto ]]; then if [[ $PANEL_SSL == auto ]]; then
if port_busy 80 tcp; then if port_busy 80 tcp; then
PANEL_SSL=none PANEL_SSL=none
warn "Порт 80 занят – сертификат для панели не получить. Панель будет доступна только через SSH-туннель." later "Порт 80 занят – сертификат для панели не получить. Панель будет доступна только через SSH-туннель."
else else
PANEL_SSL=ip PANEL_SSL=ip
fi fi
@@ -399,16 +476,36 @@ main() {
install -m 600 "$ukey" /root/cert/custom/privkey.pem install -m 600 "$ukey" /root/cert/custom/privkey.pem
fi fi
say "Ставлю пакеты: curl, jq, openssl, qrencode, ufw" kit_banner
export DEBIAN_FRONTEND=noninteractive echo
wait_apt_idle echo "Поставлю панель 3X-UI и протоколы на этот сервер. Займёт 3–5 минут."
apt-get update -qq if ! command -v curl >/dev/null || ! command -v openssl >/dev/null; then
apt-get install -y -qq curl jq openssl qrencode ca-certificates iproute2 ufw socat cron unzip >/dev/null export DEBIAN_FRONTEND=noninteractive; wait_apt_idle
apt-get update -qq; apt-get install -y -qq curl openssl ca-certificates iproute2 >/dev/null
fi
local host_given=${HOST:+yes}
HOST=${HOST:-$(public_ip)} HOST=${HOST:-$(public_ip)}
[[ -n $HOST ]] || die "Не удалось узнать внешний IP. Укажите его: --host 1.2.3.4" [[ -n $HOST ]] || die "Не удалось узнать внешний IP. Укажите его: --host 1.2.3.4"
local os_name mem_gb
os_name=$(. /etc/os-release 2>/dev/null; echo "${PRETTY_NAME:-Linux}")
mem_gb=$(awk '/MemTotal/ {printf "%.1f", $2 / 1048576}' /proc/meminfo 2>/dev/null || echo "?")
echo
echo "Сервер: ${B}$HOST${N} · $os_name · ${mem_gb} ГБ памяти"
# IP определился снаружи, а ссылки получат именно его: у части хостеров это не адрес самого сервера.
if [[ -z $host_given && $yes == no && -t 1 ]] && { : </dev/tty; } 2>/dev/null; then
ask_tty "Адрес в ссылках: $HOST. Верно? Enter – да, или введите другой IPv4: " || true
REPLY=${REPLY//[[:space:]]/}
if [[ -n $REPLY ]]; then
[[ $REPLY =~ ^(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 "Нужен IPv4-адрес, например 203.0.113.10 (или запустите с --host)."
HOST=$REPLY
fi
fi
if [[ $HOST =~ ^[0-9.]+$ ]] && ! ip -4 -o addr show 2>/dev/null | grep -qw "$HOST"; then
later "Адрес $HOST не найден на сетевых интерфейсах сервера (бывает за NAT). Если ссылки не работают – запустите с --host и верным IP."
fi
# Маскировка: из флагов, по вопросу пользователю или стандартная. # Маскировка: из флагов, по вопросу пользователю или автоматически (сосед по подсети).
if [[ -n $DOMAIN ]]; then if [[ -n $DOMAIN ]]; then
[[ $PANEL_SSL == ip ]] || die "Свой домен работает с сертификатом панели Let's Encrypt на IP: освободите порт 80 и не указывайте --cert, --key и --panel-ssl none (сертификат для домена установщик получит сам)." [[ $PANEL_SSL == ip ]] || die "Свой домен работает с сертификатом панели Let's Encrypt на IP: освободите порт 80 и не указывайте --cert, --key и --panel-ssl none (сертификат для домена установщик получит сам)."
domain_points_here "$DOMAIN" || die "Исправьте A-запись домена и запустите скрипт снова (DNS обновляется от нескольких минут до нескольких часов)." domain_points_here "$DOMAIN" || die "Исправьте A-запись домена и запустите скрипт снова (DNS обновляется от нескольких минут до нескольких часов)."
@@ -418,25 +515,51 @@ main() {
if [[ -n $DOMAIN ]]; then if [[ -n $DOMAIN ]]; then
SNI=$DOMAIN SNI=$DOMAIN
elif [[ -z $SNI ]]; then elif [[ -z $SNI ]]; then
say "Выбираю сайт для маскировки REALITY" echo
warn "Маскировка под чужой сайт: IP сервера ему не принадлежит, это заметно. Надёжнее свой домен (--domain vpn.example.com) или сайт из подсети хостера (kit sni rotate --nearby)." echo "Ищу сайт-прикрытие в подсети сервера… (около минуты)"
for s in "${SNI_CANDIDATES[@]}"; do local -a nb=()
if sni_ok "$s"; then SNI=$s; break; fi mapfile -t nb < <(nearby_sites | shuf)
done if ((${#nb[@]})); then
[[ -n $SNI ]] || die "Ни один сайт из списка не ответил по TLS 1.3 + HTTP/2. Укажите свой: --sni example.com" SNI=${nb[0]}; SNI2=${nb[1]:-}; SNI3=${nb[2]:-}
ok "найден: ${B}$SNI${N} · та же подсеть · TLS 1.3, h2, сертификат верный"
else
warn "В подсети подходящего сайта не нашёл."
echo " Лучший выход – свой домен: запустите снова с --domain vpn.example.com"
if [[ $yes == no && -t 1 ]] && { : </dev/tty; } 2>/dev/null; then
ask_tty " Продолжить с запасным сайтом из списка? [y/N] " || true
[[ $REPLY =~ ^[yYдД]$ ]] || die "Остановился по вашей просьбе. Ставить можно снова в любой момент."
else
later "Сайт-прикрытие взят из общего списка (в подсети сервера подходящего не нашлось). Надёжнее свой домен или kit net site."
fi
local -a pool=()
mapfile -t pool < <(printf '%s\n' "${SNI_CANDIDATES[@]}" | shuf)
for s in "${pool[@]}"; do
if sni_ok "$s"; then SNI=$s; break; fi
done
[[ -n $SNI ]] || die "Ни один сайт из списка не ответил по TLS 1.3 + HTTP/2. Укажите свой: --sni example.com"
ok "запасной сайт: ${B}$SNI${N}"
fi
elif ! sni_ok "$SNI"; then elif ! sni_ok "$SNI"; then
die "$SNI не отвечает по TLS 1.3 + HTTP/2 – REALITY с ним работать не будет. Выберите другой сайт." die "$SNI не отвечает по TLS 1.3 + HTTP/2 – REALITY с ним работать не будет. Выберите другой сайт."
fi fi
say "Маскировка: ${B}$SNI${N}${DOMAIN:+ (свой домен)}" LINK_HOST=${DOMAIN:-$HOST}
# Для режима «всё на 443» XHTTP и MTProto нужны свои сайты: nginx различает их по SNI. # Для режима «всё на 443» XHTTP и MTProto нужны свои сайты: nginx различает их по SNI.
for s in "${SNI_CANDIDATES[@]}"; do for s in "${SNI_CANDIDATES[@]}"; do
[[ $s == "$SNI" ]] && continue [[ $s == "$SNI" || $s == "$SNI2" || $s == "$SNI3" ]] && continue
if [[ -z $SNI2 ]] && sni_ok "$s"; then SNI2=$s; continue; fi if [[ -z $SNI2 ]] && sni_ok "$s"; then SNI2=$s; continue; fi
[[ -z $SNI3 && -n $SNI2 ]] && { SNI3=$s; break; } [[ -z $SNI3 && -n $SNI2 ]] && { SNI3=$s; break; }
done done
SNI2=${SNI2:-$SNI}; SNI3=${SNI3:-www.cloudflare.com} SNI2=${SNI2:-$SNI}; SNI3=${SNI3:-www.cloudflare.com}
step "Пакеты"
export DEBIAN_FRONTEND=noninteractive
wait_apt_idle
apt-get update -qq
apt-get install -y -qq curl jq openssl qrencode ca-certificates iproute2 ufw socat cron unzip >/dev/null
ok "curl, jq, openssl, qrencode, ufw и другое"
# --- официальный установщик 3X-UI с закреплённой версией --- # --- официальный установщик 3X-UI с закреплённой версией ---
step "Панель 3X-UI $XUI_VERSION"
local panel_port panel_path panel_user panel_pass local panel_port panel_path panel_user panel_pass
panel_port=$(free_port) panel_port=$(free_port)
panel_path=$(rand_str 18) panel_path=$(rand_str 18)
@@ -451,6 +574,7 @@ main() {
# Данные для входа – из файла, который пишет сам установщик. # Данные для входа – из файла, который пишет сам установщик.
connect_panel connect_panel
ok "панель установлена, установщик проверен по SHA256"
if [[ $PANEL_SSL == custom ]]; then if [[ $PANEL_SSL == custom ]]; then
say "Подключаю ваш сертификат к панели" say "Подключаю ваш сертификат к панели"
@@ -467,7 +591,7 @@ main() {
if [[ -n $DOMAIN ]]; then if [[ -n $DOMAIN ]]; then
ip_cert_self_signed_fallback || die "Остановил установку по вашему выбору. Запустите скрипт снова без --domain (панель будет доступна через SSH-туннель) или позже, когда сертификат на IP снова можно будет получить." ip_cert_self_signed_fallback || die "Остановил установку по вашему выбору. Запустите скрипт снова без --domain (панель будет доступна через SSH-туннель) или позже, когда сертификат на IP снова можно будет получить."
else else
warn "Let's Encrypt не выдал сертификат на IP $HOST (порт 80 закрыт у хостера, лимит выпусков или сбой). Ставлю без него: панель будет доступна только через SSH-туннель." later "Let's Encrypt не выдал сертификат на IP $HOST (порт 80 закрыт у хостера, лимит выпусков или сбой). Ставлю без него: панель будет доступна только через SSH-туннель."
PANEL_SSL=none PANEL_SSL=none
TRUSTED=no TRUSTED=no
fi fi
@@ -484,19 +608,22 @@ main() {
fi fi
# --- ядро Xray, совместимое со всеми клиентами --- # --- ядро Xray, совместимое со всеми клиентами ---
step "Ядро Xray $XRAY_CORE и сертификат"
set_xray_core set_xray_core
# --- сертификат для протоколов с TLS --- # --- сертификат для протоколов с TLS ---
setup_tls_cert setup_tls_cert
ok "ядро закреплено, сертификат для протоколов с TLS готов"
# --- подключения: все выбранные протоколы, один subId на пользователя --- # --- подключения: все выбранные протоколы, один subId на пользователя ---
step "Подключения"
EXISTING=$(api GET inbounds/list) EXISTING=$(api GET inbounds/list)
SUBID="" SUBID=""
# «Всё на 443» – для новых установок с доверенным сертификатом. Старую многопортовую # «Всё на 443» – для новых установок с доверенным сертификатом. Старую многопортовую
# установку не переделываем: перенос работающих подключений – осознанное решение. # установку не переделываем: перенос работающих подключений – осознанное решение.
if [[ $TRUSTED == yes && $multi == no ]]; then if [[ $TRUSTED == yes && $multi == no ]]; then
if jq -e 'any(.[]; .remark == "REALITY" and (.listen // "") != "127.0.0.1")' <<<"$EXISTING" >/dev/null; then if jq -e 'any(.[]; .remark == "REALITY" and (.listen // "") != "127.0.0.1")' <<<"$EXISTING" >/dev/null; then
warn "Установка уже работает в режиме с отдельными портами – оставляю его." later "Установка уже работает в режиме с отдельными портами – оставляю его."
else else
SINGLE=yes SINGLE=yes
fi fi
@@ -514,12 +641,15 @@ main() {
# --- подписка: ссылки, Clash/Mihomo и JSON с автоопределением клиента --- # --- подписка: ссылки, Clash/Mihomo и JSON с автоопределением клиента ---
setup_subscription setup_subscription
[[ $SINGLE == yes ]] && setup_nginx [[ $SINGLE == yes ]] && setup_nginx
ok "${#CREATED[@]} подключений, пользователь $NAME"
step "Защита и обновления"
install_kit_cli install_kit_cli
brand_xui_menu brand_xui_menu
# Автообновление kit и kit-sub: только подписанные релизы, выключается kit update --manual. # Автообновление kit и kit-sub: только подписанные релизы, выключается kit update --manual.
/usr/local/bin/kit update --auto >/dev/null 2>&1 || warn "Автообновление не включилось – включите позже: kit update --auto" /usr/local/bin/kit update --auto >/dev/null 2>&1 || later "Автообновление не включилось – включите позже: kit update --auto"
# Общий лимит трафика: у пользователя несколько записей (AmneziaWG считается отдельно), раз в 5 минут их трафик складывается. # Общий лимит трафика: у пользователя несколько записей (AmneziaWG считается отдельно), раз в 5 минут их трафик складывается.
/usr/local/bin/kit __limit-timer on >/dev/null 2>&1 || warn "Проверка общего лимита трафика не включилась – включите позже: kit fix" /usr/local/bin/kit __limit-timer on >/dev/null 2>&1 || later "Проверка общего лимита трафика не включилась – включите позже: kit fix"
# --- файрвол --- # --- файрвол ---
if [[ $UFW == yes ]]; then if [[ $UFW == yes ]]; then
@@ -528,6 +658,10 @@ main() {
setup_ufw setup_ufw
fi fi
step "Проверка"
if /usr/local/bin/kit check --deep >/dev/null 2>&1; then ok "подключения работают: REALITY и XHTTP проверены клиентом с самого сервера"
else later "kit check нашёл замечания – посмотрите: kit check --deep"; fi
# --- итог --- # --- итог ---
local panel_url links local panel_url links
if [[ $SINGLE == yes ]]; then if [[ $SINGLE == yes ]]; then
@@ -559,42 +693,57 @@ main() {
echo "$links" echo "$links"
} >"$RESULT" } >"$RESULT"
kit_banner
echo echo
echo "${G}${B}Готово! 3X-UI работает: ${#CREATED[@]} протоколов.${N}" echo "${G}${B}Готово! Сервер работает.${N}"
echo "${D}${CREATED[*]}${N}"
[[ -n $DOMAIN ]] && echo "Маскировка: свой домен ${B}$DOMAIN${N}, сертификат Let's Encrypt продлевается сам." [[ -n $DOMAIN ]] && echo "Маскировка: свой домен ${B}$DOMAIN${N}, сертификат Let's Encrypt продлевается сам."
[[ $SELF_IP_CERT == yes ]] && echo "${Y}Сертификат на IP самоподписанный:${N} используйте ссылки на отдельные подключения из /root/3x-ui.txt, подписка в приложениях может не открыться." [[ $SELF_IP_CERT == yes ]] && later "Сертификат на IP самоподписанный: пользуйтесь ссылками на отдельные подключения, подписка в приложениях может не открыться."
echo echo
echo "Панель: ${B}$panel_url${N}" echo "${B}ПАНЕЛЬ${N}"
echo "Логин: ${B}$XUI_USERNAME${N}" if [[ $TRUSTED == yes ]]; then
echo "Пароль: ${B}$XUI_PASSWORD${N}" echo " $panel_url"
else
echo " Только через SSH-туннель. На вашем компьютере:"
echo " ssh -L $XUI_PANEL_PORT:127.0.0.1:$XUI_PANEL_PORT root@$HOST"
echo " Затем в браузере: http://127.0.0.1:$XUI_PANEL_PORT/$XUI_WEB_BASE_PATH"
fi
echo " Логин: ${B}$XUI_USERNAME${N} Пароль: ${B}$XUI_PASSWORD${N}"
echo " ${D}(сохранено в $RESULT, файл виден только root)${N}"
echo echo
if [[ $TRUSTED == yes ]]; then if [[ $TRUSTED == yes ]]; then
echo "Подписка для ${B}$NAME${N} – все протоколы одной ссылкой. Вставьте её в Hiddify, v2rayN, Happ," echo "${B}ПОДПИСКА${N} – одна ссылка для всех протоколов"
echo "Clash Verge или FlClash: приложение само получит подходящий формат." echo " ${B}$SUB_URL${N}"
echo echo " Вставьте в Happ, Hiddify, Karing, v2rayN, Clash Verge или FlClash."
echo "$SUB_URL"
echo
qrencode -t ANSIUTF8 -m 1 "$SUB_URL" || true qrencode -t ANSIUTF8 -m 1 "$SUB_URL" || true
if [[ -n $AWG_LINKS ]]; then
echo
echo "AmneziaWG приходит по подписке в Clash Verge и FlClash; для AmneziaVPN – ссылки vpn:// в $RESULT."
fi
else
echo "Без сертификата подписка недоступна снаружи – вот ссылки по одной:"
echo echo
echo "$links" KIT_NO_QR=1 /usr/local/bin/kit __links "$NAME" "$SUBID" || true
else
/usr/local/bin/kit __links "$NAME" "$SUBID" || true
fi fi
echo echo
if [[ -n $PIN && " ${CREATED[*]} " == *" TUIC "* ]]; then local main_list="" spare_list="" c
warn "TUIC со своим сертификатом: в клиенте включите «Разрешить небезопасный» (allow insecure) – отпечаток TUIC-ссылки не передают." for c in "${CREATED[@]}"; do
case $c in REALITY | XHTTP | Hysteria2) main_list+="$c, " ;; *) spare_list+="$c, " ;; esac
done
echo "${B}ПРОТОКОЛЫ${N}"
echo " Основные: ${main_list%, }"
echo " Запасные: ${spare_list%, }"
echo " Порты и выключение лишнего: ${B}kit net${N}"
if ((${#WARNINGS[@]})); then
echo
echo "${B}ВНИМАНИЕ${N}"
local w n=0
for w in "${WARNINGS[@]}"; do
n=$((n + 1)); ((n > 3)) && break
echo " ${Y}!${N} $w"
done
((${#WARNINGS[@]} > 3)) && echo " … и ещё $((${#WARNINGS[@]} - 3)): kit check"
fi fi
echo "Всё это сохранено в ${B}$RESULT${N}."
echo echo
echo "Дополнительные пользователи – одной командой, сразу во все протоколы, со своей подпиской:" echo "${B}ДАЛЬШЕ${N}"
echo " ${B}kit user add sasha --gb 50 --days 30${N}" echo " ${B}kit${N} меню управления"
echo " ${B}kit user list${N} – кто сколько израсходовал и до какого числа" echo " ${B}kit user add имя --gb 50 --days 30${N} добавить пользователя"
echo " ${B}kit check --deep${N} проверить, что всё работает"
echo " ${B}kit backup${N} копия сервера (держите в надёжном месте)"
} }
# Официальный установщик 3X-UI закреплённой версии, сверенный по SHA256. # Официальный установщик 3X-UI закреплённой версии, сверенный по SHA256.
@@ -683,7 +832,7 @@ set_xray_core() {
xray_install_fallback xray_install_fallback
cur_core=$(/usr/local/x-ui/bin/xray-linux-* version 2>/dev/null | awk 'NR==1 {print "v" $2}') cur_core=$(/usr/local/x-ui/bin/xray-linux-* version 2>/dev/null | awk 'NR==1 {print "v" $2}')
fi fi
[[ $cur_core == "$XRAY_CORE" ]] || warn "Не удалось сменить ядро Xray (сейчас $cur_core). Клиенты на Mihomo и sing-box могут не подключиться." [[ $cur_core == "$XRAY_CORE" ]] || later "Не удалось сменить ядро Xray (сейчас $cur_core). Клиенты на Mihomo и sing-box могут не подключиться."
fi fi
} }
@@ -700,9 +849,9 @@ setup_ufw() {
local ssh_port o local ssh_port o
ssh_port=$(ssh_ports) ssh_port=$(ssh_ports)
for o in ${ssh_port:-22}; do OPEN+=("$o/tcp"); done for o in ${ssh_port:-22}; do OPEN+=("$o/tcp"); done
say "Настраиваю ufw: ${OPEN[*]}" ok "ufw: открыто ${#OPEN[@]} портов (${OPEN[*]}), панель не торчит наружу"
for o in "${OPEN[@]}"; do ufw allow "$o" >/dev/null; done for o in "${OPEN[@]}"; do ufw allow "$o" >/dev/null; done
ufw --force enable >/dev/null || warn "ufw не включился (так бывает в контейнерах) – откройте порты у хостера вручную." ufw --force enable >/dev/null || later "ufw не включился (так бывает в контейнерах) – откройте порты у хостера вручную."
} }
# ---------- сертификат ---------- # ---------- сертификат ----------
@@ -767,7 +916,7 @@ add_inbound() {
[[ $net == both ]] && nets="tcp udp" [[ $net == both ]] && nets="tcp udp"
[[ $net == inner ]] && nets=tcp [[ $net == inner ]] && nets=tcp
for n in $nets; do for n in $nets; do
if port_busy "$port" "$n"; then warn "$remark пропущен: порт $port/$n занят"; return; fi if port_busy "$port" "$n"; then later "$remark пропущен: порт $port/$n занят"; return; fi
done done
body=$(jq -nc --arg rm "$remark" --argjson port "$port" --arg p "$protocol" --arg s "$settings" --arg st "$stream" --arg l "$listen" '{ body=$(jq -nc --arg rm "$remark" --argjson port "$port" --arg p "$protocol" --arg s "$settings" --arg st "$stream" --arg l "$listen" '{
remark: $rm, enable: true, listen: $l, port: $port, protocol: $p, settings: $s, streamSettings: $st, remark: $rm, enable: true, listen: $l, port: $port, protocol: $p, settings: $s, streamSettings: $st,
@@ -797,7 +946,7 @@ open_port() { # port net
ext_proxy() { # forceTls(same|tls) alpn(JSON) ext_proxy() { # forceTls(same|tls) alpn(JSON)
local sni="" local sni=""
[[ $HOST =~ ^[0-9.]+$ ]] || sni=$HOST [[ $HOST =~ ^[0-9.]+$ ]] || sni=$HOST
jq -nc --arg f "$1" --arg h "$HOST" --arg sni "$sni" --argjson alpn "${2:-null}" '[{forceTls: $f, dest: $h, port: 443, remark: ""} jq -nc --arg f "$1" --arg h "$HOST" --arg lh "${LINK_HOST:-$HOST}" --arg sni "$sni" --argjson alpn "${2:-null}" '[{forceTls: $f, dest: (if $f == "same" then $lh else $h end), port: 443, remark: ""}
+ (if $f == "tls" then {fingerprint: "chrome", alpn: $alpn} + (if $sni != "" then {sni: $sni} else {} end) else {} end)]' + (if $f == "tls" then {fingerprint: "chrome", alpn: $alpn} + (if $sni != "" then {sni: $sni} else {} end) else {} end)]'
} }
@@ -974,7 +1123,7 @@ telegram_reachable() {
proto_mtproto() { proto_mtproto() {
# MTProto бесполезен, если сам сервер не достаёт до Telegram (некоторые хостеры его блокируют). # MTProto бесполезен, если сам сервер не достаёт до Telegram (некоторые хостеры его блокируют).
if ! telegram_reachable; then if ! telegram_reachable; then
warn "MTProto пропущен: с этого сервера недоступны серверы Telegram – прокси для Telegram здесь работать не будет." later "MTProto пропущен: с этого сервера недоступны серверы Telegram – прокси для Telegram здесь работать не будет."
return return
fi fi
local settings local settings
@@ -1047,6 +1196,7 @@ install_kit_cli() {
install -d -m 700 /etc/kit install -d -m 700 /etc/kit
{ {
printf 'HOST=%q\n' "$HOST" printf 'HOST=%q\n' "$HOST"
printf 'LINK_HOST=%q\n' "${LINK_HOST:-$HOST}"
printf 'SUB_BASE=%q\n' "${SUB_URL%$SUBID}" printf 'SUB_BASE=%q\n' "${SUB_URL%$SUBID}"
printf 'SUB_PATH=%q\n' "$SUB_PATH" printf 'SUB_PATH=%q\n' "$SUB_PATH"
printf 'SUB_INTERNAL=%q\n' "${SUB_INTERNAL:-$SUB_PORT}" printf 'SUB_INTERNAL=%q\n' "${SUB_INTERNAL:-$SUB_PORT}"
@@ -1094,8 +1244,8 @@ stub_site() {
<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Welcome</title><style>body{font-family:-apple-system,Segoe UI,Roboto,sans-serif;margin:0;min-height:100vh;display:grid;place-items:center;background:#f6f7f9;color:#1f2328}main{text-align:center;padding:24px}h1{font-weight:600;font-size:28px}p{color:#57606a}</style></head><body><main><h1>Site is under construction</h1><p>Please check back soon.</p></main></body></html> <!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Welcome</title><style>body{font-family:-apple-system,Segoe UI,Roboto,sans-serif;margin:0;min-height:100vh;display:grid;place-items:center;background:#f6f7f9;color:#1f2328}main{text-align:center;padding:24px}h1{font-weight:600;font-size:28px}p{color:#57606a}</style></head><body><main><h1>Site is under construction</h1><p>Please check back soon.</p></main></body></html>
HTML HTML
;; ;;
2) cat <<'HTML' 2) cat <<HTML
<!DOCTYPE html><html><head><title>Welcome to nginx!</title><style>html{color-scheme:light dark}body{width:35em;margin:0 auto;font-family:Tahoma,Verdana,Arial,sans-serif}</style></head><body><h1>Welcome to nginx!</h1><p>If you see this page, the nginx web server is successfully installed and working. Further configuration is required.</p><p>For online documentation and support please refer to <a href="http://nginx.org/">nginx.org</a>.<br/>Commercial support is available at <a href="http://nginx.com/">nginx.com</a>.</p><p><em>Thank you for using nginx.</em></p></body></html> <!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Cumulo Developer Hub</title><style>body{margin:0;font-family:-apple-system,Segoe UI,Roboto,sans-serif;background:#f6f8fa;color:#1f2328}header{background:#0b3d5c;color:#fff;padding:40px 20px;text-align:center}h1{margin:0 0 6px;font-size:26px}p{margin:0;color:#b6d4e8}main{max-width:760px;margin:32px auto;padding:0 20px;display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:16px}.c{background:#fff;border:1px solid #d0d7de;border-radius:8px;padding:18px}.c b{display:block;margin-bottom:6px}.c span{color:#656d76;font-size:14px}footer{text-align:center;color:#8c959f;font-size:13px;padding:24px}</style></head><body><header><h1>Cumulo Developer Hub</h1><p>Guides, SDKs and release notes</p></header><main><div class="c"><b>Documentation</b><span>Getting started, tutorials and API reference.</span></div><div class="c"><b>SDKs</b><span>Client libraries for Python, Go and JavaScript.</span></div><div class="c"><b>Release notes</b><span>What changed in the latest platform release.</span></div></main><footer>&copy; $year Cumulo Cloud</footer></body></html>
HTML HTML
;; ;;
3) cat <<HTML 3) cat <<HTML
@@ -1387,12 +1537,12 @@ install_kit_sub() {
install_kit_sub_file install_kit_sub_file
if [[ $SINGLE == yes ]]; then if [[ $SINGLE == yes ]]; then
# За nginx: слушаем только localhost, TLS снимает nginx на 443. # За nginx: слушаем только localhost, TLS снимает nginx на 443.
jq -n --arg path "$SUB_PATH" --argjson port "${INNER[sub]}" --arg up "http://127.0.0.1:$SUB_INTERNAL" --arg host "$HOST" \ jq -n --arg path "$SUB_PATH" --argjson port "${INNER[sub]}" --arg up "http://127.0.0.1:$SUB_INTERNAL" --arg host "$HOST" --arg lh "${LINK_HOST:-$HOST}" \
'{listen: "127.0.0.1", port: $port, path: $path, upstream: $up, host: $host}' >/etc/kit-sub/config.json '{listen: "127.0.0.1", port: $port, path: $path, upstream: $up, host: $host} + (if $lh != $host then {link_host: $lh} else {} end)' >/etc/kit-sub/config.json
else else
jq -n --arg path "$SUB_PATH" --argjson port "$SUB_PORT" --arg up "http://127.0.0.1:$SUB_INTERNAL" \ jq -n --arg path "$SUB_PATH" --argjson port "$SUB_PORT" --arg up "http://127.0.0.1:$SUB_INTERNAL" \
--arg cert "$CERT" --arg key "$KEY" --arg host "$HOST" \ --arg cert "$CERT" --arg key "$KEY" --arg host "$HOST" --arg lh "${LINK_HOST:-$HOST}" \
'{listen: "0.0.0.0", port: $port, path: $path, upstream: $up, cert: $cert, key: $key, host: $host}' >/etc/kit-sub/config.json '{listen: "0.0.0.0", port: $port, path: $path, upstream: $up, cert: $cert, key: $key, host: $host} + (if $lh != $host then {link_host: $lh} else {} end)' >/etc/kit-sub/config.json
fi fi
chmod 600 /etc/kit-sub/config.json chmod 600 /etc/kit-sub/config.json
if [[ $SINGLE == yes ]]; then if [[ $SINGLE == yes ]]; then
@@ -1420,7 +1570,7 @@ sub_links() {
[[ -z ${SUB_INTERNAL:-} && $TRUSTED == yes ]] && scheme=https [[ -z ${SUB_INTERNAL:-} && $TRUSTED == yes ]] && scheme=https
for i in $(seq 1 "$tries"); do for i in $(seq 1 "$tries"); do
# Настоящий адрес в Host – 3X-UI подставит его в ссылки. # Настоящий адрес в Host – 3X-UI подставит его в ссылки.
raw=$(curl -fsSk -m 10 -A "v2rayN/7.0" -H "Host: $HOST:$SUB_PORT" "$scheme://127.0.0.1:$port$SUB_PATH$id" 2>/dev/null) && [[ -n $raw ]] && break raw=$(curl -fsSk -m 10 -A "v2rayN/7.0" -H "Host: ${LINK_HOST:-$HOST}:$SUB_PORT" "$scheme://127.0.0.1:$port$SUB_PATH$id" 2>/dev/null) && [[ -n $raw ]] && break
raw=""; sleep 2 raw=""; sleep 2
done done
if grep -q '://' <<<"$raw"; then echo "$raw"; else base64 -d <<<"$raw" 2>/dev/null || true; fi if grep -q '://' <<<"$raw"; then echo "$raw"; else base64 -d <<<"$raw" 2>/dev/null || true; fi
@@ -1603,8 +1753,8 @@ PY
if [[ -f /etc/kit/kit.env ]]; then if [[ -f /etc/kit/kit.env ]]; then
install_kit_file install_kit_file
brand_xui_menu brand_xui_menu
/usr/local/bin/kit update --auto >/dev/null 2>&1 || warn "Автообновление не включилось – включите позже: kit update --auto" /usr/local/bin/kit update --auto >/dev/null 2>&1 || later "Автообновление не включилось – включите позже: kit update --auto"
/usr/local/bin/kit __limit-timer on >/dev/null 2>&1 || warn "Проверка общего лимита трафика не включилась – включите позже: kit fix" /usr/local/bin/kit __limit-timer on >/dev/null 2>&1 || later "Проверка общего лимита трафика не включилась – включите позже: kit fix"
fi fi
# Порты – по подключениям из копии: что слушает не только localhost, то и открываем. # Порты – по подключениям из копии: что слушает не только localhost, то и открываем.
Binary file not shown.
+1 -1
View File
@@ -211,7 +211,7 @@ class Handler(http.server.BaseHTTPRequestHandler):
if not SUB_ID.match(sub_id): if not SUB_ID.match(sub_id):
return self.send_plain(404, "404 page not found") return self.send_plain(404, "404 page not found")
ua = self.headers.get("User-Agent", "") ua = self.headers.get("User-Agent", "")
host = self.headers.get("Host", CONF.get("host", "")) host = CONF.get("link_host") or self.headers.get("Host", CONF.get("host", ""))
accept = self.headers.get("Accept", "") accept = self.headers.get("Accept", "")
code, headers, body = upstream(sub_id, ua, host, accept) code, headers, body = upstream(sub_id, ua, host, accept)
if code is None: if code is None:
+4 -3
View File
@@ -122,7 +122,7 @@ collect_links() {
raw="" raw=""
# Внутренняя подписка отвечает по http (всё на 443) или по https (свой порт с сертификатом). # Внутренняя подписка отвечает по http (всё на 443) или по https (свой порт с сертификатом).
for scheme in http https; do for scheme in http https; do
raw=$(curl -fsSk -m 10 -A "v2rayN/7" -H "Host: $HOST" "$scheme://127.0.0.1:$SUB_INTERNAL$SUB_PATH$sid$suffix" 2>/dev/null) && [[ -n $raw ]] && break raw=$(curl -fsSk -m 10 -A "v2rayN/7" -H "Host: ${LINK_HOST:-$HOST}" "$scheme://127.0.0.1:$SUB_INTERNAL$SUB_PATH$sid$suffix" 2>/dev/null) && [[ -n $raw ]] && break
raw="" raw=""
done done
grep -q '://' <<<"$raw" || raw=$(base64 -d <<<"$raw" 2>/dev/null || true) grep -q '://' <<<"$raw" || raw=$(base64 -d <<<"$raw" 2>/dev/null || true)
@@ -1232,6 +1232,7 @@ nearby_scan() { # основа "a.b.c"
# Подходящие сайты: сначала в /24 сервера, если пусто – во втором /24 того же /23. До 8 имён. # Подходящие сайты: сначала в /24 сервера, если пусто – во втором /24 того же /23. До 8 имён.
nearby_sites() { nearby_sites() {
local me a b c base n names found=0 local me a b c base n names found=0
set +e; trap - ERR # выполняется в подпроцессе: сбой одной проверки не должен обрывать поиск
me=$(host_ip) me=$(host_ip)
[[ $me =~ ^[0-9]{1,3}(\.[0-9]{1,3}){3}$ ]] || return 0 [[ $me =~ ^[0-9]{1,3}(\.[0-9]{1,3}){3}$ ]] || return 0
IFS=. read -r a b c _ <<<"$me" IFS=. read -r a b c _ <<<"$me"
@@ -1513,7 +1514,7 @@ links_block() { # имя subId
[[ -n $raw ]] || { echo "Отдельных ссылок нет."; return 0; } [[ -n $raw ]] || { echo "Отдельных ссылок нет."; return 0; }
local script local script
IFS= read -r -d '' script <<'PY' || true IFS= read -r -d '' script <<'PY' || true
import base64, json, re, subprocess, shutil, sys import base64, json, os, re, subprocess, shutil, sys
from urllib.parse import urlparse, parse_qs, unquote from urllib.parse import urlparse, parse_qs, unquote
name = sys.argv[1] name = sys.argv[1]
MAIN = ["REALITY", "XHTTP", "Hysteria2"] MAIN = ["REALITY", "XHTTP", "Hysteria2"]
@@ -1561,7 +1562,7 @@ for title, group in (("Основные", main), ("Запасные", spare)):
print(title) print(title)
for it in group: for it in group:
show(*it) show(*it)
if not qr_done and it[0] == "REALITY" and shutil.which("qrencode"): if not qr_done and it[0] == "REALITY" and shutil.which("qrencode") and not os.environ.get("KIT_NO_QR"):
sys.stdout.flush() sys.stdout.flush()
subprocess.run(["qrencode", "-t", "ANSIUTF8", "-m", "1", it[4]]) subprocess.run(["qrencode", "-t", "ANSIUTF8", "-m", "1", it[4]])
print() print()