fix: prevent package removal during web update and fix zapret2 installation

- Fix package postrm scripts to strictly require \ to be 'remove' or 'purge' before running cleanup, preventing destructive cleanup when opkg runs postrm with empty arguments during reinstalls/upgrades.
- In action.uc install_files, ensure tachyon backend package is installed before luci-app-tachyon to prevent opkg dependency removal races.
- In action.uc install_zapret2, ensure ::1 localhost exists in /etc/hosts before package installation to prevent nftables IPv6 resolution errors, and cleanly stop/disable standalone zapret2 daemon.
- In zapret2 provider common.uc and nfqueue runtime.uc, automatically resolve required blob binary payloads and default filter parameters for strategies.
- In fe-app-tachyon, add 'access denied' / 'Доступ запрещён' to transient RPC error patterns and track install_version as self-update so rpcd reload during self-update does not abort the progress modal with an error.
- In tests/package_lifecycle.sh, avoid grep -q in pipefail pipelines to prevent upstream SIGPIPE failures.
This commit is contained in:
Dushnilin
2026-09-09 07:37:55 +03:00
parent f92c931a72
commit 8a56361bfd
13 changed files with 128 additions and 16 deletions
+13 -2
View File
@@ -461,9 +461,13 @@ EOF
[ -n "${IPKG_INSTROOT}" ] && exit 0
[ "$1" = "upgrade" ] && exit 0
[ "$1" != "remove" ] && [ "$1" != "purge" ] && [ -n "$1" ] && exit 0
if [ -f /var/run/tachyon/component-actions/action.lock ] || [ -f /tmp/run/tachyon/component-actions/action.lock ]; then
exit 0
fi
rm -f /www/cgi-bin/tachyon-agent /etc/hotplug.d/iface/99-tachyon-wan-monitor 2>/dev/null || true
rm -f /usr/share/nftables.d/chain-pre/input/10-tachyon.nft 2>/dev/null || true
rm -rf /var/run/tachyon* /tmp/tachyon* /tmp/sing-box /tmp/ai_doctor* /tmp/tg_* /tmp/warp_* 2>/dev/null || true
rm -rf /tmp/ai_doctor* /tmp/tg_* /tmp/warp_* 2>/dev/null || true
rm -rf /var/run/tachyon/section-cache /var/run/tachyon/ui-state 2>/dev/null || true
[ -x /etc/init.d/firewall ] && /etc/init.d/firewall restart >/dev/null 2>&1 || true
[ -x /etc/init.d/dnsmasq ] && /etc/init.d/dnsmasq restart >/dev/null 2>&1 || true
exit 0
@@ -519,6 +523,9 @@ EOF
[ -n "${IPKG_INSTROOT}" ] && exit 0
[ "$1" = "upgrade" ] && exit 0
[ "$1" != "remove" ] && [ "$1" != "purge" ] && [ -n "$1" ] && exit 0
if [ -f /var/run/tachyon/component-actions/action.lock ] || [ -f /tmp/run/tachyon/component-actions/action.lock ]; then
exit 0
fi
rm -f /var/luci-indexcache* /tmp/luci-indexcache* /tmp/luci-modulecache/* 2>/dev/null || true
[ -x /etc/init.d/rpcd ] && /etc/init.d/rpcd reload >/dev/null 2>&1 || true
exit 0
@@ -797,9 +804,13 @@ EOF
#!/bin/sh
[ -n "${IPKG_INSTROOT}" ] && exit 0
[ -f /usr/bin/tachyon ] && exit 0
if [ -f /var/run/tachyon/component-actions/action.lock ] || [ -f /tmp/run/tachyon/component-actions/action.lock ]; then
exit 0
fi
rm -f /www/cgi-bin/tachyon-agent /etc/hotplug.d/iface/99-tachyon-wan-monitor 2>/dev/null || true
rm -f /usr/share/nftables.d/chain-pre/input/10-tachyon.nft 2>/dev/null || true
rm -rf /var/run/tachyon* /tmp/tachyon* /tmp/sing-box /tmp/ai_doctor* /tmp/tg_* /tmp/warp_* 2>/dev/null || true
rm -rf /tmp/ai_doctor* /tmp/tg_* /tmp/warp_* 2>/dev/null || true
rm -rf /var/run/tachyon/section-cache /var/run/tachyon/ui-state 2>/dev/null || true
[ -x /etc/init.d/firewall ] && /etc/init.d/firewall restart >/dev/null 2>&1 || true
[ -x /etc/init.d/dnsmasq ] && /etc/init.d/dnsmasq restart >/dev/null 2>&1 || true
exit 0
@@ -13,6 +13,11 @@ const TRANSIENT_RPC_ERROR_PATTERNS = [
'service unavailable',
'ubus error',
'ipc error',
'access denied',
'доступ запрещён',
'доступ запрещен',
'permission denied',
'unauthorized',
];
export function isTransientRpcError(message?: string | null) {
@@ -774,7 +774,9 @@ export const TachyonShellMethods = {
const jobStartedAt = Date.now();
const isSelfUpdate =
component === 'tachyon' &&
(action === 'install' || action === 'reinstall');
(action === 'install' ||
action === 'reinstall' ||
action === 'install_version');
const targetVersion = expectedLatestVersion || '';
// Version before the update started: confirms an install even when the
// expected target version is unknown.
+4
View File
@@ -40,6 +40,10 @@ define Package/$(PKG_NAME)/postrm
[ "$$1" = "upgrade" ] && exit 0
[ "$$1" != "remove" ] && [ "$$1" != "purge" ] && [ -n "$$1" ] && exit 0
if [ -f /var/run/tachyon/component-actions/action.lock ] || [ -f /tmp/run/tachyon/component-actions/action.lock ]; then
exit 0
fi
rm -f /var/luci-indexcache* /tmp/luci-indexcache* /tmp/luci-modulecache/* 2>/dev/null || true
[ -x /etc/init.d/rpcd ] && /etc/init.d/rpcd reload >/dev/null 2>&1 || true
@@ -3462,7 +3462,12 @@ var TRANSIENT_RPC_ERROR_PATTERNS = [
"bad gateway",
"service unavailable",
"ubus error",
"ipc error"
"ipc error",
"access denied",
"\u0434\u043E\u0441\u0442\u0443\u043F \u0437\u0430\u043F\u0440\u0435\u0449\u0451\u043D",
"\u0434\u043E\u0441\u0442\u0443\u043F \u0437\u0430\u043F\u0440\u0435\u0449\u0435\u043D",
"permission denied",
"unauthorized"
];
function isTransientRpcError(message) {
if (!message) {
@@ -4053,7 +4058,7 @@ var TachyonShellMethods = {
},
waitComponentActionJob: async (jobId, component, action, expectedLatestVersion) => {
const jobStartedAt = Date.now();
const isSelfUpdate = component === "tachyon" && (action === "install" || action === "reinstall");
const isSelfUpdate = component === "tachyon" && (action === "install" || action === "reinstall" || action === "install_version");
const targetVersion = expectedLatestVersion || "";
let baselineVersion = "";
if (isSelfUpdate) {
+6 -1
View File
@@ -159,9 +159,14 @@ define Package/tachyon/postrm
[ "$$1" = "upgrade" ] && exit 0
[ "$$1" != "remove" ] && [ "$$1" != "purge" ] && [ -n "$$1" ] && exit 0
if [ -f /var/run/tachyon/component-actions/action.lock ] || [ -f /tmp/run/tachyon/component-actions/action.lock ]; then
exit 0
fi
rm -f /www/cgi-bin/tachyon-agent /etc/hotplug.d/iface/99-tachyon-wan-monitor 2>/dev/null || true
rm -f /usr/share/nftables.d/chain-pre/input/10-tachyon.nft 2>/dev/null || true
rm -rf /var/run/tachyon* /tmp/tachyon* /tmp/sing-box /tmp/ai_doctor* /tmp/tg_* /tmp/warp_* 2>/dev/null || true
rm -rf /tmp/ai_doctor* /tmp/tg_* /tmp/warp_* 2>/dev/null || true
rm -rf /var/run/tachyon/section-cache /var/run/tachyon/ui-state 2>/dev/null || true
[ -x /etc/init.d/firewall ] && /etc/init.d/firewall restart >/dev/null 2>&1 || true
[ -x /etc/init.d/dnsmasq ] && /etc/init.d/dnsmasq restart >/dev/null 2>&1 || true
+10 -3
View File
@@ -1408,6 +1408,10 @@ function install_zapret2(action, target_tag) {
if (pkg == null)
action_fail(component, action, "Failed to download " + label + " package", current_version, release.version, "", release.release_url || "");
let hosts_content = read_file("/etc/hosts") || "";
if (index(hosts_content, "::1") < 0)
command_success("printf '\\n::1 localhost ip6-localhost ip6-loopback\\n' >> /etc/hosts");
if (!run_logged("Installing " + label + " package " + pkg.name, pkg_install_files_command([ pkg.file ])))
action_fail(component, action, "Failed to install " + label + " package", current_version, pkg.version, "", release.release_url || "");
@@ -1418,8 +1422,11 @@ function install_zapret2(action, target_tag) {
if (file_exists("/opt/zapret2"))
command_status_from_args([ "chmod", "-R", "a+rX", "/opt/zapret2" ]);
command_success_from_args([ "killall", "-9", "nfqws2" ]);
disable_standalone_service(component);
command_status_from_args([ "nft", "delete", "table", "inet", "zapret2" ]);
command_status_from_args([ "nft", "delete", "table", "ip", "zapret2" ]);
command_status_from_args([ "nft", "delete", "table", "ip6", "zapret2" ]);
restart_tachyon_after_successful_change();
clear_version_caches();
current_version = provider_package_version(runtime_module);
@@ -2662,7 +2669,7 @@ function reinstall_tachyon() {
(release.i18n_url != "" && !download_with_retry(release.i18n_url, i18n_file, release.i18n_name)))
action_fail("tachyon", "reinstall", "Failed to download Tachyon release packages", TACHYON_VERSION, latest_version);
let reinstall_files = [ app_file, backend_file ];
let reinstall_files = [ backend_file, app_file ];
if (i18n_file != "")
push(reinstall_files, i18n_file);
if (!run_logged_retrying("Reinstalling Tachyon packages", pkg_install_files_command(reinstall_files, true)))
@@ -2706,7 +2713,7 @@ function install_tachyon() {
(release.i18n_url != "" && !download_with_retry(release.i18n_url, i18n_file, release.i18n_name)))
action_fail("tachyon", "install", "Failed to download Tachyon release packages", TACHYON_VERSION, latest_version);
let install_files = [ app_file, backend_file ];
let install_files = [ backend_file, app_file ];
if (i18n_file != "")
push(install_files, i18n_file);
// Installing the same tag means a rebuild of the current release; opkg skips
@@ -2750,7 +2757,7 @@ function install_tachyon_version(target_tag) {
(release.i18n_url != "" && !download_with_retry(release.i18n_url, i18n_file, release.i18n_name)))
action_fail("tachyon", "install_version", "Failed to download Tachyon release packages", TACHYON_VERSION, target_tag);
let install_files = [ app_file, backend_file ];
let install_files = [ backend_file, app_file ];
if (i18n_file != "")
push(install_files, i18n_file);
@@ -343,6 +343,10 @@ function supervisor_command(cfg, queue, raw_opt, child_pidfile) {
let args = [ cfg.binary, "--qnum=" + as_string(queue) ];
for (let arg in base_args(cfg))
push(args, arg);
if (type(cfg.prepare_strategy_args) == "function") {
for (let extra in cfg.prepare_strategy_args(raw_opt))
push(args, extra);
}
for (let word in strategy_words(raw_opt))
push(args, word);
@@ -12,6 +12,68 @@ function validator() {
return validator_module;
}
const KNOWN_BLOB_FILES = {
tls_max: { file: "tls_clienthello_max_ru.bin", size: 654 },
tls_google: { file: "tls_clienthello_www_google_com.bin", size: 681 },
tls_gosuslugi: { file: "tls_clienthello_gosuslugi_ru.bin", size: 517 },
tls_sber: { file: "tls_clienthello_sberbank_ru.bin", size: 517 },
tls_iana: { file: "tls_clienthello_iana_org.bin", size: 517 },
tls_vk: { file: "tls_clienthello_vk_com.bin", size: 517 },
tls_onetrust: { file: "tls_clienthello_www_onetrust_com.bin", size: 664 },
quic_google: { file: "quic_initial_www_google_com.bin", size: 1200 },
quic_yt1: { file: "quic_initial_rr1---sn-xguxaxjvh-n8me_googlevideo_com_kyber_1.bin", size: 1230 },
quic_vk: { file: "quic_initial_vk_com.bin", size: 1357 },
stun_fake: { file: "stun.bin", size: 100 },
discord_udp: { file: "stun.bin", size: 100 }
};
function get_blob_dir() {
let candidate_dirs = [
getenv("ZAPRET2_PROVIDER_FILES_DIR") ? (getenv("ZAPRET2_PROVIDER_FILES_DIR") + "/fake") : null,
"/opt/zapret2/files/fake",
"/usr/share/zapret2/files/fake",
"/etc/zapret2/files/fake",
"/opt/zapret/files/fake",
"/usr/share/zapret/files/fake"
];
for (let d in candidate_dirs) {
if (d && fs.stat(d) != null) return d;
}
return "/opt/zapret2/files/fake";
}
function resolve_blobs(args_str) {
if (!args_str || args_str == "") return [];
let bdir = get_blob_dir();
let result = [];
for (let name, info in KNOWN_BLOB_FILES) {
if ((index(args_str, "blob=" + name) >= 0 || index(args_str, "seqovl_pattern=" + name) >= 0) &&
index(args_str, "--blob=" + name + ":") < 0) {
let blob_path = bdir + "/" + info.file;
if (fs.stat(blob_path) != null || fs.stat("/opt/zapret2/files/fake/" + info.file) != null) {
let actual_path = fs.stat(blob_path) != null ? blob_path : ("/opt/zapret2/files/fake/" + info.file);
push(result, sprintf("--blob=%s:@%s", name, actual_path));
}
}
}
return result;
}
function prepare_strategy_args(raw_opt) {
let raw_str = as_string(raw_opt);
let extra_args = resolve_blobs(raw_str);
let filter_prefix = [];
if (index(raw_str, "--filter-tcp") < 0 && index(raw_str, "--filter-l7") < 0) {
push(filter_prefix, "--filter-tcp=443");
push(filter_prefix, "--filter-l7=tls");
push(filter_prefix, "--payload=tls_client_hello");
}
let words = [];
for (let arg in extra_args) push(words, arg);
for (let f in filter_prefix) push(words, f);
return words;
}
function config(ctx) {
let runtime_constants = (ctx && ctx.constants) || constants;
let lib_dir = (ctx && ctx.lib_dir) || LIB_DIR;
@@ -111,7 +173,8 @@ function config(ctx) {
hostlist_dir: "",
status_label: "zapret2",
check_prefix: "zapret2",
base_args
base_args,
prepare_strategy_args
};
}
-2
View File
@@ -226,8 +226,6 @@ function prerm_cleanup(action) {
// Kill any remaining tachyon processes, minus this hook's own ancestors.
system(kill_matching_command("-F '" + BIN_PATH + "'"));
restore_dnsmasq_if_needed();
if (action == "remove")
remove_managed_sing_box();
}
remove_rt_tables_entry(); // best-effort
return true;
+5 -1
View File
@@ -47,7 +47,11 @@ const RT_TABLE_NAME = getenv("RT_TABLE_NAME") || "tachyon";
const NFT_FAKEIP_MARK = getenv("NFT_FAKEIP_MARK") || "0x04000000";
const SB_DNS_INBOUND_ADDRESS = getenv("SB_DNS_INBOUND_ADDRESS") || "127.0.0.42";
const ZAPRET_PROVIDER_NFQWS_BIN = getenv("ZAPRET_PROVIDER_NFQWS_BIN") || "/opt/zapret/nfq/nfqws";
const ZAPRET2_PROVIDER_NFQWS2_BIN = getenv("ZAPRET2_PROVIDER_NFQWS2_BIN") || "/opt/zapret2/nfq2/nfqws2";
let default_zapret2_bin = "/opt/zapret2/nfq2/nfqws2";
for (let b in [ "/opt/zapret2/nfq2/nfqws2", "/opt/zapret2/nfq/nfqws2", "/opt/zapret2/nfqws2", "/usr/bin/nfqws2" ]) {
if (fs.stat(b) != null) { default_zapret2_bin = b; break; }
}
const ZAPRET2_PROVIDER_NFQWS2_BIN = getenv("ZAPRET2_PROVIDER_NFQWS2_BIN") || default_zapret2_bin;
const BYEDPI_BIN = getenv("BYEDPI_BIN") || "/usr/bin/ciadpi";
let read_json_file = common.read_json_file;
+5 -1
View File
@@ -48,7 +48,11 @@ const SB_SERVICE_MIXED_INBOUND_PORT = getenv("SB_SERVICE_MIXED_INBOUND_PORT") ||
const SB_VARIANT_STATE_FILE = getenv("SB_VARIANT_STATE_FILE") || "/etc/tachyon/sing-box-variant";
const SB_VERSION_STATE_FILE = getenv("SB_VERSION_STATE_FILE") || "/etc/tachyon/sing-box-version";
const ZAPRET_PROVIDER_NFQWS_BIN = getenv("ZAPRET_PROVIDER_NFQWS_BIN") || "/opt/zapret/nfq/nfqws";
const ZAPRET2_PROVIDER_NFQWS2_BIN = getenv("ZAPRET2_PROVIDER_NFQWS2_BIN") || "/opt/zapret2/nfq2/nfqws2";
let default_zapret2_bin = "/opt/zapret2/nfq2/nfqws2";
for (let b in [ "/opt/zapret2/nfq2/nfqws2", "/opt/zapret2/nfq/nfqws2", "/opt/zapret2/nfqws2", "/usr/bin/nfqws2" ]) {
if (fs.stat(b) != null) { default_zapret2_bin = b; break; }
}
const ZAPRET2_PROVIDER_NFQWS2_BIN = getenv("ZAPRET2_PROVIDER_NFQWS2_BIN") || default_zapret2_bin;
const BYEDPI_BIN = getenv("BYEDPI_BIN") || "/usr/bin/ciadpi";
function read_stdin() {
+2 -2
View File
@@ -319,11 +319,11 @@ grep -Fq 'procd_tachyon' "$PACKAGE_UC" ||
# Verify that write_backend_ipk_control includes autostart enable and package_postinst
sed -n '/^write_backend_ipk_control() {/,/^write_app_ipk_control() {/p' "$BUILD_SCRIPT" |
grep -Fq '/usr/bin/tachyon package_postinst' ||
grep -F '/usr/bin/tachyon package_postinst' >/dev/null ||
fail "backend IPK postinst must call /usr/bin/tachyon package_postinst"
sed -n '/^write_backend_ipk_control() {/,/^write_app_ipk_control() {/p' "$BUILD_SCRIPT" |
grep -Fq '/etc/init.d/tachyon enable' ||
grep -F '/etc/init.d/tachyon enable' >/dev/null ||
fail "backend IPK postinst must ensure service autostart is enabled"
# Verify all postrm scripts guard against upgrade