--restore: файлы настроек из копии – только по белому списку имён, без подключения в основной shell
kit-backup.env больше не подключается через «.» в установщике: значения читаются в подоболочке (env_get). safe_env проверяет имена для каждого файла (kit-backup.env, install-result.env, kit.env); лишнее имя – «Это не резервная копия 3X-UI KIT…». RESULT, XUI_ENV, PATH, IFS, KIT_* из архива не принимаются, HOST в kit.env должен совпадать с адресом копии, порт панели – число. Тест: tools/test/restore_env_test.py.
This commit is contained in:
+1
-1
@@ -9,7 +9,7 @@
|
||||
* `kit check --deep` проверяет подключение к серверу, `--fix` лечит безопасное. Общий лимит трафика считается по всем подключениям пользователя, а в подписке Clash есть «Авто»: клиент сам выбирает быстрейший протокол. DNS в подписках теперь по DoH через прокси: запросы имён не гуляют где попало.
|
||||
* Ссылка подписки в браузере открывается страницей с трафиком, сроком и ссылками (а не белым экраном). Рядом с подпиской теперь ссылки «открыть в приложении»: одно нажатие, и подписка добавлена с названием. Со своим доменом панель и подписку можно открывать и по домену: `--panel-on domain` при установке или `kit net panel domain` потом. Вернулся `--restore`: чистый сервер поднимается из `kit backup`, как из резервной копии настроек телефона. Ссылки `kit user link --amnezia` и `--telegram` для AmneziaVPN и Telegram теперь одной командой, а исправление времени на Debian 13 прислал [xlok1e](https://github.com/xlok1e), спасибо.
|
||||
* Раздельная маршрутизация: `kit net split on` – и через VPN идёт только список популярных иностранских сервисов (нейросети, видео, соцсети, мессенджеры, GitHub, Google и другие), а всё остальное напрямую. Правила приезжают вместе с подпиской, домашним ничего настраивать не нужно: приложения на Mihomo (Clash Verge, FlClash, Mihomo Party) берут их из подписки, Happ – заголовком, приложения на JSON-подписке Xray – из конфига. DNS не течёт, проверено клиентами Mihomo и Xray. Список в `/etc/kit-sub/rules.yaml`, проверка по базам категорий `kit net split check`, после правки `kit net split apply`, он входит в `kit backup`. Идея и первый набросок – [VasilevVitalii](https://github.com/VasilevVitalii), спасибо. У v2rayN, Karing, Hiddify и приложений с обычным списком ссылок подписка прежняя.
|
||||
* Исправил `--restore` на сервер с другим IP: ссылки части протоколов (адреса в таблице `hosts` панели и в настройках kit-sub) оставались со старым IP, а проверка панели после запуска могла ложно сообщить «Панель не отвечает», если панель стартует не сразу.
|
||||
* Исправил `--restore` на сервер с другим IP: ссылки части протоколов (адреса в таблице `hosts` панели и в настройках kit-sub) оставались со старым IP, а проверка панели после запуска могла ложно сообщить «Панель не отвечает», если панель стартует не сразу. А файлы настроек из копии `--restore` теперь читает строго по списку, как охранник на входе: лишнее имя – и копия не принимается, ничего не меняется.
|
||||
* Исправил `kit backup`: из `/root` он падал с «No such file or directory», работал только из `/`. Теперь работает откуда угодно. Если токен API панели в `/etc/x-ui/install-result.env` не подошёл (так бывает после обновления с 1.0), `kit` сам выпускает новый и сохраняет его, а если панель всё же не отвечает – говорит по-человечески (код ответа и вероятная причина), а не «Unsupported HTTP version in response».
|
||||
* JSON-подписка снова открывается в приложениях на Xray: там, где у vless не было `encryption`, подписка теперь дописывает `none`.
|
||||
|
||||
|
||||
@@ -238,6 +238,9 @@ bash 3x-ui.sh --restore /root/kit-backup-ДАТА.tar.gz -y
|
||||
Подключения, ключи, пользователи и их лимиты вернутся прежними. Если адрес сервера сменился, подписку клиентам нужно
|
||||
добавить заново, а на домене ничего менять не придётся: достаточно переключить DNS.
|
||||
|
||||
Восстанавливайте только из своей копии. Архив с чужими путями, ссылками или лишними настройками скрипт
|
||||
не примет («Это не резервная копия 3X-UI KIT…») и ничего не изменит.
|
||||
|
||||
## Параметры
|
||||
|
||||
| Параметр | Что делает |
|
||||
|
||||
+36
-14
@@ -1653,14 +1653,25 @@ sub_links() {
|
||||
if grep -q '://' <<<"$raw"; then echo "$raw"; else base64 -d <<<"$raw" 2>/dev/null || true; fi
|
||||
}
|
||||
|
||||
# Файл настроек из копии можно подключать, только если в нём нет ничего, кроме
|
||||
# ИМЯ=значение без подстановок и команд.
|
||||
safe_env() {
|
||||
[[ -r $1 ]] || die "В копии нет файла ${1##*/}."
|
||||
grep -qvE "^([A-Z][A-Z0-9_]*=([A-Za-z0-9._:/@%+,=_-]*|'[^']*'))?$" "$1" && die "В копии подозрительный файл ${1##*/} – не восстанавливаю."
|
||||
# Файл настроек из копии можно читать, только если в нём нет ничего, кроме
|
||||
# ИМЯ=значение без подстановок и команд, и все имена – из списка для этого файла.
|
||||
# Иначе чужой архив подменил бы переменные установщика или kit (RESULT, PATH, KIT_VERSION…).
|
||||
safe_env() { # файл имя...
|
||||
local f=$1 n; shift
|
||||
[[ -r $f ]] || die "В копии нет файла ${f##*/}."
|
||||
grep -qvE "^([A-Z][A-Z0-9_]*=([A-Za-z0-9._:/@%+,=_-]*|'[^']*'))?$" "$f" \
|
||||
&& die "Это не резервная копия 3X-UI KIT или она повреждена (файл ${f##*/}). Ничего не менял."
|
||||
while IFS= read -r n; do
|
||||
[[ " $* " == *" $n "* ]] || die "Это не резервная копия 3X-UI KIT или она повреждена (лишнее имя $n в ${f##*/}). Ничего не менял."
|
||||
done < <(grep -oE '^[A-Z][A-Z0-9_]*' "$f")
|
||||
return 0
|
||||
}
|
||||
|
||||
# Одно значение из файла, проверенного safe_env: читаем в подоболочке, основной shell не трогаем.
|
||||
env_get() { # файл имя
|
||||
( . "$1"; printf '%s' "${!2:-}" )
|
||||
}
|
||||
|
||||
# Меняет старый IP на новый в базе панели и файлах kit (только целиком, 1.2.3.4 не заденет 11.2.3.45).
|
||||
replace_host() { # старый новый файлы...
|
||||
python3 - "$@" <<'PY'
|
||||
@@ -1736,13 +1747,22 @@ with tarfile.open(sys.argv[1], "r:gz") as t:
|
||||
t.extractall(sys.argv[2], members=ms)
|
||||
PY
|
||||
local f
|
||||
for f in "$tmp/kit-backup.env" "$tmp/etc/x-ui/install-result.env" "$tmp/etc/kit/kit.env"; do if [[ -f $f ]]; then safe_env "$f"; fi; done
|
||||
safe_env "$tmp/kit-backup.env" BACKUP_KIT_VERSION BACKUP_HOST BACKUP_SSL BACKUP_DATE
|
||||
safe_env "$tmp/etc/x-ui/install-result.env" XUI_USERNAME XUI_PASSWORD XUI_PANEL_PORT XUI_WEB_BASE_PATH XUI_ACCESS_URL XUI_API_TOKEN XUI_DB_TYPE
|
||||
[[ $(env_get "$tmp/etc/x-ui/install-result.env" XUI_PANEL_PORT) =~ ^[0-9]{1,5}$ ]] \
|
||||
|| die "Это не резервная копия 3X-UI KIT или она повреждена (порт панели). Ничего не менял."
|
||||
if [[ -f $tmp/etc/kit/kit.env ]]; then
|
||||
safe_env "$tmp/etc/kit/kit.env" HOST LINK_HOST PANEL_ON SUB_BASE SUB_PATH SUB_INTERNAL SINGLE MTPROTO_INNER
|
||||
fi
|
||||
python3 -c "import sqlite3,sys; c=sqlite3.connect(sys.argv[1]); assert c.execute('PRAGMA integrity_check').fetchone()[0]=='ok'; c.execute('SELECT count(*) FROM inbounds')" \
|
||||
"$tmp/etc/x-ui/x-ui.db" 2>/dev/null || die "База панели в копии повреждена. Ничего не менял."
|
||||
|
||||
local BACKUP_HOST="" BACKUP_SSL="" BACKUP_DATE="" BACKUP_KIT_VERSION=""
|
||||
# shellcheck disable=SC1090
|
||||
. "$tmp/kit-backup.env"
|
||||
local BACKUP_HOST BACKUP_SSL BACKUP_DATE BACKUP_KIT_VERSION
|
||||
BACKUP_HOST=$(env_get "$tmp/kit-backup.env" BACKUP_HOST)
|
||||
BACKUP_SSL=$(env_get "$tmp/kit-backup.env" BACKUP_SSL)
|
||||
BACKUP_DATE=$(env_get "$tmp/kit-backup.env" BACKUP_DATE)
|
||||
BACKUP_KIT_VERSION=$(env_get "$tmp/kit-backup.env" BACKUP_KIT_VERSION)
|
||||
[[ $BACKUP_HOST =~ ^[A-Za-z0-9.:-]+$ ]] || die "Это не резервная копия 3X-UI KIT или она повреждена (адрес сервера). Ничего не менял."
|
||||
[[ $BACKUP_SSL =~ ^(ip|custom|none)$ ]] || die "В копии нет данных о сертификате."
|
||||
PANEL_SSL=$BACKUP_SSL
|
||||
# Домен переезжает вместе с сервером (поменяйте A-запись), IP – нет.
|
||||
@@ -1777,12 +1797,14 @@ PY
|
||||
# проверенные значения (путь подписки, порты, режим).
|
||||
SINGLE=no; SUB_PATH=""; SUB_INTERNAL=""; SUB_PORT=""
|
||||
if [[ -f $tmp/etc/kit/kit.env ]]; then
|
||||
SINGLE=$(. "$tmp/etc/kit/kit.env"; echo "${SINGLE:-no}")
|
||||
SUB_PATH=$(. "$tmp/etc/kit/kit.env"; echo "${SUB_PATH:-}")
|
||||
LINK_HOST=$(. "$tmp/etc/kit/kit.env"; echo "${LINK_HOST:-}")
|
||||
SINGLE=$(env_get "$tmp/etc/kit/kit.env" SINGLE); SINGLE=${SINGLE:-no}
|
||||
SUB_PATH=$(env_get "$tmp/etc/kit/kit.env" SUB_PATH)
|
||||
LINK_HOST=$(env_get "$tmp/etc/kit/kit.env" LINK_HOST)
|
||||
[[ $LINK_HOST =~ ^[A-Za-z0-9.:-]*$ ]] || LINK_HOST=""
|
||||
SUB_INTERNAL=$(. "$tmp/etc/kit/kit.env"; echo "${SUB_INTERNAL:-}")
|
||||
[[ $SINGLE =~ ^(yes|no)$ && $SUB_PATH =~ ^/[A-Za-z0-9_-]+/$ && $SUB_INTERNAL =~ ^[0-9]{1,5}$ ]] \
|
||||
SUB_INTERNAL=$(env_get "$tmp/etc/kit/kit.env" SUB_INTERNAL)
|
||||
# kit потом подключает этот файл от root: адрес в нём должен быть тем же, что в копии.
|
||||
[[ $SINGLE =~ ^(yes|no)$ && $SUB_PATH =~ ^/[A-Za-z0-9_-]+/$ && $SUB_INTERNAL =~ ^[0-9]{1,5}$ \
|
||||
&& $(env_get "$tmp/etc/kit/kit.env" HOST) == "$BACKUP_HOST" ]] \
|
||||
|| die "В копии странные настройки kit (kit.env) – не восстанавливаю."
|
||||
fi
|
||||
if [[ -f $tmp/etc/kit-sub/config.json ]]; then
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
"""Проверки разбора файлов настроек из резервной копии (safe_env, env_get в 3x-ui.sh):
|
||||
python3 tools/test/restore_env_test.py"""
|
||||
import os, re, subprocess, tempfile, unittest
|
||||
|
||||
root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
src = open(os.path.join(root, "scripts", "3x-ui.sh"), encoding="utf-8").read()
|
||||
funcs = "".join(re.search(r"^%s\(\) \{.*?^\}\n" % n, src, re.S | re.M).group(0) for n in ("safe_env", "env_get"))
|
||||
|
||||
BACKUP = ("BACKUP_KIT_VERSION", "BACKUP_HOST", "BACKUP_SSL", "BACKUP_DATE")
|
||||
XUI = ("XUI_USERNAME", "XUI_PASSWORD", "XUI_PANEL_PORT", "XUI_WEB_BASE_PATH", "XUI_ACCESS_URL", "XUI_API_TOKEN", "XUI_DB_TYPE")
|
||||
KIT = ("HOST", "LINK_HOST", "PANEL_ON", "SUB_BASE", "SUB_PATH", "SUB_INTERNAL", "SINGLE", "MTPROTO_INNER")
|
||||
|
||||
# Как пишут kit backup, установщик 3X-UI и install_kit_cli (printf %q).
|
||||
GOOD = {
|
||||
"kit-backup.env": "BACKUP_KIT_VERSION=1.2\nBACKUP_HOST=1.2.3.4\nBACKUP_SSL=ip\nBACKUP_DATE=2026-10-06\n",
|
||||
"install-result.env": "XUI_USERNAME=abcDEF1234\nXUI_PASSWORD=Pass1234567890abcdef\nXUI_PANEL_PORT=31234\n"
|
||||
"XUI_WEB_BASE_PATH=AbCdEf123456789012\nXUI_ACCESS_URL=https://1.2.3.4:31234/AbCdEf123456789012\n"
|
||||
"XUI_API_TOKEN=tok_ABCdef0123456789\nXUI_DB_TYPE=sqlite\n",
|
||||
"kit.env": "HOST=1.2.3.4\nLINK_HOST=1.2.3.4\nPANEL_ON=ip\nSUB_BASE=https://1.2.3.4/AbC123/\nSUB_PATH=/AbC123/\n"
|
||||
"SUB_INTERNAL=2096\nSINGLE=yes\nMTPROTO_INNER=''\n",
|
||||
}
|
||||
NAMES = {"kit-backup.env": BACKUP, "install-result.env": XUI, "kit.env": KIT}
|
||||
|
||||
|
||||
def run(name, text, extra=""):
|
||||
d = tempfile.mkdtemp()
|
||||
p = os.path.join(d, name)
|
||||
open(p, "w").write(text)
|
||||
script = ('set -Eeuo pipefail\ndie() { echo "DIE: $*"; exit 1; }\n' + funcs
|
||||
+ 'RESULT=/root/3x-ui.txt\nsafe_env "$1" ' + " ".join(NAMES[name]) + '\necho OK\n' + extra)
|
||||
r = subprocess.run(["bash", "-c", script, "t", p], capture_output=True, text=True)
|
||||
return r.returncode, r.stdout + r.stderr
|
||||
|
||||
|
||||
class SafeEnv(unittest.TestCase):
|
||||
def test_good_copy(self):
|
||||
for name, text in GOOD.items():
|
||||
code, out = run(name, text)
|
||||
self.assertEqual(code, 0, out)
|
||||
self.assertIn("OK", out)
|
||||
|
||||
def test_values(self):
|
||||
code, out = run("kit-backup.env", GOOD["kit-backup.env"],
|
||||
'echo "[$(env_get "$1" BACKUP_HOST)|$(env_get "$1" BACKUP_SSL)|$(env_get "$1" NOPE)]"\n')
|
||||
self.assertIn("[1.2.3.4|ip|]", out)
|
||||
code, out = run("kit.env", GOOD["kit.env"], 'echo "[$(env_get "$1" SUB_PATH)|$(env_get "$1" MTPROTO_INNER)]"\n')
|
||||
self.assertIn("[/AbC123/|]", out)
|
||||
|
||||
def test_env_get_does_not_touch_shell(self):
|
||||
code, out = run("kit-backup.env", GOOD["kit-backup.env"], 'env_get "$1" BACKUP_HOST >/dev/null; echo "[${BACKUP_HOST:-none}]"\n')
|
||||
self.assertIn("[none]", out)
|
||||
|
||||
def rejected(self, name, text):
|
||||
code, out = run(name, text, 'echo "RESULT=$RESULT"\n')
|
||||
self.assertNotEqual(code, 0, out)
|
||||
self.assertIn("Это не резервная копия 3X-UI KIT", out)
|
||||
self.assertNotIn("OK", out)
|
||||
|
||||
def test_result_in_backup_env(self):
|
||||
self.rejected("kit-backup.env", GOOD["kit-backup.env"] + "RESULT=/etc/cron.d/x\n")
|
||||
|
||||
def test_kit_version_in_kit_env(self):
|
||||
self.rejected("kit.env", GOOD["kit.env"] + "KIT_VERSION=9.9\n")
|
||||
|
||||
def test_foreign_names(self):
|
||||
for bad in ("PATH=/tmp", "IFS=x", "XUI_ENV=/etc/cron.d/x", "HOST=5.6.7.8", "KIT_RAW=https://example.org/"):
|
||||
self.rejected("kit-backup.env", GOOD["kit-backup.env"] + bad + "\n")
|
||||
for bad in ("PATH=/tmp", "RESULT=/etc/cron.d/x", "XUI_ENV=/tmp/x", "KIT_VERSION=9.9"):
|
||||
self.rejected("install-result.env", GOOD["install-result.env"] + bad + "\n")
|
||||
for bad in ("RESULT=/etc/cron.d/x", "XUI_PIN=v0", "BACKUP_HOST=1.2.3.4"):
|
||||
self.rejected("kit.env", GOOD["kit.env"] + bad + "\n")
|
||||
|
||||
def test_substitutions(self):
|
||||
for bad in ("BACKUP_DATE=$(id)", "BACKUP_DATE=`id`", 'BACKUP_DATE="$HOME"', "export BACKUP_DATE=1", "BACKUP_DATE=1; id", " BACKUP_DATE=1"):
|
||||
self.rejected("kit-backup.env", GOOD["kit-backup.env"] + bad + "\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user