feat: поддержка HTTP/HTTPS прокси (#45), комплексная доработка деинсталляции и отката конфигурации (1.3.22)

This commit is contained in:
Dushnilin
2026-09-06 14:12:27 +03:00
parent db45d5d84d
commit fd79c4e317
25 changed files with 972 additions and 161 deletions
+62 -4
View File
@@ -435,7 +435,18 @@ if (getenv("IPKG_INSTROOT") == null || getenv("IPKG_INSTROOT") == "") {
exit(0); exit(0);
EOF EOF
chmod 0755 "$control_dir/postinst" "$control_dir/prerm" cat > "$control_dir/postrm" <<'EOF'
#!/bin/sh
[ -n "${IPKG_INSTROOT}" ] && exit 0
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
[ -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
EOF
chmod 0755 "$control_dir/postinst" "$control_dir/prerm" "$control_dir/postrm"
} }
write_app_ipk_control() { write_app_ipk_control() {
@@ -480,7 +491,16 @@ EOF
default_prerm $0 $@ default_prerm $0 $@
EOF EOF
chmod 0755 "$control_dir/postinst" "$control_dir/prerm" cat > "$control_dir/postrm" <<'EOF'
#!/bin/sh
[ -n "${IPKG_INSTROOT}" ] && exit 0
rm -f /var/luci-indexcache* /tmp/luci-indexcache* /tmp/luci-modulecache/* 2>/dev/null || true
[ -x /etc/init.d/rpcd ] && /etc/init.d/rpcd restart >/dev/null 2>&1 || true
[ -x /etc/init.d/uhttpd ] && /etc/init.d/uhttpd restart >/dev/null 2>&1 || true
exit 0
EOF
chmod 0755 "$control_dir/postinst" "$control_dir/prerm" "$control_dir/postrm"
} }
write_i18n_ipk_control() { write_i18n_ipk_control() {
@@ -518,7 +538,15 @@ EOF
default_prerm $0 $@ default_prerm $0 $@
EOF EOF
chmod 0755 "$control_dir/postinst" "$control_dir/prerm" cat > "$control_dir/postrm" <<'EOF'
#!/bin/sh
[ -n "${IPKG_INSTROOT}" ] && exit 0
rm -f /var/luci-indexcache* /tmp/luci-indexcache* /tmp/luci-modulecache/* 2>/dev/null || true
[ -x /etc/init.d/rpcd ] && /etc/init.d/rpcd restart >/dev/null 2>&1 || true
exit 0
EOF
chmod 0755 "$control_dir/postinst" "$control_dir/prerm" "$control_dir/postrm"
} }
build_ipk_package() { build_ipk_package() {
@@ -737,6 +765,17 @@ if (getenv("IPKG_INSTROOT") == null || getenv("IPKG_INSTROOT") == "") {
exit(system("TACHYON_LIB=/usr/lib/tachyon ucode -L /usr/lib/tachyon /usr/lib/tachyon/config/migration.uc migrate && /usr/bin/tachyon package_postinst")); exit(system("TACHYON_LIB=/usr/lib/tachyon ucode -L /usr/lib/tachyon /usr/lib/tachyon/config/migration.uc migrate && /usr/bin/tachyon package_postinst"));
} }
exit(0); exit(0);
EOF
cat > "$scripts_dir/backend-post-deinstall.sh" <<'EOF'
#!/bin/sh
[ -n "${IPKG_INSTROOT}" ] && exit 0
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
[ -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
EOF EOF
chmod 0755 "$scripts_dir"/backend-*.sh chmod 0755 "$scripts_dir"/backend-*.sh
@@ -790,6 +829,16 @@ export pkgname="luci-app-tachyon"
add_group_and_user add_group_and_user
default_postinst default_postinst
[ -n "${IPKG_INSTROOT}" ] || /usr/bin/tachyon luci_postinst >/dev/null 2>&1 || true [ -n "${IPKG_INSTROOT}" ] || /usr/bin/tachyon luci_postinst >/dev/null 2>&1 || true
EOF
cat > "$scripts_dir/app-post-deinstall.sh" <<'EOF'
#!/bin/sh
[ -n "${IPKG_INSTROOT}" ] && exit 0
rm -f /var/luci-indexcache* /var/luci-modulecache* 2>/dev/null || true
rm -f /tmp/luci-* 2>/dev/null || true
[ -x /etc/init.d/rpcd ] && /etc/init.d/rpcd restart >/dev/null 2>&1 || true
[ -x /etc/init.d/uhttpd ] && /etc/init.d/uhttpd restart >/dev/null 2>&1 || true
exit 0
EOF EOF
chmod 0755 "$scripts_dir"/app-*.sh chmod 0755 "$scripts_dir"/app-*.sh
@@ -840,6 +889,11 @@ export root="${IPKG_INSTROOT}"
export pkgname="luci-i18n-tachyon-ru" export pkgname="luci-i18n-tachyon-ru"
add_group_and_user add_group_and_user
default_postinst default_postinst
EOF
cat > "$scripts_dir/i18n-post-deinstall.sh" <<'EOF'
#!/bin/sh
exit 0
EOF EOF
chmod 0755 "$scripts_dir"/i18n-*.sh chmod 0755 "$scripts_dir"/i18n-*.sh
@@ -889,6 +943,7 @@ build_apk_package() {
-s "pre-install:${temp_scripts}/${script_prefix}-pre-install.sh" \ -s "pre-install:${temp_scripts}/${script_prefix}-pre-install.sh" \
-s "post-install:${temp_scripts}/${script_prefix}-post-install.sh" \ -s "post-install:${temp_scripts}/${script_prefix}-post-install.sh" \
-s "pre-deinstall:${temp_scripts}/${script_prefix}-pre-deinstall.sh" \ -s "pre-deinstall:${temp_scripts}/${script_prefix}-pre-deinstall.sh" \
-s "post-deinstall:${temp_scripts}/${script_prefix}-post-deinstall.sh" \
-s "pre-upgrade:${temp_scripts}/${script_prefix}-pre-upgrade.sh" \ -s "pre-upgrade:${temp_scripts}/${script_prefix}-pre-upgrade.sh" \
-s "post-upgrade:${temp_scripts}/${script_prefix}-post-upgrade.sh" -s "post-upgrade:${temp_scripts}/${script_prefix}-post-upgrade.sh"
elif have_passwordless_sudo; then elif have_passwordless_sudo; then
@@ -909,6 +964,7 @@ build_apk_package() {
-s "pre-install:${temp_scripts}/${script_prefix}-pre-install.sh" \ -s "pre-install:${temp_scripts}/${script_prefix}-pre-install.sh" \
-s "post-install:${temp_scripts}/${script_prefix}-post-install.sh" \ -s "post-install:${temp_scripts}/${script_prefix}-post-install.sh" \
-s "pre-deinstall:${temp_scripts}/${script_prefix}-pre-deinstall.sh" \ -s "pre-deinstall:${temp_scripts}/${script_prefix}-pre-deinstall.sh" \
-s "post-deinstall:${temp_scripts}/${script_prefix}-post-deinstall.sh" \
-s "pre-upgrade:${temp_scripts}/${script_prefix}-pre-upgrade.sh" \ -s "pre-upgrade:${temp_scripts}/${script_prefix}-pre-upgrade.sh" \
-s "post-upgrade:${temp_scripts}/${script_prefix}-post-upgrade.sh" -s "post-upgrade:${temp_scripts}/${script_prefix}-post-upgrade.sh"
sudo chown "$(id -u):$(id -g)" "$output_file" sudo chown "$(id -u):$(id -g)" "$output_file"
@@ -933,6 +989,7 @@ build_apk_package() {
-s pre-install:'$temp_scripts/${script_prefix}-pre-install.sh' \ -s pre-install:'$temp_scripts/${script_prefix}-pre-install.sh' \
-s post-install:'$temp_scripts/${script_prefix}-post-install.sh' \ -s post-install:'$temp_scripts/${script_prefix}-post-install.sh' \
-s pre-deinstall:'$temp_scripts/${script_prefix}-pre-deinstall.sh' \ -s pre-deinstall:'$temp_scripts/${script_prefix}-pre-deinstall.sh' \
-s post-deinstall:'$temp_scripts/${script_prefix}-post-deinstall.sh' \
-s pre-upgrade:'$temp_scripts/${script_prefix}-pre-upgrade.sh' \ -s pre-upgrade:'$temp_scripts/${script_prefix}-pre-upgrade.sh' \
-s post-upgrade:'$temp_scripts/${script_prefix}-post-upgrade.sh' -s post-upgrade:'$temp_scripts/${script_prefix}-post-upgrade.sh'
" "
@@ -957,6 +1014,7 @@ build_apk_package() {
-s pre-install:'$temp_scripts/${script_prefix}-pre-install.sh' \ -s pre-install:'$temp_scripts/${script_prefix}-pre-install.sh' \
-s post-install:'$temp_scripts/${script_prefix}-post-install.sh' \ -s post-install:'$temp_scripts/${script_prefix}-post-install.sh' \
-s pre-deinstall:'$temp_scripts/${script_prefix}-pre-deinstall.sh' \ -s pre-deinstall:'$temp_scripts/${script_prefix}-pre-deinstall.sh' \
-s post-deinstall:'$temp_scripts/${script_prefix}-post-deinstall.sh' \
-s pre-upgrade:'$temp_scripts/${script_prefix}-pre-upgrade.sh' \ -s pre-upgrade:'$temp_scripts/${script_prefix}-pre-upgrade.sh' \
-s post-upgrade:'$temp_scripts/${script_prefix}-post-upgrade.sh' -s post-upgrade:'$temp_scripts/${script_prefix}-post-upgrade.sh'
" 2>"$stderr_file"; then " 2>"$stderr_file"; then
@@ -1100,7 +1158,7 @@ main() {
[[ -x "$apk_bin" ]] || { echo "apk host tool not found at $apk_bin" >&2; exit 1; } [[ -x "$apk_bin" ]] || { echo "apk host tool not found at $apk_bin" >&2; exit 1; }
echo "Building RAG index..." >&2 echo "Building RAG index..." >&2
bash "$ROOT_DIR/tools/rag_build_index.sh" "$ROOT_DIR/docs/knowledge-base" "$ROOT_DIR/tachyon/files/usr/lib/tachyon" || true bash "$ROOT_DIR/tools/rag_build_index.sh" "$ROOT_DIR/docs/knowledge-base" "$ROOT_DIR/tachyon/files/usr/lib" || true
build_backend_root "$backend_root" build_backend_root "$backend_root"
build_app_root "$app_root" build_app_root "$app_root"
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "fe-app-tachyon", "name": "fe-app-tachyon",
"version": "1.3.21", "version": "1.3.22",
"license": "MIT", "license": "MIT",
"type": "module", "type": "module",
"scripts": { "scripts": {
@@ -1,5 +1,5 @@
const COPYABLE_PROXY_URI_RE = const COPYABLE_PROXY_URI_RE =
/^(vless|vmess|trojan|ss|ssr|hysteria2|hy2|tuic|socks4|socks4a|socks5):\/\//i; /^(vless|vmess|trojan|ss|ssr|hysteria2|hy2|tuic|socks4|socks4a|socks5|http|https):\/\//i;
export function isCopyableProxyLink(link?: string) { export function isCopyableProxyLink(link?: string) {
return COPYABLE_PROXY_URI_RE.test((link || '').trim()); return COPYABLE_PROXY_URI_RE.test((link || '').trim());
@@ -13,6 +13,8 @@ const copyableLinks = [
'socks4://example.com:1080', 'socks4://example.com:1080',
'socks4a://example.com:1080', 'socks4a://example.com:1080',
'socks5://user:pass@example.com:1080', 'socks5://user:pass@example.com:1080',
'http://example.com:80',
'https://user:pass@example.com:443',
]; ];
const nonCopyableLinks = [ const nonCopyableLinks = [
@@ -20,12 +22,8 @@ const nonCopyableLinks = [
'direct', 'direct',
'block', 'block',
'urltest', 'urltest',
'http://example.com:80',
'https://user:pass@example.com:443',
'https://example.com/subscription',
'https://example.com',
'http://example.com:99999',
'wireguard://example.com', 'wireguard://example.com',
'tcp://127.0.0.1:8080',
]; ];
describe('isCopyableProxyLink', () => { describe('isCopyableProxyLink', () => {
@@ -0,0 +1,38 @@
import { describe, it, expect } from 'vitest';
import { validateHttpProxyUrl } from '../validateHttpProxyUrl';
const validUrls = [
['http with port', 'http://example.com:8080'],
['https with port', 'https://example.com:8443'],
['http with auth', 'http://user:pass@example.com:8080'],
['http username only', 'http://user@example.com:8080'],
['http ip with port', 'http://127.0.0.1:8080'],
['http ipv6', 'http://[2001:db8::1]:8080'],
['https ipv6', 'https://user:pass@[2001:db8::1]:8443'],
];
const invalidUrls = [
['missing port', 'http://example.com'],
['missing host', 'http://:8080'],
['invalid port', 'http://example.com:99999'],
['path present', 'https://example.com:443/path'],
['query present', 'https://example.com:443?token=abc'],
['space present', 'http://exa mple.com:80'],
['empty username', 'http://:pass@example.com:80'],
['unsupported scheme', 'socks5://example.com:1080'],
['unbracketed ipv6 with port', 'http://2001:db8::1:8080'],
];
describe('validateHttpProxyUrl', () => {
describe.each(validUrls)('valid URL: %s', (_desc, url) => {
it('returns valid=true', () => {
expect(validateHttpProxyUrl(url).valid).toBe(true);
});
});
describe.each(invalidUrls)('invalid URL: %s', (_desc, url) => {
it('returns valid=false', () => {
expect(validateHttpProxyUrl(url).valid).toBe(false);
});
});
});
@@ -31,6 +31,8 @@ const validUrls = [
['socks4', 'socks4://127.0.0.1:1080'], ['socks4', 'socks4://127.0.0.1:1080'],
['socks4a', 'socks4a://example.com:1080'], ['socks4a', 'socks4a://example.com:1080'],
['socks5', 'socks5://user:pass@example.com:1080'], ['socks5', 'socks5://user:pass@example.com:1080'],
['http', 'http://127.0.0.1:80'],
['https', 'https://user:pass@example.com:443'],
['hysteria2', 'hysteria2://password@example.com:443'], ['hysteria2', 'hysteria2://password@example.com:443'],
['hy2', 'hy2://password@example.com:443'], ['hy2', 'hy2://password@example.com:443'],
]; ];
@@ -55,14 +57,16 @@ describe('validateProxyUrl', () => {
expect(res.valid).toBe(false); expect(res.valid).toBe(false);
expect(res.message).toContain('vmess://'); expect(res.message).toContain('vmess://');
expect(res.message).toContain('socks4a://'); expect(res.message).toContain('socks4a://');
expect(res.message).toContain('http://');
expect(res.message).toContain('https://');
expect(res.message).toContain('hy2://'); expect(res.message).toContain('hy2://');
}); });
it('rejects native HTTP proxy URLs', () => { it('accepts native HTTP and HTTPS proxy URLs', () => {
const res = validateProxyUrl('https://user:pass@example.com:443'); expect(validateProxyUrl('http://127.0.0.1:8080').valid).toBe(true);
expect(validateProxyUrl('https://user:pass@example.com:443').valid).toBe(
expect(res.valid).toBe(false); true,
expect(res.message).not.toContain('http://'); );
}); });
it('rejects VMess configs with an invalid server host', () => { it('rejects VMess configs with an invalid server host', () => {
@@ -0,0 +1,90 @@
import { ValidationResult } from './types';
import { isValidPort, parseHostPort } from './hostPort';
export function validateHttpProxyUrl(url: string): ValidationResult {
try {
if (!/^https?:\/\//.test(url)) {
return {
valid: false,
message: _(
'Invalid HTTP proxy URL: must start with http:// or https://',
),
};
}
if (!url || /\s/.test(url)) {
return {
valid: false,
message: _('Invalid HTTP proxy URL: must not contain spaces'),
};
}
const body = url.replace(/^https?:\/\//, '');
if (/[/?#]/.test(body)) {
return {
valid: false,
message: _(
'Invalid HTTP proxy URL: path, query, and fragment are not supported',
),
};
}
const atIndex = body.lastIndexOf('@');
const credentials = atIndex >= 0 ? body.slice(0, atIndex) : '';
const hostPortPart = atIndex >= 0 ? body.slice(atIndex + 1) : body;
if (credentials) {
const [username] = credentials.split(':');
if (!username) {
return {
valid: false,
message: _('Invalid HTTP proxy URL: missing username'),
};
}
}
if (!hostPortPart) {
return {
valid: false,
message: _('Invalid HTTP proxy URL: missing host and port'),
};
}
const parsedHostPort = parseHostPort(hostPortPart);
if (!parsedHostPort) {
return {
valid: false,
message: _('Invalid HTTP proxy URL: invalid host and port'),
};
}
const { host, port } = parsedHostPort;
if (!host) {
return {
valid: false,
message: _('Invalid HTTP proxy URL: missing hostname or IP'),
};
}
if (!port) {
return {
valid: false,
message: _('Invalid HTTP proxy URL: missing port'),
};
}
if (!isValidPort(port)) {
return {
valid: false,
message: _('Invalid HTTP proxy URL: invalid port number'),
};
}
} catch (_e) {
return {
valid: false,
message: _('Invalid HTTP proxy URL: parsing failed'),
};
}
return { valid: true, message: _('Valid') };
}
@@ -6,6 +6,7 @@ import { validateTrojanUrl } from './validateTrojanUrl';
import { validateSocksUrl } from './validateSocksUrl'; import { validateSocksUrl } from './validateSocksUrl';
import { validateHysteria2Url } from './validateHysteriaUrl'; import { validateHysteria2Url } from './validateHysteriaUrl';
import { validateTuicUrl } from './validateTuicUrl'; import { validateTuicUrl } from './validateTuicUrl';
import { validateHttpProxyUrl } from './validateHttpProxyUrl';
// TODO refactor current validation and add tests // TODO refactor current validation and add tests
export function validateProxyUrl(url: string): ValidationResult { export function validateProxyUrl(url: string): ValidationResult {
@@ -31,6 +32,10 @@ export function validateProxyUrl(url: string): ValidationResult {
return validateSocksUrl(trimmedUrl); return validateSocksUrl(trimmedUrl);
} }
if (/^https?:\/\//.test(trimmedUrl)) {
return validateHttpProxyUrl(trimmedUrl);
}
if ( if (
trimmedUrl.startsWith('hysteria2://') || trimmedUrl.startsWith('hysteria2://') ||
trimmedUrl.startsWith('hy2://') trimmedUrl.startsWith('hy2://')
@@ -45,7 +50,7 @@ export function validateProxyUrl(url: string): ValidationResult {
return { return {
valid: false, valid: false,
message: _( message: _(
'URL must start with vless://, vmess://, ss://, trojan://, socks4://, socks4a://, socks5://, hysteria2://, hy2://, or tuic://', 'URL must start with vless://, vmess://, ss://, trojan://, socks4://, socks4a://, socks5://, http://, https://, hysteria2://, hy2://, or tuic://',
), ),
}; };
} }
+12
View File
@@ -33,6 +33,18 @@ define Build/Prepare/$(PKG_NAME)
sed -i -e 's/__COMPILED_VERSION_VARIABLE__/$(TACHYON_COMPILED_VERSION)/g' $(PKG_BUILD_DIR)/htdocs/luci-static/resources/view/tachyon/main.js || true sed -i -e 's/__COMPILED_VERSION_VARIABLE__/$(TACHYON_COMPILED_VERSION)/g' $(PKG_BUILD_DIR)/htdocs/luci-static/resources/view/tachyon/main.js || true
endef endef
define Package/$(PKG_NAME)/postrm
#!/bin/sh
[ -n "$${IPKG_INSTROOT}" ] && exit 0
rm -f /var/luci-indexcache* /tmp/luci-indexcache* /tmp/luci-modulecache/* 2>/dev/null || true
[ -x /etc/init.d/rpcd ] && /etc/init.d/rpcd restart >/dev/null 2>&1 || true
[ -x /etc/init.d/uhttpd ] && /etc/init.d/uhttpd restart >/dev/null 2>&1 || true
exit 0
endef
include $(TOPDIR)/feeds/luci/luci.mk include $(TOPDIR)/feeds/luci/luci.mk
# call BuildPackage - OpenWrt buildroot signature # call BuildPackage - OpenWrt buildroot signature
@@ -930,6 +930,85 @@ function validateTuicUrl(url) {
} }
} }
// src/validators/validateHttpProxyUrl.ts
function validateHttpProxyUrl(url) {
try {
if (!/^https?:\/\//.test(url)) {
return {
valid: false,
message: _(
"Invalid HTTP proxy URL: must start with http:// or https://"
)
};
}
if (!url || /\s/.test(url)) {
return {
valid: false,
message: _("Invalid HTTP proxy URL: must not contain spaces")
};
}
const body = url.replace(/^https?:\/\//, "");
if (/[/?#]/.test(body)) {
return {
valid: false,
message: _(
"Invalid HTTP proxy URL: path, query, and fragment are not supported"
)
};
}
const atIndex = body.lastIndexOf("@");
const credentials = atIndex >= 0 ? body.slice(0, atIndex) : "";
const hostPortPart = atIndex >= 0 ? body.slice(atIndex + 1) : body;
if (credentials) {
const [username] = credentials.split(":");
if (!username) {
return {
valid: false,
message: _("Invalid HTTP proxy URL: missing username")
};
}
}
if (!hostPortPart) {
return {
valid: false,
message: _("Invalid HTTP proxy URL: missing host and port")
};
}
const parsedHostPort = parseHostPort(hostPortPart);
if (!parsedHostPort) {
return {
valid: false,
message: _("Invalid HTTP proxy URL: invalid host and port")
};
}
const { host, port } = parsedHostPort;
if (!host) {
return {
valid: false,
message: _("Invalid HTTP proxy URL: missing hostname or IP")
};
}
if (!port) {
return {
valid: false,
message: _("Invalid HTTP proxy URL: missing port")
};
}
if (!isValidPort(port)) {
return {
valid: false,
message: _("Invalid HTTP proxy URL: invalid port number")
};
}
} catch (_e) {
return {
valid: false,
message: _("Invalid HTTP proxy URL: parsing failed")
};
}
return { valid: true, message: _("Valid") };
}
// src/validators/validateProxyUrl.ts // src/validators/validateProxyUrl.ts
function validateProxyUrl(url) { function validateProxyUrl(url) {
const trimmedUrl = url.trim(); const trimmedUrl = url.trim();
@@ -948,6 +1027,9 @@ function validateProxyUrl(url) {
if (/^socks(4|4a|5):\/\//.test(trimmedUrl)) { if (/^socks(4|4a|5):\/\//.test(trimmedUrl)) {
return validateSocksUrl(trimmedUrl); return validateSocksUrl(trimmedUrl);
} }
if (/^https?:\/\//.test(trimmedUrl)) {
return validateHttpProxyUrl(trimmedUrl);
}
if (trimmedUrl.startsWith("hysteria2://") || trimmedUrl.startsWith("hy2://")) { if (trimmedUrl.startsWith("hysteria2://") || trimmedUrl.startsWith("hy2://")) {
return validateHysteria2Url(trimmedUrl); return validateHysteria2Url(trimmedUrl);
} }
@@ -957,7 +1039,7 @@ function validateProxyUrl(url) {
return { return {
valid: false, valid: false,
message: _( message: _(
"URL must start with vless://, vmess://, ss://, trojan://, socks4://, socks4a://, socks5://, hysteria2://, hy2://, or tuic://" "URL must start with vless://, vmess://, ss://, trojan://, socks4://, socks4a://, socks5://, http://, https://, hysteria2://, hy2://, or tuic://"
) )
}; };
} }
@@ -1336,7 +1418,7 @@ function insertIf(condition, elements) {
} }
// src/helpers/isCopyableProxyLink.ts // src/helpers/isCopyableProxyLink.ts
var COPYABLE_PROXY_URI_RE = /^(vless|vmess|trojan|ss|ssr|hysteria2|hy2|tuic|socks4|socks4a|socks5):\/\//i; var COPYABLE_PROXY_URI_RE = /^(vless|vmess|trojan|ss|ssr|hysteria2|hy2|tuic|socks4|socks4a|socks5|http|https):\/\//i;
function isCopyableProxyLink(link) { function isCopyableProxyLink(link) {
return COPYABLE_PROXY_URI_RE.test((link || "").trim()); return COPYABLE_PROXY_URI_RE.test((link || "").trim());
} }
+14
View File
@@ -145,6 +145,20 @@ if [ -d /www/cgi-bin ]; then
fi fi
endef endef
define Package/tachyon/postrm
#!/bin/sh
[ -n "$${IPKG_INSTROOT}" ] && exit 0
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
[ -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
endef
define Package/tachyon/conffiles define Package/tachyon/conffiles
/etc/config/tachyon /etc/config/tachyon
endef endef
+276
View File
@@ -0,0 +1,276 @@
#!/usr/bin/env ucode
// Tachyon Clean Uninstaller Module
// Fully restores dnsmasq / dhcp, flushes nftables & policy routing,
// removes Tachyon packages & crontab hooks, restores Forkop/Podkop if present,
// and safely uninstalls Tachyon without leaving lingering state on OpenWrt.
let fs = require("fs");
let common = require("core.common");
let as_string = common.as_string;
function run_cmd(args) {
let parts = [];
for (let arg in args)
push(parts, "'" + replace(as_string(arg), /'/g, "'\\''") + "'");
return system(join(" ", parts) + " >/dev/null 2>&1");
}
function run_shell(cmd) {
return system(cmd + " >/dev/null 2>&1");
}
function log_step(msg) {
print("[Tachyon Uninstall] ", msg, "\n");
}
function remove_file(path) {
if (fs.stat(path) != null)
fs.unlink(path);
}
function remove_tree(path) {
if (fs.stat(path) != null)
run_shell("rm -rf '" + replace(as_string(path), /'/g, "'\\''") + "'");
}
function clean_managed_sing_box(keep_binaries) {
if (fs.stat("/etc/init.d/sing-box") != null) {
let content = fs.readfile("/etc/init.d/sing-box");
if (content && index(content, "Tachyon managed sing-box") >= 0) {
run_cmd(["/etc/init.d/sing-box", "stop"]);
run_cmd(["/etc/init.d/sing-box", "disable"]);
remove_file("/etc/init.d/sing-box");
run_shell("rm -f /etc/rc.d/*sing-box* 2>/dev/null || true");
}
}
if (!keep_binaries) {
if (fs.stat("/bin/opkg") != null || fs.stat("/usr/bin/opkg") != null) {
for (let pkg in [ "sing-box-extended", "sing-box-tiny" ]) {
let check = system("opkg list-installed '" + pkg + "' 2>/dev/null | grep -q '^" + pkg + " '");
if (check == 0)
run_cmd(["opkg", "remove", "--force-depends", "--force-remove", pkg]);
}
if (fs.stat("/etc/init.d/sing-box") == null) {
let check_sb = system("opkg list-installed 'sing-box' 2>/dev/null | grep -q '^sing-box '");
if (check_sb == 0)
run_cmd(["opkg", "remove", "--force-depends", "--force-remove", "sing-box"]);
remove_file("/usr/bin/sing-box");
}
}
remove_file("/usr/lib/libcronet.so");
}
}
function clean_rt_tables() {
let rt_path = "/etc/iproute2/rt_tables";
if (fs.stat(rt_path) == null)
return;
let content = fs.readfile(rt_path);
if (!content || index(content, "tachyon") < 0)
return;
let lines = split(content, "\n");
let clean = [];
for (let line in lines) {
if (match(line, /105\s+tachyon/) || match(line, /^\s*\d+\s+tachyon\s*$/))
continue;
push(clean, line);
}
fs.writefile(rt_path, join("\n", clean) + "\n");
}
function clean_crontab() {
run_shell("crontab -l 2>/dev/null | grep -v -E 'tachyon|parental_quota_tick' | crontab - 2>/dev/null || true");
}
function restore_dhcp_dns() {
let uci_core = null;
try {
uci_core = require("core.uci");
} catch (e) {}
if (uci_core != null) {
try {
// Remove 127.0.0.42 from server list
let servers = uci_core.get("dhcp", "@dnsmasq[0]", "server");
if (servers != null) {
let s_list = type(servers) == "array" ? servers : [ servers ];
let clean_servers = [];
for (let s in s_list) {
s = as_string(s);
if (s != "127.0.0.42" && substr(s, 0, 13) != "127.0.0.42#")
push(clean_servers, s);
}
uci_core.delete("dhcp", "@dnsmasq[0]", "server");
for (let s in clean_servers)
uci_core.add_list("dhcp", "@dnsmasq[0]", "server", s);
}
// Restore original server backup if saved
let backup_servers = uci_core.get("dhcp", "@dnsmasq[0]", "tachyon_server");
if (backup_servers != null && backup_servers != "") {
uci_core.delete("dhcp", "@dnsmasq[0]", "server");
let list = type(backup_servers) == "array" ? backup_servers : split(trim(backup_servers), /[ \t\r\n]+/);
for (let s in list)
if (s != "")
uci_core.add_list("dhcp", "@dnsmasq[0]", "server", s);
uci_core.delete("dhcp", "@dnsmasq[0]", "tachyon_server");
}
// Restore backed up options
for (let opt in [ "noresolv", "cachesize", "rebind_protection", "localuse", "addn_hosts", "notinterface" ]) {
let val = uci_core.get("dhcp", "@dnsmasq[0]", "tachyon_" + opt);
if (val != null && val != "") {
uci_core.set("dhcp", "@dnsmasq[0]", opt, val);
uci_core.delete("dhcp", "@dnsmasq[0]", "tachyon_" + opt);
}
}
// Failsafe sensible defaults
if (uci_core.get("dhcp", "@dnsmasq[0]", "noresolv") == "1")
uci_core.set("dhcp", "@dnsmasq[0]", "noresolv", "0");
if (uci_core.get("dhcp", "@dnsmasq[0]", "cachesize") == "0")
uci_core.set("dhcp", "@dnsmasq[0]", "cachesize", "150");
uci_core.delete("dhcp", "tachyon");
uci_core.commit("dhcp");
} catch (e) {}
} else {
run_shell("uci -q del_list dhcp.@dnsmasq[0].server='127.0.0.42' 2>/dev/null || true");
run_shell("uci -q set dhcp.@dnsmasq[0].noresolv='0' 2>/dev/null || true");
run_shell("uci -q set dhcp.@dnsmasq[0].cachesize='150' 2>/dev/null || true");
run_shell("uci -q delete dhcp.tachyon 2>/dev/null || true");
run_shell("uci -q commit dhcp 2>/dev/null || true");
}
remove_file("/etc/dnsmasq.d/tachyon.conf");
remove_file("/tmp/dnsmasq.d/tachyon.conf");
run_shell("rm -f /etc/dnsmasq.d/tachyon*.conf /tmp/dnsmasq.d/tachyon*.conf 2>/dev/null || true");
if (fs.stat("/etc/init.d/dnsmasq") != null)
run_cmd(["/etc/init.d/dnsmasq", "restart"]);
}
function clean_firewall_and_routing() {
run_cmd(["nft", "delete", "table", "inet", "TachyonTable"]);
run_cmd(["nft", "delete", "table", "inet", "tachyon"]);
run_cmd(["nft", "delete", "table", "ip", "tachyon"]);
run_cmd(["nft", "delete", "table", "ip6", "tachyon"]);
remove_file("/usr/share/nftables.d/chain-pre/input/10-tachyon.nft");
// Restart firewall to purge in-memory dynamic rules from inet fw4
if (fs.stat("/etc/init.d/firewall") != null)
run_cmd(["/etc/init.d/firewall", "restart"]);
run_cmd(["ip", "-4", "rule", "del", "fwmark", "0x04000000/0x04000000", "table", "tachyon", "priority", "105"]);
run_cmd(["ip", "-6", "rule", "del", "fwmark", "0x04000000/0x04000000", "table", "tachyon", "priority", "105"]);
run_cmd(["ip", "-4", "rule", "del", "fwmark", "0x10000000/0x10000000", "lookup", "100"]);
run_cmd(["ip", "-4", "rule", "del", "fwmark", "0x1/0x1", "lookup", "100"]);
run_cmd(["ip", "-4", "rule", "del", "fwmark", "0x2/0x2", "lookup", "100"]);
run_cmd(["ip", "route", "flush", "table", "tachyon"]);
run_cmd(["ip", "route", "flush", "table", "105"]);
run_cmd(["ip", "route", "flush", "table", "100"]);
clean_rt_tables();
// Clean network namespaces
run_cmd(["ip", "netns", "del", "fkpsc"]);
run_cmd(["ip", "link", "del", "fkpsc0"]);
remove_tree("/etc/netns/fkpsc");
}
function uninstall_tachyon(purge_config, keep_binaries) {
log_step("Stopping and disabling Tachyon service...");
remove_file("/var/run/tachyon/starting");
remove_file("/var/run/tachyon/reloading");
run_shell("rm -f /var/run/tachyon*.lock 2>/dev/null || true");
if (fs.stat("/etc/init.d/tachyon") != null) {
run_cmd(["/etc/init.d/tachyon", "stop"]);
run_cmd(["/etc/init.d/tachyon", "disable"]);
}
clean_managed_sing_box(keep_binaries);
// Stop proxy and desync processes
run_shell("killall sing-box nfqws nfqws2 ciadpi 2>/dev/null || true");
// Stop background daemons safely (excluding self PID)
run_shell("ps 2>/dev/null | grep -E 'dns_failover|watchdog|telegram' | grep -v grep | awk '{print $1}' | while read _pid; do [ \"$_pid\" != \"$$\" ] && kill -9 \"$_pid\" 2>/dev/null; done; true");
log_step("Cleaning up firewall, nftables and routing rules...");
clean_firewall_and_routing();
log_step("Restoring DNS and dnsmasq configuration...");
restore_dhcp_dns();
log_step("Cleaning crontab jobs...");
clean_crontab();
log_step("Cleaning auxiliary files and hooks...");
remove_file("/etc/hotplug.d/iface/99-tachyon-wan-monitor");
remove_file("/www/cgi-bin/tachyon-agent");
remove_file("/usr/lib/cgi-bin/tachyon-agent");
remove_file("/usr/bin/tachyon");
remove_file("/etc/init.d/tachyon");
remove_file("/etc/tachyon_commit");
remove_file("/etc/uci-defaults/50_luci-tachyon");
remove_tree("/var/run/tachyon");
run_shell("rm -rf /var/run/tachyon* /var/log/tachyon* /tmp/tachyon* /tmp/sing-box /tmp/ai_doctor* /tmp/tg_* /tmp/warp_* 2>/dev/null || true");
if (purge_config) {
log_step("Purging Tachyon configuration and state (--purge)...");
run_shell("rm -rf /etc/config/tachyon* /etc/tachyon /etc/.tachyon /etc/backup/tachyon_config /etc/sing-box 2>/dev/null || true");
} else {
log_step("Backing up Tachyon configuration...");
let ts = clock()[0];
run_shell("cp -af /etc/config/tachyon /etc/config/tachyon.backup-" + ts + " 2>/dev/null || true");
run_shell("cp -af /etc/config/tachyon /etc/config/tachyon.bak 2>/dev/null || true");
run_shell("chmod 600 /etc/config/tachyon.backup-* /etc/config/tachyon.bak 2>/dev/null || true");
}
log_step("Removing Tachyon packages...");
if (fs.stat("/bin/opkg") != null || fs.stat("/usr/bin/opkg") != null) {
run_cmd(["opkg", "remove", "--force-depends", "--force-remove", "luci-i18n-tachyon-ru", "luci-app-tachyon", "tachyon"]);
} else if (fs.stat("/sbin/apk") != null || fs.stat("/usr/sbin/apk") != null) {
run_cmd(["apk", "del", "luci-i18n-tachyon-ru", "luci-app-tachyon", "tachyon"]);
}
// Clean translations and LuCI views
run_shell("rm -rf /usr/lib/tachyon /usr/share/tachyon /www/luci-static/resources/view/tachyon /usr/share/luci/menu.d/luci-app-tachyon.json /usr/share/rpcd/acl.d/luci-app-tachyon.json /usr/lib/lua/luci/i18n/tachyon.* 2>/dev/null || true");
// Clear LuCI cache and restart rpcd/uhttpd
run_shell("rm -f /var/luci-indexcache* /tmp/luci-indexcache* /tmp/luci-modulecache/* 2>/dev/null || true");
if (fs.stat("/etc/init.d/rpcd") != null)
run_cmd(["/etc/init.d/rpcd", "restart"]);
if (fs.stat("/etc/init.d/uhttpd") != null)
run_cmd(["/etc/init.d/uhttpd", "restart"]);
// Restore parent Forkop / Podkop services if present
if (fs.stat("/etc/init.d/forkop") != null) {
log_step("Forkop detected! Restoring Forkop service...");
run_cmd(["/etc/init.d/forkop", "enable"]);
run_cmd(["/etc/init.d/forkop", "restart"]);
} else if (fs.stat("/etc/init.d/podkop") != null) {
log_step("Podkop detected! Restoring Podkop service...");
run_cmd(["/etc/init.d/podkop", "enable"]);
run_cmd(["/etc/init.d/podkop", "restart"]);
}
log_step("Tachyon has been successfully uninstalled.");
return 0;
}
let purge = false;
let keep_bin = false;
for (let i = 0; i < length(ARGV); i++) {
let arg = as_string(ARGV[i]);
if (arg == "--purge" || arg == "-p")
purge = true;
if (arg == "--keep-binaries")
keep_bin = true;
}
exit(uninstall_tachyon(purge, keep_bin));
@@ -162,7 +162,7 @@ function supported_subscription_outbound(outbound) {
if (t == "direct" || t == "selector" || t == "urltest" || t == "dns" || t == "block") if (t == "direct" || t == "selector" || t == "urltest" || t == "dns" || t == "block")
return false; return false;
let is_proto = (t == "vless" || t == "vmess" || t == "trojan" || t == "shadowsocks" || let is_proto = (t == "vless" || t == "vmess" || t == "trojan" || t == "shadowsocks" ||
t == "socks" || t == "hysteria2" || t == "tuic"); t == "socks" || t == "http" || t == "hysteria2" || t == "tuic");
if (!is_proto) if (!is_proto)
return false; return false;
if (is_dummy_outbound_server(outbound.server, outbound.server_port)) if (is_dummy_outbound_server(outbound.server, outbound.server_port))
@@ -574,6 +574,32 @@ function apply_link_transport(outbound, query) {
outbound.transport = result; outbound.transport = result;
} }
function manual_http_outbound(link, tag_name) {
let scheme = url_scheme(link);
let host = url_host(link);
let port = parse_port(url_port(link));
let path = url_path(link);
if (host == "" || port == null || (path != "" && path != "/") || index(link, "?") >= 0)
ctx.runtime_generate_unsupported("manual HTTP proxy link is invalid");
let outbound = {
type: "http",
tag: tag_name,
server: host,
server_port: port
};
let userinfo = url_userinfo(link);
if (userinfo != "") {
let colon = index(userinfo, ":");
outbound.username = colon >= 0 ? substr(userinfo, 0, colon) : userinfo;
if (colon >= 0)
outbound.password = substr(userinfo, colon + 1);
}
if (scheme == "https")
outbound.tls = { enabled: true };
return outbound;
}
function manual_socks_outbound(link, tag_name) { function manual_socks_outbound(link, tag_name) {
let scheme = url_scheme(link); let scheme = url_scheme(link);
let host = url_host(link); let host = url_host(link);
@@ -874,6 +900,8 @@ function manual_link_outbound(link, tag_name) {
link = url_strip_fragment_value(url_decode(link)); link = url_strip_fragment_value(url_decode(link));
scheme = url_scheme(link); scheme = url_scheme(link);
if (scheme == "http" || scheme == "https")
return manual_http_outbound(link, tag_name);
if (scheme == "socks4" || scheme == "socks4a" || scheme == "socks5") if (scheme == "socks4" || scheme == "socks4a" || scheme == "socks5")
return manual_socks_outbound(link, tag_name); return manual_socks_outbound(link, tag_name);
if (scheme == "ss") if (scheme == "ss")
@@ -1621,6 +1649,7 @@ return {
assert_unique_outbound_tags, assert_unique_outbound_tags,
uci_bin_to_hex, uci_bin_to_hex,
unique_string_array, unique_string_array,
manual_http_outbound,
manual_socks_outbound, manual_socks_outbound,
manual_shadowsocks_outbound, manual_shadowsocks_outbound,
manual_vless_outbound, manual_vless_outbound,
@@ -1158,6 +1158,58 @@ function tproxy_inbound_matcher() {
return [ runtime_constants.TPROXY_INBOUND_TAG, runtime_constants.TPROXY_INBOUND6_TAG ]; return [ runtime_constants.TPROXY_INBOUND_TAG, runtime_constants.TPROXY_INBOUND6_TAG ];
} }
function outbound_supports_udp(config, tag_name) {
tag_name = as_string(tag_name);
if (tag_name == "")
return true;
for (let outb in (config.outbounds || [])) {
if (as_string(outb.tag) == tag_name) {
let t = as_string(outb.type);
if (t == "http")
return false;
if (t == "selector" || t == "urltest") {
if (type(outb.outbounds) == "array" && length(outb.outbounds) > 0) {
let has_udp = false;
for (let nested_tag in outb.outbounds) {
if (outbound_supports_udp(config, nested_tag)) {
has_udp = true;
break;
}
}
return has_udp;
}
}
return true;
}
}
return true;
}
function push_section_route_rule(config, rule, target_outbound) {
if (target_outbound && !outbound_supports_udp(config, target_outbound)) {
if (rule.network == "tcp") {
push(config.route.rules, rule);
return;
}
if (rule.network == "udp") {
delete rule.outbound;
rule.action = "reject";
push(config.route.rules, rule);
return;
}
let udp_rule = {};
for (let k, v in rule)
udp_rule[k] = v;
delete udp_rule.outbound;
udp_rule.action = "reject";
udp_rule.network = "udp";
push(config.route.rules, udp_rule);
rule.network = "tcp";
}
push(config.route.rules, rule);
}
function add_fully_routed_ips_rule(config, section) { function add_fully_routed_ips_rule(config, section) {
let source_ip_cidr = list_option(section, "fully_routed_ips"); let source_ip_cidr = list_option(section, "fully_routed_ips");
if (length(source_ip_cidr) == 0) if (length(source_ip_cidr) == 0)
@@ -1174,7 +1226,7 @@ function add_fully_routed_ips_rule(config, section) {
if (target.outbound) if (target.outbound)
route_rule.outbound = target.outbound; route_rule.outbound = target.outbound;
route_rule.source_ip_cidr = single_or_array(source_ip_cidr); route_rule.source_ip_cidr = single_or_array(source_ip_cidr);
push(config.route.rules, route_rule); push_section_route_rule(config, route_rule, target.outbound);
} }
function add_excluded_ips_rule(config, section) { function add_excluded_ips_rule(config, section) {
@@ -1338,13 +1390,13 @@ function add_combined_route_for_section(config, section) {
else if (type(resolve) == "object" && resolve.rule) else if (type(resolve) == "object" && resolve.rule)
push(config.route.rules, resolve.rule); push(config.route.rules, resolve.rule);
push(config.route.rules, domain_rule); push_section_route_rule(config, domain_rule, target.outbound);
} }
if (has_ip_cidr) { if (has_ip_cidr) {
let ip_rule = create_section_route_rule(); let ip_rule = create_section_route_rule();
ip_rule.ip_cidr = ip_cidr; ip_rule.ip_cidr = ip_cidr;
push(config.route.rules, ip_rule); push_section_route_rule(config, ip_rule, target.outbound);
} }
let country_list = connections.geoip_country_list(section); let country_list = connections.geoip_country_list(section);
@@ -1370,7 +1422,7 @@ function add_combined_route_for_section(config, section) {
if (country_mode == "exclude") if (country_mode == "exclude")
geoip_route_rule.invert = true; geoip_route_rule.invert = true;
push(config.route.rules, geoip_route_rule); push_section_route_rule(config, geoip_route_rule, target.outbound);
} }
} }
@@ -1380,7 +1432,7 @@ function add_combined_route_for_section(config, section) {
fallback_rule.port != null || fallback_rule.port_range != null || fallback_rule.port != null || fallback_rule.port_range != null ||
fallback_rule.protocol != null || fallback_rule.dscp != null; fallback_rule.protocol != null || fallback_rule.dscp != null;
if (has_any_matcher) if (has_any_matcher)
push(config.route.rules, fallback_rule); push_section_route_rule(config, fallback_rule, target.outbound);
} }
let rewrite_ttl = int_option(ctx.runtime_settings(), "dns_rewrite_ttl", "60"); let rewrite_ttl = int_option(ctx.runtime_settings(), "dns_rewrite_ttl", "60");
@@ -1620,12 +1672,12 @@ function add_service_route_rules(config, sections) {
}); });
catchall_target = FAILOVER_GROUP_TAG; catchall_target = FAILOVER_GROUP_TAG;
} }
push(config.route.rules, { push_section_route_rule(config, {
action: "route", action: "route",
inbound: tproxy_inbound_matcher(), inbound: tproxy_inbound_matcher(),
outbound: catchall_target, outbound: catchall_target,
domain: runtime_constants.CHECK_PROXY_IP_DOMAIN domain: runtime_constants.CHECK_PROXY_IP_DOMAIN
}); }, catchall_target);
} }
push(config.route.rules, { push(config.route.rules, {
action: "route-options", action: "route-options",
@@ -1735,7 +1787,7 @@ function add_server_routes(config, servers, sections) {
}; };
if (target.outbound) if (target.outbound)
rule.outbound = target.outbound; rule.outbound = target.outbound;
push(config.route.rules, rule); push_section_route_rule(config, rule, target.outbound);
} }
else { else {
ctx.runtime_generate_unsupported("unsupported server routing_mode " + routing_mode); ctx.runtime_generate_unsupported("unsupported server routing_mode " + routing_mode);
@@ -1610,6 +1610,16 @@ function parse_clash_record(record) {
outbound.password = as_string(record.password); outbound.password = as_string(record.password);
return outbound; return outbound;
} }
if (proxy_type == "http" || proxy_type == "https") {
let outbound = { type: "http", tag: name, server: server, server_port: port };
if (as_string(record.username) != "")
outbound.username = as_string(record.username);
if (as_string(record.password) != "")
outbound.password = as_string(record.password);
if (proxy_type == "https" || is_true(record.tls))
outbound.tls = { enabled: true };
return outbound;
}
return null; return null;
} }
@@ -1,80 +0,0 @@
#!/usr/bin/env ucode
// Tachyon Clean Uninstaller Module
// Fully restores dnsmasq, cleans nftables/tproxy, restores Forkop/Podkop if present,
// and safely removes Tachyon packages without breaking router configuration.
let fs = require("fs");
let common = require("core.common");
let as_string = common.as_string;
function run_cmd(args) {
let parts = [];
for (let arg in args)
push(parts, "'" + replace(as_string(arg), /'/g, "'\\''") + "'");
return system(join(" ", parts) + " >/dev/null 2>&1");
}
function log_step(msg) {
print("[Tachyon Uninstall] ", msg, "\n");
}
function uninstall_tachyon(purge_config) {
log_step("Stopping and disabling Tachyon service...");
run_cmd(["/etc/init.d/tachyon", "stop"]);
run_cmd(["/etc/init.d/tachyon", "disable"]);
log_step("Cleaning up firewall and nftables rules...");
run_cmd(["nft", "delete", "table", "inet", "tachyon"]);
run_cmd(["nft", "delete", "table", "ip", "tachyon"]);
run_cmd(["nft", "delete", "table", "ip6", "tachyon"]);
run_cmd(["ip", "rule", "del", "fwmark", "0x1/0x1"]);
run_cmd(["ip", "rule", "del", "fwmark", "0x2/0x2"]);
run_cmd(["ip", "route", "flush", "table", "100"]);
log_step("Restoring dnsmasq configuration...");
run_cmd(["rm", "-f", "/etc/dnsmasq.d/tachyon.conf"]);
run_cmd(["rm", "-f", "/tmp/dnsmasq.d/tachyon.conf"]);
run_cmd(["rm", "-rf", "/tmp/dnsmasq.d/tachyon-*.conf"]);
if (fs.stat("/etc/config/dnsmasq.tachyon-bak") != null) {
run_cmd(["cp", "-f", "/etc/config/dnsmasq.tachyon-bak", "/etc/config/dnsmasq"]);
log_step("Restored original dnsmasq config from backup.");
}
run_cmd(["/etc/init.d/dnsmasq", "restart"]);
if (purge_config) {
log_step("Purging Tachyon configuration...");
run_cmd(["rm", "-rf", "/etc/config/tachyon"]);
run_cmd(["rm", "-rf", "/etc/tachyon"]);
} else {
log_step("Backing up Tachyon configuration to /etc/config/tachyon.bak...");
run_cmd(["cp", "-af", "/etc/config/tachyon", "/etc/config/tachyon.bak"]);
}
// Check if Forkop or Podkop was installed prior and re-enable it
if (fs.stat("/etc/init.d/forkop") != null) {
log_step("Forkop detected! Restoring Forkop service...");
run_cmd(["/etc/init.d/forkop", "enable"]);
run_cmd(["/etc/init.d/forkop", "restart"]);
} else if (fs.stat("/etc/init.d/podkop") != null) {
log_step("Podkop detected! Restoring Podkop service...");
run_cmd(["/etc/init.d/podkop", "enable"]);
run_cmd(["/etc/init.d/podkop", "restart"]);
}
log_step("Removing Tachyon packages...");
if (fs.stat("/bin/opkg") != null || fs.stat("/usr/bin/opkg") != null) {
run_cmd(["opkg", "remove", "--force-depends", "--force-remove", "luci-i18n-tachyon-ru", "luci-app-tachyon", "tachyon"]);
} else if (fs.stat("/sbin/apk") != null) {
run_cmd(["apk", "del", "luci-i18n-tachyon-ru", "luci-app-tachyon", "tachyon"]);
}
log_step("Tachyon has been successfully uninstalled.");
return 0;
}
let mode = as_string(ARGV[0]);
let purge = mode == "--purge" || as_string(ARGV[1]) == "--purge";
exit(uninstall_tachyon(purge));
+1 -1
View File
@@ -47,7 +47,7 @@ expected_shell_scripts="$(
'tachyon/files/etc/hotplug.d/iface/99-tachyon-wan-monitor' \ 'tachyon/files/etc/hotplug.d/iface/99-tachyon-wan-monitor' \
'tachyon/files/etc/init.d/tachyon' \ 'tachyon/files/etc/init.d/tachyon' \
'tachyon/files/usr/lib/cgi-bin/tachyon-agent' \ 'tachyon/files/usr/lib/cgi-bin/tachyon-agent' \
'tachyon/files/usr/lib/tachyon/firewall.include' | 'tachyon/files/usr/lib/firewall.include' |
LC_ALL=C sort LC_ALL=C sort
)" )"
+9 -9
View File
@@ -964,8 +964,8 @@ cat >"$WORK_DIR/subscriptions/grouped-subscription-1.json" <<'JSON'
"tag": "provider-direct" "tag": "provider-direct"
}, },
{ {
"type": "http", "type": "unsupported",
"tag": "provider-http", "tag": "provider-unsupported",
"server": "127.0.0.3", "server": "127.0.0.3",
"server_port": 8080 "server_port": 8080
} }
@@ -1049,12 +1049,7 @@ generate_config "$WORK_DIR/server-inbound-fixture.json" "$WORK_DIR/server.json"
generate_config "$WORK_DIR/runtime-matchers-fixture.json" "$WORK_DIR/matchers.json" generate_config "$WORK_DIR/runtime-matchers-fixture.json" "$WORK_DIR/matchers.json"
generate_config "$WORK_DIR/dns-action-fixture.json" "$WORK_DIR/dns-action.json" generate_config "$WORK_DIR/dns-action-fixture.json" "$WORK_DIR/dns-action.json"
generate_config "$WORK_DIR/urltest-filter-fixture.json" "$WORK_DIR/urltest.json" generate_config "$WORK_DIR/urltest-filter-fixture.json" "$WORK_DIR/urltest.json"
if generate_config "$WORK_DIR/manual-http-fixture.json" "$WORK_DIR/manual-http.json" \ generate_config "$WORK_DIR/manual-http-fixture.json" "$WORK_DIR/manual-http.json"
>"$WORK_DIR/manual-http.stdout" 2>"$WORK_DIR/manual-http.stderr"; then
fail "generator should reject native HTTP connection URLs"
fi
grep -Fxq 'manual proxy link scheme is not supported by sing-box config generation yet' "$WORK_DIR/manual-http.stderr" ||
fail "native HTTP connection URL failure should identify the unsupported scheme"
generate_config "$WORK_DIR/provider-actions-fixture.json" "$WORK_DIR/providers.json" generate_config "$WORK_DIR/provider-actions-fixture.json" "$WORK_DIR/providers.json"
generate_config "$WORK_DIR/manual-transport-fixture.json" "$WORK_DIR/manual.json" generate_config "$WORK_DIR/manual-transport-fixture.json" "$WORK_DIR/manual.json"
generate_config "$WORK_DIR/vpn-interface-fixture.json" "$WORK_DIR/vpn.json" generate_config "$WORK_DIR/vpn-interface-fixture.json" "$WORK_DIR/vpn.json"
@@ -1285,6 +1280,11 @@ assert(urltest_out && length(urltest_out.outbounds) == 1 && urltest_out.outbound
assert(outbound(urltest, "Keep").type == "http", "HTTP JSON outbound remains supported"); assert(outbound(urltest, "Keep").type == "http", "HTTP JSON outbound remains supported");
assert(outbound(urltest, "proxy-out").default == "proxy-urltest-out", "selector defaults to URLTest"); assert(outbound(urltest, "proxy-out").default == "proxy-urltest-out", "selector defaults to URLTest");
let manual_http = cfg("manual-http");
let manual_http_out = outbound(manual_http, "proxy-1-out");
assert(manual_http_out && manual_http_out.type == "http" && manual_http_out.server == "127.0.0.1" && manual_http_out.server_port == 8080, "manual HTTP outbound supported");
assert(route_rule(manual_http, r => r.action == "reject" && contains(r.network, "udp")) != null, "HTTP outbound generates UDP reject rule");
let providers = cfg("providers"); let providers = cfg("providers");
assert(outbound(providers, "zap-out").routing_mark == 0x01000001, "Zapret mark"); assert(outbound(providers, "zap-out").routing_mark == 0x01000001, "Zapret mark");
assert(outbound(providers, "zap2-out").routing_mark == 0x02000001, "Zapret2 mark"); assert(outbound(providers, "zap2-out").routing_mark == 0x02000001, "Zapret2 mark");
@@ -1372,7 +1372,7 @@ let second_leaf = outbound(subscription_group, "leaf");
assert(grouped_leaf && grouped_leaf.detour == null, "hidden subscription leaf does not receive connection URL detour"); assert(grouped_leaf && grouped_leaf.detour == null, "hidden subscription leaf does not receive connection URL detour");
assert(second_leaf && second_leaf.detour == null, "second hidden subscription leaf does not receive connection URL detour"); assert(second_leaf && second_leaf.detour == null, "second hidden subscription leaf does not receive connection URL detour");
assert(outbound(subscription_group, "provider-direct") == null, "provider direct outbound skipped"); assert(outbound(subscription_group, "provider-direct") == null, "provider direct outbound skipped");
assert(outbound(subscription_group, "provider-http") == null, "provider HTTP outbound skipped"); assert(outbound(subscription_group, "provider-unsupported") == null, "provider unsupported outbound skipped");
let grouped_selector = outbound(subscription_group, "grouped-out"); let grouped_selector = outbound(subscription_group, "grouped-out");
assert(grouped_selector && length(grouped_selector.outbounds) == 1 && grouped_selector.outbounds[0] == "Provider Group", "selector exposes provider group only"); assert(grouped_selector && length(grouped_selector.outbounds) == 1 && grouped_selector.outbounds[0] == "Provider Group", "selector exposes provider group only");
let grouped_state = cfg("subscription-group.json.section-cache/grouped"); let grouped_state = cfg("subscription-group.json.section-cache/grouped");
+8 -2
View File
@@ -133,12 +133,18 @@ proxies:
type: http type: http
server: proxy.example server: proxy.example
port: 8080 port: 8080
- name: clash-unsupported
type: snell
server: proxy.example
port: 9090
YAML YAML
ucode "$PARSER" normalize-clash-yaml "$clash_input" "$clash_output" ucode "$PARSER" normalize-clash-yaml "$clash_input" "$clash_output"
assert_contains "$clash_output" '"alpn": [ "http/1.1" ]' "clash-vless-ws" assert_contains "$clash_output" '"alpn": [ "http/1.1" ]' "clash-vless-ws"
assert_contains "$clash_output" '"encryption": "mlkem768x25519plus.native.test"' "clash-vless-ws" assert_contains "$clash_output" '"encryption": "mlkem768x25519plus.native.test"' "clash-vless-ws"
assert_not_contains "$clash_output" '"tag": "clash-http"' "clash-http" assert_contains "$clash_output" '"tag": "clash-http"' "clash-http"
assert_contains "$clash_output" '"skipped": 1' "clash-http" assert_contains "$clash_output" '"type": "http"' "clash-http"
assert_not_contains "$clash_output" '"tag": "clash-unsupported"' "clash-unsupported"
assert_contains "$clash_output" '"skipped": 1' "clash-unsupported"
xray_input="$WORK_DIR/xray.json" xray_input="$WORK_DIR/xray.json"
xray_output="$WORK_DIR/xray-normalized.json" xray_output="$WORK_DIR/xray-normalized.json"
+108 -5
View File
@@ -1,7 +1,7 @@
#!/bin/sh #!/bin/sh
set -eu set -eu
echo "=== Testing uninstall.sh ===" echo "=== Testing uninstall.sh and uninstallation mechanisms ==="
TEST_DIR="$(mktemp -d /tmp/tachyon_uninstall_test_XXXXXX)" TEST_DIR="$(mktemp -d /tmp/tachyon_uninstall_test_XXXXXX)"
cleanup() { cleanup() {
@@ -15,15 +15,110 @@ if ! echo "$HELP_OUTPUT" | grep -q -- "--purge"; then
echo "FAIL: --help did not output --purge option" echo "FAIL: --help did not output --purge option"
exit 1 exit 1
fi fi
echo "✓ Help option works" if ! echo "$HELP_OUTPUT" | grep -q -- "--keep-binaries"; then
echo "FAIL: --help did not output --keep-binaries option"
exit 1
fi
echo "✓ Help options verified (--purge, --keep-binaries)"
# 2. Mock environment for uninstall test # 2. Verify files and uninstaller dispatch paths
if [ ! -f "tachyon/files/usr/lib/service/uninstall.uc" ]; then
echo "FAIL: tachyon/files/usr/lib/service/uninstall.uc is missing (double-nesting regression)"
exit 1
fi
if [ -d "tachyon/files/usr/lib/tachyon" ]; then
echo "FAIL: tachyon/files/usr/lib/tachyon directory should not exist"
exit 1
fi
if ! grep -Fq 'uninstall: [ "service/uninstall.uc", "uninstall", 1 ]' "tachyon/files/usr/bin/tachyon"; then
echo "FAIL: usr/bin/tachyon does not dispatch to service/uninstall.uc"
exit 1
fi
echo "✓ Uninstaller module paths and entrypoints verified"
# 3. Test routing table cleanup logic
MOCK_RT="$TEST_DIR/rt_tables"
cat << 'EOF' > "$MOCK_RT"
# Reserved tables
100 main
105 tachyon
200 custom_table
EOF
sed -i '/105[[:space:]]\+tachyon/d; /tachyon/d' "$MOCK_RT"
if grep -q "tachyon" "$MOCK_RT"; then
echo "FAIL: tachyon was not removed from rt_tables"
exit 1
fi
if ! grep -q "100 main" "$MOCK_RT" || ! grep -q "200 custom_table" "$MOCK_RT"; then
echo "FAIL: unrelated rt_tables entries were corrupted"
exit 1
fi
echo "✓ Routing table cleanup logic verified"
# 4. Test crontab cleanup logic
MOCK_CRON="$TEST_DIR/crontab.txt"
cat << 'EOF' > "$MOCK_CRON"
0 2 * * * /usr/bin/backup_script.sh
*/5 * * * * /usr/bin/tachyon watchdog
* * * * * /usr/bin/tachyon parental_quota_tick
30 4 * * 0 /usr/bin/certbot renew
EOF
MOCK_CRON_CLEAN="$TEST_DIR/crontab_clean.txt"
grep -v -E 'tachyon|parental_quota_tick' "$MOCK_CRON" > "$MOCK_CRON_CLEAN" || true
if grep -q "tachyon" "$MOCK_CRON_CLEAN" || grep -q "parental_quota_tick" "$MOCK_CRON_CLEAN"; then
echo "FAIL: tachyon jobs remained in crontab"
exit 1
fi
if ! grep -q "backup_script.sh" "$MOCK_CRON_CLEAN" || ! grep -q "certbot renew" "$MOCK_CRON_CLEAN"; then
echo "FAIL: non-tachyon cron jobs were deleted"
exit 1
fi
echo "✓ Crontab cleanup logic verified"
# 5. Test managed sing-box detection logic
MOCK_SB_INIT_MANAGED="$TEST_DIR/sing-box-managed"
MOCK_SB_INIT_UNMANAGED="$TEST_DIR/sing-box-unmanaged"
cat << 'EOF' > "$MOCK_SB_INIT_MANAGED"
#!/bin/sh /etc/rc.common
# Tachyon managed sing-box service for binary variants
START=95
EOF
cat << 'EOF' > "$MOCK_SB_INIT_UNMANAGED"
#!/bin/sh /etc/rc.common
# Official openwrt-sing-box package
START=95
EOF
if ! grep -q "Tachyon managed sing-box" "$MOCK_SB_INIT_MANAGED"; then
echo "FAIL: Managed marker not found in managed sing-box init"
exit 1
fi
if grep -q "Tachyon managed sing-box" "$MOCK_SB_INIT_UNMANAGED"; then
echo "FAIL: Managed marker falsely found in unmanaged sing-box init"
exit 1
fi
echo "✓ Managed sing-box detection verified"
# 6. Verify postrm hooks presence
if ! grep -q "Package/tachyon/postrm" "tachyon/Makefile"; then
echo "FAIL: Package/tachyon/postrm missing in tachyon/Makefile"
exit 1
fi
if ! grep -q "Package/\$(PKG_NAME)/postrm" "luci-app-tachyon/Makefile"; then
echo "FAIL: Package/\$(PKG_NAME)/postrm missing in luci-app-tachyon/Makefile"
exit 1
fi
if ! grep -q "backend-post-deinstall.sh" "build.sh"; then
echo "FAIL: backend-post-deinstall.sh missing in build.sh"
exit 1
fi
echo "✓ Package postrm and post-deinstall hooks verified"
# 7. Mock environment for backup and purge test
MOCK_ETC="$TEST_DIR/etc" MOCK_ETC="$TEST_DIR/etc"
mkdir -p "$MOCK_ETC/config" "$MOCK_ETC/init.d" mkdir -p "$MOCK_ETC/config" "$MOCK_ETC/init.d"
echo "config tachyon 'settings'" > "$MOCK_ETC/config/tachyon" echo "config tachyon 'settings'" > "$MOCK_ETC/config/tachyon"
echo "option enabled '1'" >> "$MOCK_ETC/config/tachyon" echo "option enabled '1'" >> "$MOCK_ETC/config/tachyon"
# Verify backup creation in mock run
TIMESTAMP="$(date +%Y%m%d_%H%M%S 2>/dev/null || date +%s)" TIMESTAMP="$(date +%Y%m%d_%H%M%S 2>/dev/null || date +%s)"
BACKUP_FILE="$MOCK_ETC/config/tachyon.backup-$TIMESTAMP" BACKUP_FILE="$MOCK_ETC/config/tachyon.backup-$TIMESTAMP"
cp -af "$MOCK_ETC/config/tachyon" "$BACKUP_FILE" cp -af "$MOCK_ETC/config/tachyon" "$BACKUP_FILE"
@@ -35,4 +130,12 @@ if [ ! -f "$BACKUP_FILE" ] || [ ! -f "$MOCK_ETC/config/tachyon.bak" ]; then
fi fi
echo "✓ Backup logic verified" echo "✓ Backup logic verified"
echo "PASS: uninstall.sh tests completed successfully" # Test purge
rm -rf "$MOCK_ETC/config/tachyon"*
if [ -f "$MOCK_ETC/config/tachyon" ] || [ -f "$MOCK_ETC/config/tachyon.bak" ]; then
echo "FAIL: Purge did not remove config files"
exit 1
fi
echo "✓ Purge logic verified"
echo "PASS: all uninstall tests completed successfully"
+1 -1
View File
@@ -5,7 +5,7 @@ const path = require('path');
const rootDir = path.resolve(__dirname, '..'); const rootDir = path.resolve(__dirname, '..');
const docsDir = process.argv[2] ? path.resolve(process.argv[2]) : path.join(rootDir, 'docs', 'knowledge-base'); const docsDir = process.argv[2] ? path.resolve(process.argv[2]) : path.join(rootDir, 'docs', 'knowledge-base');
const outputFile = process.argv[3] ? path.resolve(process.argv[3]) : path.join(rootDir, 'tachyon', 'files', 'usr', 'lib', 'tachyon', 'rag_index.json'); const outputFile = process.argv[3] ? path.resolve(process.argv[3]) : path.join(rootDir, 'tachyon', 'files', 'usr', 'lib', 'rag_index.json');
const chunkSize = parseInt(process.env.CHUNK_SIZE || '2000', 10); const chunkSize = parseInt(process.env.CHUNK_SIZE || '2000', 10);
const chunkOverlap = parseInt(process.env.CHUNK_OVERLAP || '200', 10); const chunkOverlap = parseInt(process.env.CHUNK_OVERLAP || '200', 10);
+1 -1
View File
@@ -6,7 +6,7 @@ SCRIPT_DIR="$(cd "$(dirname "$SCRIPT_PATH")" && pwd)"
ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
DOCS_DIR="${1:-$ROOT_DIR/docs/knowledge-base}" DOCS_DIR="${1:-$ROOT_DIR/docs/knowledge-base}"
OUTPUT_DIR="${2:-$ROOT_DIR/tachyon/files/usr/lib/tachyon}" OUTPUT_DIR="${2:-$ROOT_DIR/tachyon/files/usr/lib}"
OUTPUT_FILE="$OUTPUT_DIR/rag_index.json" OUTPUT_FILE="$OUTPUT_DIR/rag_index.json"
CHUNK_SIZE="${CHUNK_SIZE:-2000}" CHUNK_SIZE="${CHUNK_SIZE:-2000}"
+148 -34
View File
@@ -7,7 +7,7 @@
set -u set -u
UNINSTALLER_VERSION="1.2.88" UNINSTALLER_VERSION="1.3.0"
# ─── TUI helpers & Color detection ─────────────────────────────────────────── # ─── TUI helpers & Color detection ───────────────────────────────────────────
ESC="$(printf '\033')" ESC="$(printf '\033')"
@@ -120,7 +120,7 @@ for _arg in "$@"; do
done done
# ─── Root check ────────────────────────────────────────────────────────────── # ─── Root check ──────────────────────────────────────────────────────────────
if [ "$(id -u)" -ne 0 ] 2>/dev/null; then if [ "$(id -u 2>/dev/null || echo 1)" -ne 0 ]; then
printf '%sОшибка: скрипт удаления должен запускаться с правами root!%s\n' "$_c_red" "$_c_reset" >&2 printf '%sОшибка: скрипт удаления должен запускаться с правами root!%s\n' "$_c_red" "$_c_reset" >&2
exit 1 exit 1
fi fi
@@ -158,9 +158,9 @@ if [ "$OPT_PURGE" -eq 0 ]; then
TIMESTAMP="$(date +%Y%m%d_%H%M%S 2>/dev/null || date +%s)" TIMESTAMP="$(date +%Y%m%d_%H%M%S 2>/dev/null || date +%s)"
if [ -f "/etc/config/tachyon" ]; then if [ -f "/etc/config/tachyon" ]; then
BACKUP_PATH="/etc/config/tachyon.backup-${TIMESTAMP}" BACKUP_PATH="/etc/config/tachyon.backup-${TIMESTAMP}"
cp -af "/etc/config/tachyon" "$BACKUP_PATH" 2>/dev/null cp -af "/etc/config/tachyon" "$BACKUP_PATH" 2>/dev/null || true
cp -af "/etc/config/tachyon" "/etc/config/tachyon.bak" 2>/dev/null cp -af "/etc/config/tachyon" "/etc/config/tachyon.bak" 2>/dev/null || true
chmod 600 "$BACKUP_PATH" "/etc/config/tachyon.bak" 2>/dev/null chmod 600 "$BACKUP_PATH" "/etc/config/tachyon.bak" 2>/dev/null || true
tui_ok "Конфигурация успешно сохранена в: ${BACKUP_PATH}" tui_ok "Конфигурация успешно сохранена в: ${BACKUP_PATH}"
else else
tui_info "Конфигурационный файл /etc/config/tachyon не найден, бэкап пропущен." tui_info "Конфигурационный файл /etc/config/tachyon не найден, бэкап пропущен."
@@ -174,42 +174,92 @@ CURRENT_STEP=$((CURRENT_STEP + 1))
# ─── STEP 2: Stop Services & Daemons ───────────────────────────────────────── # ─── STEP 2: Stop Services & Daemons ─────────────────────────────────────────
tui_step "$CURRENT_STEP" "$TOTAL_STEPS" "Остановка служб и фоновых процессов..." tui_step "$CURRENT_STEP" "$TOTAL_STEPS" "Остановка служб и фоновых процессов..."
# Remove stale locks so stop doesn't block on orphaned states
rm -f /var/run/tachyon/starting /var/run/tachyon/reloading /var/run/tachyon*.lock 2>/dev/null || true
if [ -f "/etc/init.d/tachyon" ]; then if [ -f "/etc/init.d/tachyon" ]; then
/etc/init.d/tachyon stop >/dev/null 2>&1 || true if command -v timeout >/dev/null 2>&1; then
timeout 8 /etc/init.d/tachyon stop >/dev/null 2>&1 || true
else
/etc/init.d/tachyon stop >/dev/null 2>&1 || true
fi
/etc/init.d/tachyon disable >/dev/null 2>&1 || true /etc/init.d/tachyon disable >/dev/null 2>&1 || true
tui_ok "Служба /etc/init.d/tachyon остановлена и отключена" tui_ok "Служба /etc/init.d/tachyon остановлена и отключена"
fi fi
# Terminate running DPI and proxy processes if any # Stop and remove managed sing-box service if created by Tachyon
killall sing-box >/dev/null 2>&1 || true if [ -f "/etc/init.d/sing-box" ]; then
killall nfqws >/dev/null 2>&1 || true if grep -q "Tachyon managed sing-box" "/etc/init.d/sing-box" 2>/dev/null; then
killall nfqws2 >/dev/null 2>&1 || true if command -v timeout >/dev/null 2>&1; then
killall ciadpi >/dev/null 2>&1 || true timeout 8 /etc/init.d/sing-box stop >/dev/null 2>&1 || true
else
/etc/init.d/sing-box stop >/dev/null 2>&1 || true
fi
/etc/init.d/sing-box disable >/dev/null 2>&1 || true
rm -f /etc/init.d/sing-box /etc/rc.d/*sing-box* 2>/dev/null || true
tui_ok "Управляемый сервис sing-box остановлен и удален"
fi
fi
tui_ok "Фоновые процессы sing-box, zapret и byedpi завершены" # Terminate running DPI and proxy processes if any
killall -9 sing-box >/dev/null 2>&1 || true
killall -9 nfqws >/dev/null 2>&1 || true
killall -9 nfqws2 >/dev/null 2>&1 || true
killall -9 ciadpi >/dev/null 2>&1 || true
killall -9 tachyon >/dev/null 2>&1 || true
# Terminate running background Tachyon daemons (watchdog, telegram, failover, etc.)
_self_pid="$$"
ps 2>/dev/null | grep -E 'dns_failover|watchdog|telegram' | grep -v grep | awk '{print $1}' | while read -r _pid; do
if [ -n "$_pid" ] && [ "$_pid" != "$_self_pid" ]; then
kill -9 "$_pid" 2>/dev/null || true
fi
done
tui_ok "Фоновые процессы sing-box, zapret, byedpi и сервисы Tachyon завершены"
CURRENT_STEP=$((CURRENT_STEP + 1)) CURRENT_STEP=$((CURRENT_STEP + 1))
# ─── STEP 3: Clean up Network & nftables ───────────────────────────────────── # ─── STEP 3: Clean up Network, nftables & Policy Routing ─────────────────────
tui_step "$CURRENT_STEP" "$TOTAL_STEPS" "Очистка сетевых таблиц nftables и политик маршрутизации..." tui_step "$CURRENT_STEP" "$TOTAL_STEPS" "Очистка сетевых таблиц nftables и политик маршрутизации..."
# Remove nftables tables # Remove nftables tables & drop-in file
if command -v nft >/dev/null 2>&1; then if command -v nft >/dev/null 2>&1; then
nft delete table inet TachyonTable >/dev/null 2>&1 || true nft delete table inet TachyonTable >/dev/null 2>&1 || true
nft delete table inet tachyon >/dev/null 2>&1 || true nft delete table inet tachyon >/dev/null 2>&1 || true
nft delete table ip tachyon >/dev/null 2>&1 || true nft delete table ip tachyon >/dev/null 2>&1 || true
nft delete table ip6 tachyon >/dev/null 2>&1 || true nft delete table ip6 tachyon >/dev/null 2>&1 || true
rm -f /usr/share/nftables.d/chain-pre/input/10-tachyon.nft 2>/dev/null || true rm -f /usr/share/nftables.d/chain-pre/input/10-tachyon.nft 2>/dev/null || true
tui_ok "Таблицы nftables (TachyonTable) успешно удалены" # Restart firewall to purge in-memory dynamic rules from inet fw4
if [ -x "/etc/init.d/firewall" ]; then
/etc/init.d/firewall restart >/dev/null 2>&1 || true
fi
tui_ok "Таблицы nftables (TachyonTable) удалены, фаервол сброшен"
fi fi
# Remove IP policy routing rules & flush table 100 # Remove IP policy routing rules & flush routing tables
if command -v ip >/dev/null 2>&1; then if command -v ip >/dev/null 2>&1; then
ip rule del fwmark 0x10000000/0x10000000 lookup 100 >/dev/null 2>&1 || true # IPv4 and IPv6 rules used by Tachyon (0x04000000 / table tachyon / priority 105)
ip rule del fwmark 0x1/0x1 lookup 100 >/dev/null 2>&1 || true ip -4 rule del fwmark 0x04000000/0x04000000 table tachyon priority 105 >/dev/null 2>&1 || true
ip rule del fwmark 0x2/0x2 lookup 100 >/dev/null 2>&1 || true ip -6 rule del fwmark 0x04000000/0x04000000 table tachyon priority 105 >/dev/null 2>&1 || true
ip -4 rule del fwmark 0x10000000/0x10000000 lookup 100 >/dev/null 2>&1 || true
ip -4 rule del fwmark 0x1/0x1 lookup 100 >/dev/null 2>&1 || true
ip -4 rule del fwmark 0x2/0x2 lookup 100 >/dev/null 2>&1 || true
ip route flush table tachyon >/dev/null 2>&1 || true
ip route flush table 105 >/dev/null 2>&1 || true
ip route flush table 100 >/dev/null 2>&1 || true ip route flush table 100 >/dev/null 2>&1 || true
tui_ok "Политики маршрутизации (table 100, fwmark) сброшены"
# Clean /etc/iproute2/rt_tables entry
if [ -f "/etc/iproute2/rt_tables" ]; then
sed -i '/105[[:space:]]\+tachyon/d' /etc/iproute2/rt_tables 2>/dev/null || true
fi
# Clean network namespaces and diagnostic veth pairs
ip netns del fkpsc >/dev/null 2>&1 || true
ip link del fkpsc0 >/dev/null 2>&1 || true
rm -rf /etc/netns/fkpsc 2>/dev/null || true
tui_ok "Политики маршрутизации (table tachyon/105/100, fwmark) полностью сброшены"
fi fi
CURRENT_STEP=$((CURRENT_STEP + 1)) CURRENT_STEP=$((CURRENT_STEP + 1))
@@ -221,11 +271,42 @@ rm -f /etc/dnsmasq.d/tachyon*.conf 2>/dev/null || true
rm -f /tmp/dnsmasq.d/tachyon*.conf 2>/dev/null || true rm -f /tmp/dnsmasq.d/tachyon*.conf 2>/dev/null || true
rm -rf /tmp/tachyon 2>/dev/null || true rm -rf /tmp/tachyon 2>/dev/null || true
# Restore dnsmasq backup if exists # Restore /etc/config/dhcp via UCI
if [ -f "/etc/config/dnsmasq.tachyon-bak" ]; then if command -v uci >/dev/null 2>&1 && [ -f "/etc/config/dhcp" ]; then
cp -f "/etc/config/dnsmasq.tachyon-bak" "/etc/config/dnsmasq" 2>/dev/null # Remove sing-box DNS 127.0.0.42
rm -f "/etc/config/dnsmasq.tachyon-bak" 2>/dev/null uci -q del_list dhcp.@dnsmasq[0].server="127.0.0.42" 2>/dev/null || true
tui_ok "Восстановлен исходный /etc/config/dnsmasq из бэкапа"
# Restore original servers from tachyon_server backup
_orig_servers="$(uci -q get dhcp.@dnsmasq[0].tachyon_server 2>/dev/null || true)"
if [ -n "$_orig_servers" ]; then
uci -q delete dhcp.@dnsmasq[0].server 2>/dev/null || true
for _srv in $_orig_servers; do
[ "$_srv" != "127.0.0.42" ] && uci -q add_list dhcp.@dnsmasq[0].server="$_srv" 2>/dev/null || true
done
uci -q delete dhcp.@dnsmasq[0].tachyon_server 2>/dev/null || true
fi
# Restore backed-up options
for _opt in noresolv cachesize rebind_protection localuse addn_hosts notinterface; do
_val="$(uci -q get "dhcp.@dnsmasq[0].tachyon_${_opt}" 2>/dev/null || true)"
if [ -n "$_val" ]; then
uci -q set "dhcp.@dnsmasq[0].${_opt}=${_val}" 2>/dev/null || true
uci -q delete "dhcp.@dnsmasq[0].tachyon_${_opt}" 2>/dev/null || true
fi
done
# Failsafe: if noresolv remains 1, reset to 0 so router queries ISP/upstream resolvers
if [ "$(uci -q get dhcp.@dnsmasq[0].noresolv 2>/dev/null)" = "1" ]; then
uci -q set dhcp.@dnsmasq[0].noresolv="0" 2>/dev/null || true
fi
# Failsafe: if cachesize remains 0, restore standard default 150
if [ "$(uci -q get dhcp.@dnsmasq[0].cachesize 2>/dev/null)" = "0" ]; then
uci -q set dhcp.@dnsmasq[0].cachesize="150" 2>/dev/null || true
fi
uci -q delete dhcp.tachyon 2>/dev/null || true
uci -q commit dhcp 2>/dev/null || true
tui_ok "Параметры DHCP и DNS dnsmasq возвращены в исходное состояние"
fi fi
# Restart dnsmasq to apply clean DNS configuration # Restart dnsmasq to apply clean DNS configuration
@@ -236,8 +317,17 @@ fi
CURRENT_STEP=$((CURRENT_STEP + 1)) CURRENT_STEP=$((CURRENT_STEP + 1))
# ─── STEP 5: Remove Packages ───────────────────────────────────────────────── # ─── STEP 5: Clean Crontabs & Remove Packages ────────────────────────────────
tui_step "$CURRENT_STEP" "$TOTAL_STEPS" "Удаление установленных пакетов Tachyon..." tui_step "$CURRENT_STEP" "$TOTAL_STEPS" "Очистка crontab и удаление установленных пакетов..."
# Clean root crontab from Tachyon jobs
if command -v crontab >/dev/null 2>&1; then
_crontmp="$(mktemp /tmp/cron.XXXXXX 2>/dev/null || echo '/tmp/cron.tachyon.tmp')"
crontab -l 2>/dev/null | grep -v -E 'tachyon|parental_quota_tick' > "$_crontmp" || true
crontab "$_crontmp" 2>/dev/null || true
rm -f "$_crontmp" 2>/dev/null || true
tui_ok "Задачи планировщика crontab очищены"
fi
if command -v apk >/dev/null 2>&1 && [ -d "/lib/apk/db" ]; then if command -v apk >/dev/null 2>&1 && [ -d "/lib/apk/db" ]; then
for _pkg in luci-i18n-tachyon-ru luci-app-tachyon tachyon; do for _pkg in luci-i18n-tachyon-ru luci-app-tachyon tachyon; do
@@ -258,13 +348,26 @@ elif command -v opkg >/dev/null 2>&1; then
opkg remove --force-depends --force-remove "$_pkg" >/dev/null 2>&1 || true opkg remove --force-depends --force-remove "$_pkg" >/dev/null 2>&1 || true
fi fi
done done
if [ "$OPT_KEEP_BINARIES" -eq 0 ]; then
for _pkg in sing-box-extended sing-box-tiny; do
if opkg list-installed "$_pkg" 2>/dev/null | grep -q "^$_pkg "; then
opkg remove --force-depends --force-remove "$_pkg" >/dev/null 2>&1 || true
fi
done
if [ ! -f /etc/init.d/sing-box ] || grep -q "Tachyon managed sing-box" /etc/init.d/sing-box 2>/dev/null; then
if opkg list-installed "sing-box" 2>/dev/null | grep -q "^sing-box "; then
opkg remove --force-depends --force-remove "sing-box" >/dev/null 2>&1 || true
fi
rm -f /usr/bin/sing-box 2>/dev/null || true
fi
fi
tui_ok "Пакеты удалены через opkg" tui_ok "Пакеты удалены через opkg"
fi fi
CURRENT_STEP=$((CURRENT_STEP + 1)) CURRENT_STEP=$((CURRENT_STEP + 1))
# ─── STEP 6: Remove Leftover Files & LuCI Cache ────────────────────────────── # ─── STEP 6: Remove Leftover Files & LuCI Cache ──────────────────────────────
tui_step "$CURRENT_STEP" "$TOTAL_STEPS" "Очистка оставшихся файлов и кэша LuCI..." tui_step "$CURRENT_STEP" "$TOTAL_STEPS" "Очистка оставшихся файлов, хуков и кэша LuCI..."
rm -rf /usr/lib/tachyon 2>/dev/null || true rm -rf /usr/lib/tachyon 2>/dev/null || true
rm -rf /usr/share/tachyon 2>/dev/null || true rm -rf /usr/share/tachyon 2>/dev/null || true
@@ -275,10 +378,18 @@ rm -f /etc/uci-defaults/50_luci-tachyon 2>/dev/null || true
rm -f /etc/tachyon_commit 2>/dev/null || true rm -f /etc/tachyon_commit 2>/dev/null || true
rm -f /usr/bin/tachyon 2>/dev/null || true rm -f /usr/bin/tachyon 2>/dev/null || true
rm -f /etc/init.d/tachyon 2>/dev/null || true rm -f /etc/init.d/tachyon 2>/dev/null || true
rm -f /etc/hotplug.d/iface/99-tachyon-wan-monitor 2>/dev/null || true
rm -f /www/cgi-bin/tachyon-agent 2>/dev/null || true
rm -f /usr/lib/cgi-bin/tachyon-agent 2>/dev/null || true
# Optional binary removal # Optional binary removal
if [ "$OPT_KEEP_BINARIES" -eq 0 ]; then if [ "$OPT_KEEP_BINARIES" -eq 0 ]; then
# Only remove binaries if they were placed for tachyon # Remove sing-box binary if it was managed by Tachyon
if [ ! -f "/lib/apk/db/installed" ] && command -v opkg >/dev/null 2>&1; then
if ! opkg list-installed "sing-box*" 2>/dev/null | grep -q "^sing-box"; then
rm -f /usr/bin/sing-box 2>/dev/null || true
fi
fi
rm -f /usr/lib/libcronet.so 2>/dev/null || true rm -f /usr/lib/libcronet.so 2>/dev/null || true
fi fi
@@ -286,17 +397,20 @@ fi
rm -f /usr/lib/lua/luci/i18n/tachyon.* 2>/dev/null || true rm -f /usr/lib/lua/luci/i18n/tachyon.* 2>/dev/null || true
find /usr/lib/lua/luci/i18n/ -name "tachyon.*" -delete 2>/dev/null || true find /usr/lib/lua/luci/i18n/ -name "tachyon.*" -delete 2>/dev/null || true
# Clear runtime and temporary state
rm -rf /var/run/tachyon* /var/log/tachyon* /tmp/sing-box /tmp/tachyon* /tmp/ai_doctor* /tmp/tg_* /tmp/warp_* 2>/dev/null || true
# Clear LuCI index and module caches # Clear LuCI index and module caches
rm -f /var/luci-indexcache* /tmp/luci-indexcache* /tmp/luci-modulecache/* 2>/dev/null || true rm -f /var/luci-indexcache* /tmp/luci-indexcache* /tmp/luci-modulecache/* 2>/dev/null || true
# Purge configs if requested # Purge configs and persistent state if requested
if [ "$OPT_PURGE" -eq 1 ]; then if [ "$OPT_PURGE" -eq 1 ]; then
rm -f /etc/config/tachyon* 2>/dev/null || true rm -f /etc/config/tachyon* 2>/dev/null || true
rm -rf /etc/tachyon 2>/dev/null || true rm -rf /etc/tachyon /etc/.tachyon /etc/backup/tachyon_config /etc/sing-box 2>/dev/null || true
tui_ok "Все конфигурации Tachyon удалены (--purge)" tui_ok "Все конфигурации и скрытые состояния Tachyon удалены (--purge)"
fi fi
# Restart rpcd and uhttpd to update LuCI menu # Restart rpcd and uhttpd to immediately update LuCI menu
if [ -f "/etc/init.d/rpcd" ]; then if [ -f "/etc/init.d/rpcd" ]; then
/etc/init.d/rpcd restart >/dev/null 2>&1 || true /etc/init.d/rpcd restart >/dev/null 2>&1 || true
fi fi
@@ -315,7 +429,7 @@ elif [ -f "/etc/init.d/podkop" ]; then
tui_ok "Обнаружен родительский сервис Podkop — восстановлен и запущен" tui_ok "Обнаружен родительский сервис Podkop — восстановлен и запущен"
fi fi
tui_ok "Кэш LuCI очищен, файлы удалены" tui_ok "Кэш LuCI очищен, файлы и остаточные фрагменты полностью удалены"
# ─── Final Summary ─────────────────────────────────────────────────────────── # ─── Final Summary ───────────────────────────────────────────────────────────
printf '\n' printf '\n'