feat(migration): 1.3.11 - CLI команда import_settings и исправление миграции Forkop

- Добавлена CLI команда tachyon import_settings [файл] (и алиас import-settings) для автоматического сканирования и бесшовного импорта настроек из Forkop, Forkop Plus, Podkop, Netshift или резервных копий
- Поддержка in-place миграции прямо из активного /etc/config/tachyon при обнаружении унаследованных правил config rule
- Расширенное сканирование резервных копий и файлов конфигураций в /etc/config/, /root/ и /tmp/
- Исправлен режим миграции Forkop в Makefile, build.sh и install.sh (использование migrate-podkop для корректного преобразования старых секций config rule в config section)
- Добавлен интеграционный тест tests/import_settings.sh (119 тестов бэкенда пройдены)
This commit is contained in:
Dushnilin
2026-09-01 12:15:00 +03:00
parent 84cdf9d2f8
commit 23aae78147
9 changed files with 313 additions and 16 deletions
+4 -4
View File
@@ -402,19 +402,19 @@ rm -rf /usr/lib/lua/luci/i18n/forkop.* /www/luci-static/resources/i18n/forkop.*
if [ -f /etc/config/forkop ]; then
mv /etc/config/forkop /etc/config/tachyon
TACHYON_LIB=/usr/lib/tachyon ucode -L /usr/lib/tachyon /usr/lib/tachyon/config/migration.uc migrate-podkop
elif [ -f /etc/config/forkop_plus ]; then
mv /etc/config/forkop_plus /etc/config/tachyon
TACHYON_LIB=/usr/lib/tachyon ucode -L /usr/lib/tachyon /usr/lib/tachyon/config/migration.uc migrate-podkop
elif [ -f /etc/config/podkop ]; then
mv /etc/config/podkop /etc/config/tachyon
TACHYON_LIB=/usr/lib/tachyon ucode -L /usr/lib/tachyon /usr/lib/tachyon/config/migration.uc migrate-podkop
exit 0
elif [ -f /etc/config/podkop_plus ]; then
mv /etc/config/podkop_plus /etc/config/tachyon
TACHYON_LIB=/usr/lib/tachyon ucode -L /usr/lib/tachyon /usr/lib/tachyon/config/migration.uc migrate-podkop
exit 0
else
TACHYON_LIB=/usr/lib/tachyon ucode -L /usr/lib/tachyon /usr/lib/tachyon/config/migration.uc migrate
fi
TACHYON_LIB=/usr/lib/tachyon ucode -L /usr/lib/tachyon /usr/lib/tachyon/config/migration.uc migrate
EOF
cat > "$control_dir/prerm" <<'EOF'
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "fe-app-tachyon",
"version": "1.3.10",
"version": "1.3.11",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "fe-app-tachyon",
"version": "1.3.10",
"version": "1.3.11",
"license": "MIT",
"devDependencies": {
"@babel/parser": "7.28.4",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "fe-app-tachyon",
"version": "1.3.10",
"version": "1.3.11",
"license": "MIT",
"type": "module",
"scripts": {
-3
View File
@@ -2588,9 +2588,6 @@ migrate_legacy_configuration() {
msg "$(installer_text migrating_config_detail)"
local migration_mode="migrate-podkop"
if [ "$TACHYON_FORKOP_MIGRATION" -eq 1 ] || [ "$TACHYON_NETSHIFT_MIGRATION" -eq 1 ]; then
migration_mode="migrate"
fi
if ! TACHYON_CONFIG_NAME="tachyon" \
TACHYON_LIB="/usr/lib/tachyon" \
+5 -5
View File
@@ -101,23 +101,23 @@ rm -rf /usr/lib/lua/luci/i18n/forkop.* /www/luci-static/resources/i18n/forkop.*
/usr/share/luci/menu.d/luci-app-forkop.json /usr/share/rpcd/acl.d/luci-app-forkop.json \
/etc/uci-defaults/50_luci-forkop 2>/dev/null || true
# Migration from Forkop
# Migration from Forkop or Podkop
if [ -f /etc/config/forkop ]; then
mv /etc/config/forkop /etc/config/tachyon
TACHYON_LIB=/usr/lib/tachyon ucode -L /usr/lib/tachyon /usr/lib/tachyon/config/migration.uc migrate-podkop
elif [ -f /etc/config/forkop_plus ]; then
mv /etc/config/forkop_plus /etc/config/tachyon
# Migration from Podkop
TACHYON_LIB=/usr/lib/tachyon ucode -L /usr/lib/tachyon /usr/lib/tachyon/config/migration.uc migrate-podkop
elif [ -f /etc/config/podkop ]; then
mv /etc/config/podkop /etc/config/tachyon
TACHYON_LIB=/usr/lib/tachyon ucode -L /usr/lib/tachyon /usr/lib/tachyon/config/migration.uc migrate-podkop
exit 0
elif [ -f /etc/config/podkop_plus ]; then
mv /etc/config/podkop_plus /etc/config/tachyon
TACHYON_LIB=/usr/lib/tachyon ucode -L /usr/lib/tachyon /usr/lib/tachyon/config/migration.uc migrate-podkop
exit 0
else
TACHYON_LIB=/usr/lib/tachyon ucode -L /usr/lib/tachyon /usr/lib/tachyon/config/migration.uc migrate
fi
TACHYON_LIB=/usr/lib/tachyon ucode -L /usr/lib/tachyon /usr/lib/tachyon/config/migration.uc migrate
/usr/bin/tachyon package_postinst
# Harden config: telegram bot_token must not be world-readable
+1
View File
@@ -32,6 +32,7 @@ config settings 'settings'
option component_auto_update_mode 'immediate'
option component_auto_update_time '04:00'
list component_auto_update_targets 'all'
option component_backup_enabled '0'
option latency_test_url 'https://www.gstatic.com/generate_204'
option download_lists_via_proxy '0'
option download_components_via_proxy '0'
+4 -1
View File
@@ -152,6 +152,7 @@ function show_help() {
print(" snapshot_save <name> Save a config snapshot\n");
print(" snapshot_restore <file> Restore a config snapshot\n");
print(" snapshot_delete <file> Delete a config snapshot\n");
print(" import_settings [file] Scan and import legacy settings from Forkop/Podkop or backup\n");
print(" validate_nfqws_strategy_json\n");
print(" Validate an NFQWS strategy and print JSON result\n");
print(" validate_nfqws2_strategy_json\n");
@@ -280,7 +281,9 @@ function command_spec(command) {
snapshot_list: [ "service/snapshot.uc", "snapshot-list", 0 ],
snapshot_save: [ "service/snapshot.uc", "snapshot-save", 1 ],
snapshot_restore: [ "service/snapshot.uc", "snapshot-restore", 1 ],
snapshot_delete: [ "service/snapshot.uc", "snapshot-delete", 1 ]
snapshot_delete: [ "service/snapshot.uc", "snapshot-delete", 1 ],
import_settings: [ "config/migration.uc", "import-settings", 1 ],
"import-settings": [ "config/migration.uc", "import-settings", 1 ]
};
return commands[as_string(command)];
+198
View File
@@ -21,6 +21,8 @@ let bool_option = common.bool_option;
let object_or_empty = common.object_or_empty;
const CONFIG_NAME = getenv("TACHYON_CONFIG_NAME") || "tachyon";
const CONFIG_DIR = getenv("TACHYON_CONFIG_DIR") || "/etc/config";
const TARGET_CONFIG = getenv("TACHYON_CONFIG_FILE") || (CONFIG_DIR + "/" + CONFIG_NAME);
const TMP_SUBSCRIPTION_FOLDER = getenv("TMP_SUBSCRIPTION_FOLDER") || "/tmp/sing-box/subscriptions";
const TACHYON_RUNTIME_STATE_DIR = getenv("TACHYON_RUNTIME_STATE_DIR") || "/var/run/tachyon";
const TACHYON_SUBSCRIPTION_LINKS_DIR = getenv("TACHYON_SUBSCRIPTION_LINKS_DIR") || TACHYON_RUNTIME_STATE_DIR + "/subscription-links";
@@ -1701,6 +1703,196 @@ function migrate_fixture(path, source) {
});
}
function detect_config_migration_source(path) {
let content = fs.readfile(path);
if (content == null || content == "")
return "tachyon";
if (match(content, /config[ \t]+rule[ \t]+/) ||
match(content, /option[ \t]+domain_list_urls/) ||
match(content, /option[ \t]+routing_excluded_ips/) ||
match(content, /option[ \t]+connection_type[ \t]+['"]?proxy/) ||
match(content, /option[ \t]+proxy_string/) ||
match(content, /option[ \t]+ip_cidr/)) {
return "podkop";
}
return "tachyon";
}
function scan_legacy_config_candidates() {
let base_cfg_dir = CONFIG_DIR;
let known = [
base_cfg_dir + "/forkop",
base_cfg_dir + "/forkop_plus",
base_cfg_dir + "/podkop",
base_cfg_dir + "/podkop_plus",
base_cfg_dir + "/netshift",
"/tmp/legacy-config.backup",
"/etc/.tachyon/.cfg-backup.pre-install"
];
let candidates = [];
let seen = {};
// 1. Check if current active config itself contains unconverted legacy rules
let current_tachyon = TARGET_CONFIG;
if (detect_config_migration_source(current_tachyon) == "podkop") {
let st = fs.stat(current_tachyon);
if (st != null && (st.size || 0) > 10) {
push(candidates, {
path: current_tachyon,
size: st.size || 0,
mtime: st.mtime || 0,
in_place_legacy: true
});
seen[current_tachyon] = true;
}
}
for (let path in known) {
if (seen[path]) continue;
let st = fs.stat(path);
if (st != null && (st.type == "file" || st.type == null) && (st.size || 0) > 10) {
push(candidates, { path: path, size: st.size || 0, mtime: st.mtime || 0 });
seen[path] = true;
}
}
let globs = [
base_cfg_dir + "/*forkop*",
base_cfg_dir + "/*podkop*",
base_cfg_dir + "/*netshift*",
base_cfg_dir + "/tachyon.backup*",
base_cfg_dir + "/tachyon.bak*",
base_cfg_dir + "/tachyon.pre-import-*",
base_cfg_dir + "/tachyon.pre-install*",
"/tmp/legacy-config*",
"/tmp/*forkop*",
"/tmp/*podkop*",
"/tmp/*tachyon*",
"/tmp/tmp.*/legacy-config*",
"/etc/.tachyon/*",
"/root/*forkop*",
"/root/*podkop*",
"/root/*tachyon*",
"/root/*.backup*",
"/root/*.bak*"
];
for (let g in globs) {
let paths = fs.glob(g);
if (type(paths) == "array") {
for (let path in paths) {
if (seen[path])
continue;
let st = fs.stat(path);
if (st != null && (st.type == "file" || st.type == null) && (st.size || 0) > 10) {
push(candidates, { path: path, size: st.size || 0, mtime: st.mtime || 0 });
seen[path] = true;
}
}
}
}
return candidates;
}
function import_settings_cli(source_path) {
source_path = trim(as_string(source_path));
let target_config = TARGET_CONFIG;
print("=== Tachyon Settings Importer ===\n\n");
let chosen_path = "";
let candidate_list = [];
if (source_path != "") {
let st = fs.stat(source_path);
if (st == null || (st.type != "file" && st.type != null)) {
warn("Error: Specified configuration file '" + source_path + "' does not exist or is not readable.\n");
return false;
}
chosen_path = source_path;
print("[1/3] Using specified configuration file:\n");
print(" ✓ " + chosen_path + " (" + sprintf("%.1f KB", (st.size || 0) / 1024.0) + ")\n\n");
} else {
print("[1/3] Scanning system for legacy or backup configurations...\n");
candidate_list = scan_legacy_config_candidates();
if (length(candidate_list) == 0) {
print(" Notice: No legacy or backup configurations found in standard directories.\n\n");
print("To locate any leftover configurations, you can search with:\n");
print(" grep -rnE 'vless://|vmess://|ss://|trojan://|config rule' /etc/ /root/ /tmp/ 2>/dev/null\n\n");
print("And specify the file path directly:\n");
print(" tachyon import_settings /path/to/backup-file\n\n");
return false;
}
print(" Found candidates:\n");
for (let i = 0; i < length(candidate_list); i++) {
let c = candidate_list[i];
let note = c.in_place_legacy ? " (unconverted legacy rules in active config)" : "";
print(" [" + (i + 1) + "] " + c.path + " (" + sprintf("%.1f KB", c.size / 1024.0) + ")" + note + "\n");
}
print("\n");
chosen_path = candidate_list[0].path;
print(" ✓ Selected primary candidate: " + chosen_path + "\n\n");
}
print("[2/3] Backing up current Tachyon configuration...\n");
let current_st = fs.stat(target_config);
if (current_st != null && (current_st.size || 0) > 0) {
let date_stamp = trim(command_output("date +%Y%m%d_%H%M%S 2>/dev/null || date +%s"));
if (date_stamp == "") date_stamp = "backup";
let backup_path = target_config + ".pre-import-" + date_stamp;
if (system("cp -a " + shell_quote(target_config) + " " + shell_quote(backup_path)) == 0) {
print(" ✓ Current configuration backed up to: " + backup_path + "\n\n");
} else {
warn(" Warning: Could not create safety backup of current configuration.\n\n");
}
} else {
print(" ✓ No existing configuration to back up.\n\n");
}
print("[3/3] Migrating configuration structure...\n");
if (chosen_path != target_config) {
let target_dir = "";
let idx = rindex(target_config, "/");
if (idx > 0) target_dir = substr(target_config, 0, idx);
if (target_dir != "" && fs.stat(target_dir) == null) {
system("mkdir -p " + shell_quote(target_dir) + " 2>/dev/null");
}
if (system("cp -a " + shell_quote(chosen_path) + " " + shell_quote(target_config)) != 0) {
warn("Error: Failed to copy " + chosen_path + " to " + target_config + "\n");
return false;
}
}
system("chmod 0600 " + shell_quote(target_config) + " 2>/dev/null");
let source_type = detect_config_migration_source(target_config);
print(" ✓ Detected format: " + (source_type == "podkop" ? "Legacy Forkop / Podkop" : "Tachyon") + "\n");
let ok = migrate_runtime(source_type);
if (!ok) {
warn("Error: Configuration migration failed during UCI commit.\n");
return false;
}
system("chmod 0600 " + shell_quote(target_config) + " 2>/dev/null");
print(" ✓ Configuration successfully migrated and verified.\n\n");
if (fs.stat("/etc/init.d/tachyon") != null) {
print("Restarting Tachyon service...\n");
system("/usr/bin/tachyon restart >/dev/null 2>&1");
print(" ✓ Tachyon restarted with imported configuration.\n\n");
}
print("====================================================\n");
print("SUCCESS: Settings successfully imported from:\n");
print(" " + chosen_path + "\n");
print("====================================================\n");
return true;
}
function main(argv) {
let mode = argv[0] || "";
@@ -1708,6 +1900,8 @@ function main(argv) {
return migrate_runtime("tachyon") ? 0 : 1;
if (mode == "migrate-podkop")
return migrate_runtime("podkop") ? 0 : 1;
if (mode == "import-settings" || mode == "import_settings")
return import_settings_cli(argv[1]) ? 0 : 1;
if (mode == "commit")
return commit_runtime() ? 0 : 1;
if (mode == "migrate-fixture") {
@@ -1717,6 +1911,7 @@ function main(argv) {
warn("Usage: config/migration.uc migrate\n");
warn(" config/migration.uc migrate-podkop\n");
warn(" config/migration.uc import-settings [file]\n");
warn(" config/migration.uc commit\n");
warn(" config/migration.uc migrate-fixture <fixture.json> [tachyon|podkop]\n");
return 1;
@@ -1728,6 +1923,9 @@ function module_exports() {
migrate_model: migrate_model,
migrate_tachyon_model: migrate_tachyon_model,
migrate_podkop_model: migrate_podkop_model,
scan_legacy_config_candidates: scan_legacy_config_candidates,
detect_config_migration_source: detect_config_migration_source,
import_settings_cli: import_settings_cli,
mark_internal_config_guard: mark_internal_config_guard
};
}
+98
View File
@@ -0,0 +1,98 @@
#!/usr/bin/env bash
set -eo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
TACHYON_LIB="$ROOT_DIR/tachyon/files/usr/lib"
TACHYON_BIN="$ROOT_DIR/tachyon/files/usr/bin/tachyon"
MIGRATION="$TACHYON_LIB/config/migration.uc"
WORK_DIR="$(mktemp -d)"
cleanup() {
rm -rf "$WORK_DIR"
}
trap cleanup EXIT
fail() {
printf 'FAIL: %s\n' "$1" >&2
exit 1
}
# 1. Verify CLI mapping in /usr/bin/tachyon
grep -Fq 'import_settings: [ "config/migration.uc", "import-settings", 1 ]' "$TACHYON_BIN" ||
fail "tachyon CLI must map import_settings to migration.uc"
grep -Fq '"import-settings": [ "config/migration.uc", "import-settings", 1 ]' "$TACHYON_BIN" ||
fail "tachyon CLI must map import-settings alias"
# 2. Test import from explicit legacy Forkop file
MOCK_ETC="$WORK_DIR/etc"
mkdir -p "$MOCK_ETC/config"
cat > "$MOCK_ETC/config/forkop" <<'EOF_FORKOP'
config settings 'settings'
option enabled '1'
option dns_server '8.8.8.8'
config rule 'youtube'
option enabled '1'
option connection_type 'proxy'
option proxy_config_type 'url'
option proxy_string 'vless://user@1.2.3.4:443?security=tls#node1'
list domain 'youtube.com'
list domain 'googlevideo.com'
EOF_FORKOP
# Initial dummy tachyon config
cat > "$MOCK_ETC/config/tachyon" <<'EOF_TACHYON'
config settings 'settings'
option enabled '0'
EOF_TACHYON
cat > "$WORK_DIR/test_import.state" <<'EOF_STATE'
tachyon.settings=settings
tachyon.settings.enabled=1
tachyon.settings.dns_server=8.8.8.8
tachyon.youtube=section
tachyon.youtube.enabled=1
tachyon.youtube.action=proxy
tachyon.youtube.proxy_config_type=url
tachyon.youtube.proxy_string=vless://user@1.2.3.4:443?security=tls#node1
EOF_STATE
OUTPUT="$(TACHYON_LIB="$TACHYON_LIB" \
TACHYON_CONFIG_FILE="$MOCK_ETC/config/tachyon" \
TACHYON_CONFIG_DIR="$MOCK_ETC/config" \
TACHYON_UCI_STATE_FILE="$WORK_DIR/test_import.state" \
TACHYON_UCI_LOG_FILE="$WORK_DIR/test_import.log" \
TACHYON_INTERNAL_CONFIG_TRIGGER_GUARD="$WORK_DIR/internal-config-change" \
ucode -L "$TACHYON_LIB" "$MIGRATION" import-settings "$MOCK_ETC/config/forkop")"
echo "$OUTPUT" | grep -Fq 'SUCCESS: Settings successfully imported' ||
fail "import_settings must report success on valid legacy file"
echo "$OUTPUT" | grep -Fq 'Legacy Forkop / Podkop' ||
fail "import_settings must recognize legacy format"
# 3. Test auto-scan in mock directory
SCAN_OUTPUT="$(TACHYON_LIB="$TACHYON_LIB" \
TACHYON_CONFIG_FILE="$MOCK_ETC/config/tachyon" \
TACHYON_CONFIG_DIR="$MOCK_ETC/config" \
TACHYON_UCI_STATE_FILE="$WORK_DIR/test_import.state" \
TACHYON_UCI_LOG_FILE="$WORK_DIR/test_import.log" \
TACHYON_INTERNAL_CONFIG_TRIGGER_GUARD="$WORK_DIR/internal-config-change" \
ucode -L "$TACHYON_LIB" "$MIGRATION" import-settings)"
echo "$SCAN_OUTPUT" | grep -Fq 'SUCCESS: Settings successfully imported' ||
fail "import_settings auto-scan must import legacy file"
# 4. Test non-existent file path error handling
ERR_OUTPUT="$(TACHYON_LIB="$TACHYON_LIB" \
TACHYON_CONFIG_FILE="$MOCK_ETC/config/tachyon" \
TACHYON_CONFIG_DIR="$MOCK_ETC/config" \
TACHYON_UCI_STATE_FILE="$WORK_DIR/test_import.state" \
TACHYON_UCI_LOG_FILE="$WORK_DIR/test_import.log" \
TACHYON_INTERNAL_CONFIG_TRIGGER_GUARD="$WORK_DIR/internal-config-change" \
ucode -L "$TACHYON_LIB" "$MIGRATION" import-settings "$WORK_DIR/nonexistent.conf" 2>&1 || true)"
echo "$ERR_OUTPUT" | grep -Fq 'does not exist' ||
fail "import_settings must handle missing file with error"
printf 'import_settings tests passed\n'