diff --git a/build.sh b/build.sh index b59753d4..166ecc25 100755 --- a/build.sh +++ b/build.sh @@ -435,7 +435,18 @@ if (getenv("IPKG_INSTROOT") == null || getenv("IPKG_INSTROOT") == "") { exit(0); 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() { @@ -480,7 +491,16 @@ EOF default_prerm $0 $@ 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() { @@ -518,7 +538,15 @@ EOF default_prerm $0 $@ 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() { @@ -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(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 chmod 0755 "$scripts_dir"/backend-*.sh @@ -790,6 +829,16 @@ export pkgname="luci-app-tachyon" add_group_and_user default_postinst [ -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 chmod 0755 "$scripts_dir"/app-*.sh @@ -840,6 +889,11 @@ export root="${IPKG_INSTROOT}" export pkgname="luci-i18n-tachyon-ru" add_group_and_user default_postinst +EOF + + cat > "$scripts_dir/i18n-post-deinstall.sh" <<'EOF' +#!/bin/sh +exit 0 EOF chmod 0755 "$scripts_dir"/i18n-*.sh @@ -889,6 +943,7 @@ build_apk_package() { -s "pre-install:${temp_scripts}/${script_prefix}-pre-install.sh" \ -s "post-install:${temp_scripts}/${script_prefix}-post-install.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 "post-upgrade:${temp_scripts}/${script_prefix}-post-upgrade.sh" elif have_passwordless_sudo; then @@ -909,6 +964,7 @@ build_apk_package() { -s "pre-install:${temp_scripts}/${script_prefix}-pre-install.sh" \ -s "post-install:${temp_scripts}/${script_prefix}-post-install.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 "post-upgrade:${temp_scripts}/${script_prefix}-post-upgrade.sh" 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 post-install:'$temp_scripts/${script_prefix}-post-install.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 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 post-install:'$temp_scripts/${script_prefix}-post-install.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 post-upgrade:'$temp_scripts/${script_prefix}-post-upgrade.sh' " 2>"$stderr_file"; then @@ -1100,7 +1158,7 @@ main() { [[ -x "$apk_bin" ]] || { echo "apk host tool not found at $apk_bin" >&2; exit 1; } 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_app_root "$app_root" diff --git a/fe-app-tachyon/package.json b/fe-app-tachyon/package.json index 6d2230f8..6306a4ef 100644 --- a/fe-app-tachyon/package.json +++ b/fe-app-tachyon/package.json @@ -1,6 +1,6 @@ { "name": "fe-app-tachyon", - "version": "1.3.21", + "version": "1.3.22", "license": "MIT", "type": "module", "scripts": { diff --git a/fe-app-tachyon/src/helpers/isCopyableProxyLink.ts b/fe-app-tachyon/src/helpers/isCopyableProxyLink.ts index 90832c01..62db12fb 100644 --- a/fe-app-tachyon/src/helpers/isCopyableProxyLink.ts +++ b/fe-app-tachyon/src/helpers/isCopyableProxyLink.ts @@ -1,5 +1,5 @@ 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) { return COPYABLE_PROXY_URI_RE.test((link || '').trim()); diff --git a/fe-app-tachyon/src/helpers/tests/isCopyableProxyLink.test.js b/fe-app-tachyon/src/helpers/tests/isCopyableProxyLink.test.js index a32b5192..afd3c2e3 100644 --- a/fe-app-tachyon/src/helpers/tests/isCopyableProxyLink.test.js +++ b/fe-app-tachyon/src/helpers/tests/isCopyableProxyLink.test.js @@ -13,6 +13,8 @@ const copyableLinks = [ 'socks4://example.com:1080', 'socks4a://example.com:1080', 'socks5://user:pass@example.com:1080', + 'http://example.com:80', + 'https://user:pass@example.com:443', ]; const nonCopyableLinks = [ @@ -20,12 +22,8 @@ const nonCopyableLinks = [ 'direct', 'block', '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', + 'tcp://127.0.0.1:8080', ]; describe('isCopyableProxyLink', () => { diff --git a/fe-app-tachyon/src/validators/tests/validateHttpProxyUrl.test.js b/fe-app-tachyon/src/validators/tests/validateHttpProxyUrl.test.js new file mode 100644 index 00000000..131e532c --- /dev/null +++ b/fe-app-tachyon/src/validators/tests/validateHttpProxyUrl.test.js @@ -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); + }); + }); +}); diff --git a/fe-app-tachyon/src/validators/tests/validateProxyUrl.test.js b/fe-app-tachyon/src/validators/tests/validateProxyUrl.test.js index 0cd59b95..043d044b 100644 --- a/fe-app-tachyon/src/validators/tests/validateProxyUrl.test.js +++ b/fe-app-tachyon/src/validators/tests/validateProxyUrl.test.js @@ -31,6 +31,8 @@ const validUrls = [ ['socks4', 'socks4://127.0.0.1:1080'], ['socks4a', 'socks4a://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'], ['hy2', 'hy2://password@example.com:443'], ]; @@ -55,14 +57,16 @@ describe('validateProxyUrl', () => { expect(res.valid).toBe(false); expect(res.message).toContain('vmess://'); expect(res.message).toContain('socks4a://'); + expect(res.message).toContain('http://'); + expect(res.message).toContain('https://'); expect(res.message).toContain('hy2://'); }); - it('rejects native HTTP proxy URLs', () => { - const res = validateProxyUrl('https://user:pass@example.com:443'); - - expect(res.valid).toBe(false); - expect(res.message).not.toContain('http://'); + it('accepts native HTTP and HTTPS proxy URLs', () => { + expect(validateProxyUrl('http://127.0.0.1:8080').valid).toBe(true); + expect(validateProxyUrl('https://user:pass@example.com:443').valid).toBe( + true, + ); }); it('rejects VMess configs with an invalid server host', () => { diff --git a/fe-app-tachyon/src/validators/validateHttpProxyUrl.ts b/fe-app-tachyon/src/validators/validateHttpProxyUrl.ts new file mode 100644 index 00000000..d256f396 --- /dev/null +++ b/fe-app-tachyon/src/validators/validateHttpProxyUrl.ts @@ -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') }; +} diff --git a/fe-app-tachyon/src/validators/validateProxyUrl.ts b/fe-app-tachyon/src/validators/validateProxyUrl.ts index 92376ad0..e37af7a6 100644 --- a/fe-app-tachyon/src/validators/validateProxyUrl.ts +++ b/fe-app-tachyon/src/validators/validateProxyUrl.ts @@ -6,6 +6,7 @@ import { validateTrojanUrl } from './validateTrojanUrl'; import { validateSocksUrl } from './validateSocksUrl'; import { validateHysteria2Url } from './validateHysteriaUrl'; import { validateTuicUrl } from './validateTuicUrl'; +import { validateHttpProxyUrl } from './validateHttpProxyUrl'; // TODO refactor current validation and add tests export function validateProxyUrl(url: string): ValidationResult { @@ -31,6 +32,10 @@ export function validateProxyUrl(url: string): ValidationResult { return validateSocksUrl(trimmedUrl); } + if (/^https?:\/\//.test(trimmedUrl)) { + return validateHttpProxyUrl(trimmedUrl); + } + if ( trimmedUrl.startsWith('hysteria2://') || trimmedUrl.startsWith('hy2://') @@ -45,7 +50,7 @@ export function validateProxyUrl(url: string): ValidationResult { return { valid: false, 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://', ), }; } diff --git a/luci-app-tachyon/Makefile b/luci-app-tachyon/Makefile index 2712badb..67e834f5 100644 --- a/luci-app-tachyon/Makefile +++ b/luci-app-tachyon/Makefile @@ -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 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 # call BuildPackage - OpenWrt buildroot signature diff --git a/luci-app-tachyon/htdocs/luci-static/resources/view/tachyon/main.js b/luci-app-tachyon/htdocs/luci-static/resources/view/tachyon/main.js index d916f6b9..4ceed806 100644 --- a/luci-app-tachyon/htdocs/luci-static/resources/view/tachyon/main.js +++ b/luci-app-tachyon/htdocs/luci-static/resources/view/tachyon/main.js @@ -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 function validateProxyUrl(url) { const trimmedUrl = url.trim(); @@ -948,6 +1027,9 @@ function validateProxyUrl(url) { if (/^socks(4|4a|5):\/\//.test(trimmedUrl)) { return validateSocksUrl(trimmedUrl); } + if (/^https?:\/\//.test(trimmedUrl)) { + return validateHttpProxyUrl(trimmedUrl); + } if (trimmedUrl.startsWith("hysteria2://") || trimmedUrl.startsWith("hy2://")) { return validateHysteria2Url(trimmedUrl); } @@ -957,7 +1039,7 @@ function validateProxyUrl(url) { return { valid: false, 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 -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) { return COPYABLE_PROXY_URI_RE.test((link || "").trim()); } diff --git a/tachyon/Makefile b/tachyon/Makefile index f784cf1e..788aeaf6 100644 --- a/tachyon/Makefile +++ b/tachyon/Makefile @@ -145,6 +145,20 @@ if [ -d /www/cgi-bin ]; then fi 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 /etc/config/tachyon endef diff --git a/tachyon/files/usr/lib/tachyon/firewall.include b/tachyon/files/usr/lib/firewall.include similarity index 100% rename from tachyon/files/usr/lib/tachyon/firewall.include rename to tachyon/files/usr/lib/firewall.include diff --git a/tachyon/files/usr/lib/tachyon/rag_index.json b/tachyon/files/usr/lib/rag_index.json similarity index 100% rename from tachyon/files/usr/lib/tachyon/rag_index.json rename to tachyon/files/usr/lib/rag_index.json diff --git a/tachyon/files/usr/lib/service/uninstall.uc b/tachyon/files/usr/lib/service/uninstall.uc new file mode 100644 index 00000000..ec2f5e54 --- /dev/null +++ b/tachyon/files/usr/lib/service/uninstall.uc @@ -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)); diff --git a/tachyon/files/usr/lib/singbox/generator_outbounds.uc b/tachyon/files/usr/lib/singbox/generator_outbounds.uc index 9f548c42..a24caa02 100644 --- a/tachyon/files/usr/lib/singbox/generator_outbounds.uc +++ b/tachyon/files/usr/lib/singbox/generator_outbounds.uc @@ -162,7 +162,7 @@ function supported_subscription_outbound(outbound) { if (t == "direct" || t == "selector" || t == "urltest" || t == "dns" || t == "block") return false; 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) return false; if (is_dummy_outbound_server(outbound.server, outbound.server_port)) @@ -574,6 +574,32 @@ function apply_link_transport(outbound, query) { 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) { let scheme = url_scheme(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)); scheme = url_scheme(link); + if (scheme == "http" || scheme == "https") + return manual_http_outbound(link, tag_name); if (scheme == "socks4" || scheme == "socks4a" || scheme == "socks5") return manual_socks_outbound(link, tag_name); if (scheme == "ss") @@ -1621,6 +1649,7 @@ return { assert_unique_outbound_tags, uci_bin_to_hex, unique_string_array, + manual_http_outbound, manual_socks_outbound, manual_shadowsocks_outbound, manual_vless_outbound, diff --git a/tachyon/files/usr/lib/singbox/generator_routes.uc b/tachyon/files/usr/lib/singbox/generator_routes.uc index 571f7df5..2cd1d232 100644 --- a/tachyon/files/usr/lib/singbox/generator_routes.uc +++ b/tachyon/files/usr/lib/singbox/generator_routes.uc @@ -1158,6 +1158,58 @@ function tproxy_inbound_matcher() { 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) { let source_ip_cidr = list_option(section, "fully_routed_ips"); if (length(source_ip_cidr) == 0) @@ -1174,7 +1226,7 @@ function add_fully_routed_ips_rule(config, section) { if (target.outbound) route_rule.outbound = target.outbound; 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) { @@ -1338,13 +1390,13 @@ function add_combined_route_for_section(config, section) { else if (type(resolve) == "object" && 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) { let ip_rule = create_section_route_rule(); 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); @@ -1370,7 +1422,7 @@ function add_combined_route_for_section(config, section) { if (country_mode == "exclude") 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.protocol != null || fallback_rule.dscp != null; 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"); @@ -1620,12 +1672,12 @@ function add_service_route_rules(config, sections) { }); catchall_target = FAILOVER_GROUP_TAG; } - push(config.route.rules, { + push_section_route_rule(config, { action: "route", inbound: tproxy_inbound_matcher(), outbound: catchall_target, domain: runtime_constants.CHECK_PROXY_IP_DOMAIN - }); + }, catchall_target); } push(config.route.rules, { action: "route-options", @@ -1735,7 +1787,7 @@ function add_server_routes(config, servers, sections) { }; if (target.outbound) rule.outbound = target.outbound; - push(config.route.rules, rule); + push_section_route_rule(config, rule, target.outbound); } else { ctx.runtime_generate_unsupported("unsupported server routing_mode " + routing_mode); diff --git a/tachyon/files/usr/lib/subscription/parser.uc b/tachyon/files/usr/lib/subscription/parser.uc index f5279b96..ffd020e6 100644 --- a/tachyon/files/usr/lib/subscription/parser.uc +++ b/tachyon/files/usr/lib/subscription/parser.uc @@ -1610,6 +1610,16 @@ function parse_clash_record(record) { outbound.password = as_string(record.password); 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; } diff --git a/tachyon/files/usr/lib/tachyon/service/uninstall.uc b/tachyon/files/usr/lib/tachyon/service/uninstall.uc deleted file mode 100644 index 01166979..00000000 --- a/tachyon/files/usr/lib/tachyon/service/uninstall.uc +++ /dev/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)); diff --git a/tests/shell_inventory.sh b/tests/shell_inventory.sh index 50360bfe..4907d25e 100644 --- a/tests/shell_inventory.sh +++ b/tests/shell_inventory.sh @@ -47,7 +47,7 @@ expected_shell_scripts="$( 'tachyon/files/etc/hotplug.d/iface/99-tachyon-wan-monitor' \ 'tachyon/files/etc/init.d/tachyon' \ '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 )" diff --git a/tests/sing_box_runtime.sh b/tests/sing_box_runtime.sh index cbc090b8..af641bc3 100644 --- a/tests/sing_box_runtime.sh +++ b/tests/sing_box_runtime.sh @@ -964,8 +964,8 @@ cat >"$WORK_DIR/subscriptions/grouped-subscription-1.json" <<'JSON' "tag": "provider-direct" }, { - "type": "http", - "tag": "provider-http", + "type": "unsupported", + "tag": "provider-unsupported", "server": "127.0.0.3", "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/dns-action-fixture.json" "$WORK_DIR/dns-action.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" \ - >"$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/manual-http-fixture.json" "$WORK_DIR/manual-http.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/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, "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"); assert(outbound(providers, "zap-out").routing_mark == 0x01000001, "Zapret 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(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-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"); 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"); diff --git a/tests/subscription_alpn.sh b/tests/subscription_alpn.sh index a3910bfa..90144e16 100644 --- a/tests/subscription_alpn.sh +++ b/tests/subscription_alpn.sh @@ -133,12 +133,18 @@ proxies: type: http server: proxy.example port: 8080 + - name: clash-unsupported + type: snell + server: proxy.example + port: 9090 YAML ucode "$PARSER" normalize-clash-yaml "$clash_input" "$clash_output" assert_contains "$clash_output" '"alpn": [ "http/1.1" ]' "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" '"skipped": 1' "clash-http" +assert_contains "$clash_output" '"tag": "clash-http"' "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_output="$WORK_DIR/xray-normalized.json" diff --git a/tests/uninstall_test.sh b/tests/uninstall_test.sh index 1d2c0d6f..eefe51f2 100644 --- a/tests/uninstall_test.sh +++ b/tests/uninstall_test.sh @@ -1,7 +1,7 @@ #!/bin/sh set -eu -echo "=== Testing uninstall.sh ===" +echo "=== Testing uninstall.sh and uninstallation mechanisms ===" TEST_DIR="$(mktemp -d /tmp/tachyon_uninstall_test_XXXXXX)" cleanup() { @@ -15,15 +15,110 @@ if ! echo "$HELP_OUTPUT" | grep -q -- "--purge"; then echo "FAIL: --help did not output --purge option" exit 1 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" mkdir -p "$MOCK_ETC/config" "$MOCK_ETC/init.d" echo "config tachyon 'settings'" > "$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)" BACKUP_FILE="$MOCK_ETC/config/tachyon.backup-$TIMESTAMP" cp -af "$MOCK_ETC/config/tachyon" "$BACKUP_FILE" @@ -35,4 +130,12 @@ if [ ! -f "$BACKUP_FILE" ] || [ ! -f "$MOCK_ETC/config/tachyon.bak" ]; then fi 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" diff --git a/tools/rag_build_index.js b/tools/rag_build_index.js index 4045098c..59fc4955 100644 --- a/tools/rag_build_index.js +++ b/tools/rag_build_index.js @@ -5,7 +5,7 @@ const path = require('path'); const rootDir = path.resolve(__dirname, '..'); 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 chunkOverlap = parseInt(process.env.CHUNK_OVERLAP || '200', 10); diff --git a/tools/rag_build_index.sh b/tools/rag_build_index.sh index b7d1b5a4..2d556bf8 100644 --- a/tools/rag_build_index.sh +++ b/tools/rag_build_index.sh @@ -6,7 +6,7 @@ SCRIPT_DIR="$(cd "$(dirname "$SCRIPT_PATH")" && pwd)" ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" 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" CHUNK_SIZE="${CHUNK_SIZE:-2000}" diff --git a/uninstall.sh b/uninstall.sh index c2258328..77cd138a 100644 --- a/uninstall.sh +++ b/uninstall.sh @@ -7,7 +7,7 @@ set -u -UNINSTALLER_VERSION="1.2.88" +UNINSTALLER_VERSION="1.3.0" # ─── TUI helpers & Color detection ─────────────────────────────────────────── ESC="$(printf '\033')" @@ -120,7 +120,7 @@ for _arg in "$@"; do done # ─── 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 exit 1 fi @@ -158,9 +158,9 @@ if [ "$OPT_PURGE" -eq 0 ]; then TIMESTAMP="$(date +%Y%m%d_%H%M%S 2>/dev/null || date +%s)" if [ -f "/etc/config/tachyon" ]; then BACKUP_PATH="/etc/config/tachyon.backup-${TIMESTAMP}" - cp -af "/etc/config/tachyon" "$BACKUP_PATH" 2>/dev/null - cp -af "/etc/config/tachyon" "/etc/config/tachyon.bak" 2>/dev/null - chmod 600 "$BACKUP_PATH" "/etc/config/tachyon.bak" 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 || true + chmod 600 "$BACKUP_PATH" "/etc/config/tachyon.bak" 2>/dev/null || true tui_ok "Конфигурация успешно сохранена в: ${BACKUP_PATH}" else tui_info "Конфигурационный файл /etc/config/tachyon не найден, бэкап пропущен." @@ -174,42 +174,92 @@ CURRENT_STEP=$((CURRENT_STEP + 1)) # ─── STEP 2: Stop Services & Daemons ───────────────────────────────────────── 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 - /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 tui_ok "Служба /etc/init.d/tachyon остановлена и отключена" fi -# Terminate running DPI and proxy processes if any -killall sing-box >/dev/null 2>&1 || true -killall nfqws >/dev/null 2>&1 || true -killall nfqws2 >/dev/null 2>&1 || true -killall ciadpi >/dev/null 2>&1 || true +# Stop and remove managed sing-box service if created by Tachyon +if [ -f "/etc/init.d/sing-box" ]; then + if grep -q "Tachyon managed sing-box" "/etc/init.d/sing-box" 2>/dev/null; then + if command -v timeout >/dev/null 2>&1; then + 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)) -# ─── STEP 3: Clean up Network & nftables ───────────────────────────────────── +# ─── STEP 3: Clean up Network, nftables & Policy Routing ───────────────────── 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 nft delete table inet TachyonTable >/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 ip6 tachyon >/dev/null 2>&1 || 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 -# 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 - ip rule del fwmark 0x10000000/0x10000000 lookup 100 >/dev/null 2>&1 || true - ip rule del fwmark 0x1/0x1 lookup 100 >/dev/null 2>&1 || true - ip rule del fwmark 0x2/0x2 lookup 100 >/dev/null 2>&1 || true + # IPv4 and IPv6 rules used by Tachyon (0x04000000 / table tachyon / priority 105) + ip -4 rule del fwmark 0x04000000/0x04000000 table tachyon priority 105 >/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 - 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 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 -rf /tmp/tachyon 2>/dev/null || true -# Restore dnsmasq backup if exists -if [ -f "/etc/config/dnsmasq.tachyon-bak" ]; then - cp -f "/etc/config/dnsmasq.tachyon-bak" "/etc/config/dnsmasq" 2>/dev/null - rm -f "/etc/config/dnsmasq.tachyon-bak" 2>/dev/null - tui_ok "Восстановлен исходный /etc/config/dnsmasq из бэкапа" +# Restore /etc/config/dhcp via UCI +if command -v uci >/dev/null 2>&1 && [ -f "/etc/config/dhcp" ]; then + # Remove sing-box DNS 127.0.0.42 + uci -q del_list dhcp.@dnsmasq[0].server="127.0.0.42" 2>/dev/null || true + + # 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 # Restart dnsmasq to apply clean DNS configuration @@ -236,8 +317,17 @@ fi CURRENT_STEP=$((CURRENT_STEP + 1)) -# ─── STEP 5: Remove Packages ───────────────────────────────────────────────── -tui_step "$CURRENT_STEP" "$TOTAL_STEPS" "Удаление установленных пакетов Tachyon..." +# ─── STEP 5: Clean Crontabs & Remove Packages ──────────────────────────────── +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 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 fi 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" fi CURRENT_STEP=$((CURRENT_STEP + 1)) # ─── 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/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 /usr/bin/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 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 fi @@ -286,17 +397,20 @@ fi 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 +# 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 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 rm -f /etc/config/tachyon* 2>/dev/null || true - rm -rf /etc/tachyon 2>/dev/null || true - tui_ok "Все конфигурации Tachyon удалены (--purge)" + rm -rf /etc/tachyon /etc/.tachyon /etc/backup/tachyon_config /etc/sing-box 2>/dev/null || true + tui_ok "Все конфигурации и скрытые состояния Tachyon удалены (--purge)" 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 /etc/init.d/rpcd restart >/dev/null 2>&1 || true fi @@ -315,7 +429,7 @@ elif [ -f "/etc/init.d/podkop" ]; then tui_ok "Обнаружен родительский сервис Podkop — восстановлен и запущен" fi -tui_ok "Кэш LuCI очищен, файлы удалены" +tui_ok "Кэш LuCI очищен, файлы и остаточные фрагменты полностью удалены" # ─── Final Summary ─────────────────────────────────────────────────────────── printf '\n'