feat: поддержка HTTP/HTTPS прокси (#45), комплексная доработка деинсталляции и отката конфигурации (1.3.22)
This commit is contained in:
@@ -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,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://',
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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));
|
|
||||||
@@ -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
|
||||||
)"
|
)"
|
||||||
|
|
||||||
|
|||||||
@@ -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");
|
||||||
|
|||||||
@@ -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
@@ -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"
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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'
|
||||||
|
|||||||
Reference in New Issue
Block a user