Merge: объединить изменения из upstream forkop в tachyon, разрешить конфликты и исправить тесты E2E

This commit is contained in:
Dushnilin
2026-07-15 17:56:18 +03:00
55 changed files with 788 additions and 648 deletions
-2
View File
@@ -13,8 +13,6 @@ sh <(wget -O - https://raw.githubusercontent.com/Dushnilin/tachyon/main/install.
> [!NOTE]
> Конфигурация от **Forkop** (или **Podkop Plus**) полностью совместима и автоматически перенесется и переименуется в `/etc/config/tachyon` во время установки!
### Что нового в этом форке
* **Поддержка подписок**: автоматическое управление узлами и их регулярное обновление.
+7 -78
View File
@@ -1762,7 +1762,7 @@
"call": "Fastest",
"key": "Fastest",
"places": [
"src/tachyon/methods/custom/getDashboardSections.ts:638"
"src/tachyon/methods/custom/getDashboardSections.ts:647"
]
},
{
@@ -2239,76 +2239,6 @@
"src/validators/validateSubnet.ts:10"
]
},
{
"call": "Invalid HTTP proxy URL: invalid host and port",
"key": "Invalid HTTP proxy URL: invalid host and port",
"places": [
"src/validators/validateHttpProxyUrl.ts:57"
]
},
{
"call": "Invalid HTTP proxy URL: invalid port number",
"key": "Invalid HTTP proxy URL: invalid port number",
"places": [
"src/validators/validateHttpProxyUrl.ts:79"
]
},
{
"call": "Invalid HTTP proxy URL: missing host and port",
"key": "Invalid HTTP proxy URL: missing host and port",
"places": [
"src/validators/validateHttpProxyUrl.ts:49"
]
},
{
"call": "Invalid HTTP proxy URL: missing hostname or IP",
"key": "Invalid HTTP proxy URL: missing hostname or IP",
"places": [
"src/validators/validateHttpProxyUrl.ts:65"
]
},
{
"call": "Invalid HTTP proxy URL: missing port",
"key": "Invalid HTTP proxy URL: missing port",
"places": [
"src/validators/validateHttpProxyUrl.ts:72"
]
},
{
"call": "Invalid HTTP proxy URL: missing username",
"key": "Invalid HTTP proxy URL: missing username",
"places": [
"src/validators/validateHttpProxyUrl.ts:41"
]
},
{
"call": "Invalid HTTP proxy URL: must not contain spaces",
"key": "Invalid HTTP proxy URL: must not contain spaces",
"places": [
"src/validators/validateHttpProxyUrl.ts:18"
]
},
{
"call": "Invalid HTTP proxy URL: must start with http:// or https://",
"key": "Invalid HTTP proxy URL: must start with http:// or https://",
"places": [
"src/validators/validateHttpProxyUrl.ts:9"
]
},
{
"call": "Invalid HTTP proxy URL: parsing failed",
"key": "Invalid HTTP proxy URL: parsing failed",
"places": [
"src/validators/validateHttpProxyUrl.ts:85"
]
},
{
"call": "Invalid HTTP proxy URL: path, query, and fragment are not supported",
"key": "Invalid HTTP proxy URL: path, query, and fragment are not supported",
"places": [
"src/validators/validateHttpProxyUrl.ts:26"
]
},
{
"call": "Invalid HY2 URL: insecure must be 0 or 1",
"key": "Invalid HY2 URL: insecure must be 0 or 1",
@@ -5218,7 +5148,6 @@
"call": "unknown",
"key": "unknown",
"places": [
"src/tachyon/helpers/singBoxVariant.ts:28",
"src/tachyon/services/systemInfo.service.ts:10",
"src/tachyon/services/systemInfo.service.ts:11",
"src/tachyon/services/systemInfo.service.ts:12",
@@ -5228,6 +5157,7 @@
"src/tachyon/services/systemInfo.service.ts:22",
"src/tachyon/services/systemInfo.service.ts:25",
"src/tachyon/services/systemInfo.service.ts:26",
"src/tachyon/helpers/singBoxVariant.ts:28",
"src/tachyon/tabs/updates/initController.ts:329",
"src/tachyon/tabs/diagnostic/checks/runInboundsCheck.ts:25"
]
@@ -5337,10 +5267,10 @@
]
},
{
"call": "URL must start with vless://, vmess://, ss://, trojan://, socks4://, socks4a://, socks5://, http://, https://, hysteria2://, or hy2://",
"key": "URL must start with vless://, vmess://, ss://, trojan://, socks4://, socks4a://, socks5://, http://, https://, hysteria2://, or hy2://",
"call": "URL must start with vless://, vmess://, ss://, trojan://, socks4://, socks4a://, socks5://, hysteria2://, or hy2://",
"key": "URL must start with vless://, vmess://, ss://, trojan://, socks4://, socks4a://, socks5://, hysteria2://, or hy2://",
"places": [
"src/validators/validateProxyUrl.ts:47"
"src/validators/validateProxyUrl.ts:42"
]
},
{
@@ -5545,7 +5475,6 @@
"src/validators/validateDns.ts:29",
"src/validators/validateDomain.ts:57",
"src/validators/validateDomain.ts:76",
"src/validators/validateHttpProxyUrl.ts:89",
"src/validators/validateHysteriaUrl.ts:151",
"src/validators/validateIp.ts:25",
"src/validators/validateOutboundJson.ts:118",
@@ -5610,8 +5539,8 @@
]
},
{
"call": "vless://, vmess://, ss://, trojan://, socks4/5://, http(s)://, hy2/hysteria2:// links",
"key": "vless://, vmess://, ss://, trojan://, socks4/5://, http(s)://, hy2/hysteria2:// links",
"call": "vless://, vmess://, ss://, trojan://, socks4/5://, hy2/hysteria2:// links",
"key": "vless://, vmess://, ss://, trojan://, socks4/5://, hy2/hysteria2:// links",
"places": [
"../luci-app-tachyon/htdocs/luci-static/resources/view/tachyon/section.js:7137"
]
+7 -48
View File
@@ -7,8 +7,8 @@ msgid ""
msgstr ""
"Project-Id-Version: TACHYON\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-14 05:07+0300\n"
"PO-Revision-Date: 2026-07-14 05:07+0300\n"
"POT-Creation-Date: 2026-07-15 06:45+0300\n"
"PO-Revision-Date: 2026-07-15 06:45+0300\n"
"Last-Translator: Dushnilin <dushnilin@gmail.com>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
@@ -1064,7 +1064,7 @@ msgstr ""
msgid "Faster server search interval"
msgstr ""
#: src/tachyon/methods/custom/getDashboardSections.ts:638
#: src/tachyon/methods/custom/getDashboardSections.ts:647
msgid "Fastest"
msgstr ""
@@ -1347,46 +1347,6 @@ msgstr ""
msgid "Invalid format. Use an IP address or CIDR subnet"
msgstr ""
#: src/validators/validateHttpProxyUrl.ts:57
msgid "Invalid HTTP proxy URL: invalid host and port"
msgstr ""
#: src/validators/validateHttpProxyUrl.ts:79
msgid "Invalid HTTP proxy URL: invalid port number"
msgstr ""
#: src/validators/validateHttpProxyUrl.ts:49
msgid "Invalid HTTP proxy URL: missing host and port"
msgstr ""
#: src/validators/validateHttpProxyUrl.ts:65
msgid "Invalid HTTP proxy URL: missing hostname or IP"
msgstr ""
#: src/validators/validateHttpProxyUrl.ts:72
msgid "Invalid HTTP proxy URL: missing port"
msgstr ""
#: src/validators/validateHttpProxyUrl.ts:41
msgid "Invalid HTTP proxy URL: missing username"
msgstr ""
#: src/validators/validateHttpProxyUrl.ts:18
msgid "Invalid HTTP proxy URL: must not contain spaces"
msgstr ""
#: src/validators/validateHttpProxyUrl.ts:9
msgid "Invalid HTTP proxy URL: must start with http:// or https://"
msgstr ""
#: src/validators/validateHttpProxyUrl.ts:85
msgid "Invalid HTTP proxy URL: parsing failed"
msgstr ""
#: src/validators/validateHttpProxyUrl.ts:26
msgid "Invalid HTTP proxy URL: path, query, and fragment are not supported"
msgstr ""
#: src/validators/validateHysteriaUrl.ts:104
msgid "Invalid HY2 URL: insecure must be 0 or 1"
msgstr ""
@@ -3089,7 +3049,6 @@ msgstr ""
msgid "unique UCI identifier"
msgstr ""
#: src/tachyon/helpers/singBoxVariant.ts:28
#: src/tachyon/services/systemInfo.service.ts:10
#: src/tachyon/services/systemInfo.service.ts:11
#: src/tachyon/services/systemInfo.service.ts:12
@@ -3099,6 +3058,7 @@ msgstr ""
#: src/tachyon/services/systemInfo.service.ts:22
#: src/tachyon/services/systemInfo.service.ts:25
#: src/tachyon/services/systemInfo.service.ts:26
#: src/tachyon/helpers/singBoxVariant.ts:28
#: src/tachyon/tabs/updates/initController.ts:329
#: src/tachyon/tabs/diagnostic/checks/runInboundsCheck.ts:25
msgid "unknown"
@@ -3166,8 +3126,8 @@ msgstr ""
msgid "Uploaded"
msgstr ""
#: src/validators/validateProxyUrl.ts:47
msgid "URL must start with vless://, vmess://, ss://, trojan://, socks4://, socks4a://, socks5://, http://, https://, hysteria2://, or hy2://"
#: src/validators/validateProxyUrl.ts:42
msgid "URL must start with vless://, vmess://, ss://, trojan://, socks4://, socks4a://, socks5://, hysteria2://, or hy2://"
msgstr ""
#: src/validators/validateUrl.ts:18
@@ -3293,7 +3253,6 @@ msgstr ""
#: src/validators/validateDns.ts:29
#: src/validators/validateDomain.ts:57
#: src/validators/validateDomain.ts:76
#: src/validators/validateHttpProxyUrl.ts:89
#: src/validators/validateHysteriaUrl.ts:151
#: src/validators/validateIp.ts:25
#: src/validators/validateOutboundJson.ts:118
@@ -3338,7 +3297,7 @@ msgid "VLESS flow"
msgstr ""
#: ../luci-app-tachyon/htdocs/luci-static/resources/view/tachyon/section.js:7137
msgid "vless://, vmess://, ss://, trojan://, socks4/5://, http(s)://, hy2/hysteria2:// links"
msgid "vless://, vmess://, ss://, trojan://, socks4/5://, hy2/hysteria2:// links"
msgstr ""
#: src/tachyon/tabs/diagnostic/checks/runInboundsCheck.ts:70
+6 -36
View File
@@ -7,8 +7,8 @@ msgid ""
msgstr ""
"Project-Id-Version: TACHYON\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-14 08:07+0300\n"
"PO-Revision-Date: 2026-07-14 08:07+0300\n"
"POT-Creation-Date: 2026-07-15 09:45+0300\n"
"PO-Revision-Date: 2026-07-15 09:45+0300\n"
"Last-Translator: Dushnilin\n"
"Language-Team: none\n"
"Language: ru\n"
@@ -926,36 +926,6 @@ msgstr "Неверный домен"
msgid "Invalid format. Use an IP address or CIDR subnet"
msgstr "Неверный формат. Используйте IP-адрес или подсеть CIDR"
msgid "Invalid HTTP proxy URL: invalid host and port"
msgstr "Неверный URL HTTP proxy: неверный хост и порт"
msgid "Invalid HTTP proxy URL: invalid port number"
msgstr "Неверный URL HTTP proxy: неверный номер порта"
msgid "Invalid HTTP proxy URL: missing host and port"
msgstr "Неверный URL HTTP proxy: отсутствуют хост и порт"
msgid "Invalid HTTP proxy URL: missing hostname or IP"
msgstr "Неверный URL HTTP proxy: отсутствует имя хоста или IP-адрес"
msgid "Invalid HTTP proxy URL: missing port"
msgstr "Неверный URL HTTP proxy: отсутствует порт"
msgid "Invalid HTTP proxy URL: missing username"
msgstr "Неверный URL HTTP proxy: отсутствует имя пользователя"
msgid "Invalid HTTP proxy URL: must not contain spaces"
msgstr "Неверный URL HTTP proxy: не должен содержать пробелов"
msgid "Invalid HTTP proxy URL: must start with http:// or https://"
msgstr "Неверный URL HTTP proxy: должен начинаться с http:// или https://"
msgid "Invalid HTTP proxy URL: parsing failed"
msgstr "Неверный URL HTTP proxy: ошибка разбора"
msgid "Invalid HTTP proxy URL: path, query, and fragment are not supported"
msgstr "Неверный URL HTTP proxy: путь, query и fragment не поддерживаются"
msgid "Invalid HY2 URL: insecure must be 0 or 1"
msgstr "Неверный URL Hysteria2: параметр insecure должен быть 0 или 1"
@@ -2204,8 +2174,8 @@ msgstr "Исходящий"
msgid "Uploaded"
msgstr "Отдано"
msgid "URL must start with vless://, vmess://, ss://, trojan://, socks4://, socks4a://, socks5://, http://, https://, hysteria2://, or hy2://"
msgstr "URL должен начинаться с vless://, vmess://, ss://, trojan://, socks4://, socks4a://, socks5://, http://, https://, hysteria2:// или hy2://"
msgid "URL must start with vless://, vmess://, ss://, trojan://, socks4://, socks4a://, socks5://, hysteria2://, or hy2://"
msgstr ""
msgid "URL must use one of the following protocols:"
msgstr "URL должен использовать один из следующих протоколов:"
@@ -2306,8 +2276,8 @@ msgstr "Видимое имя этой секции"
msgid "VLESS flow"
msgstr "VLESS flow"
msgid "vless://, vmess://, ss://, trojan://, socks4/5://, http(s)://, hy2/hysteria2:// links"
msgstr "ссылки vless://, vmess://, ss://, trojan://, socks4/5://, http(s)://, hy2/hysteria2://"
msgid "vless://, vmess://, ss://, trojan://, socks4/5://, hy2/hysteria2:// links"
msgstr ""
msgid "WAN"
msgstr "WAN"
@@ -1,19 +1,6 @@
const COPYABLE_PROXY_URI_RE =
/^(vless|vmess|trojan|ss|ssr|hysteria2|hy2|tuic|socks4|socks4a|socks5):\/\//i;
const HTTP_PROXY_URI_RE =
/^https?:\/\/(?:[^/@\s]+(?::[^/@\s]*)?@)?[^:/?#@\s]+:(\d{1,5})$/i;
export function isCopyableProxyLink(link?: string) {
const value = (link || '').trim();
if (COPYABLE_PROXY_URI_RE.test(value)) {
return true;
}
const httpProxy = value.match(HTTP_PROXY_URI_RE);
if (!httpProxy) {
return false;
}
const port = Number(httpProxy[1]);
return Number.isInteger(port) && port >= 1 && port <= 65535;
return COPYABLE_PROXY_URI_RE.test((link || '').trim());
}
@@ -13,8 +13,6 @@ 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 = [
@@ -22,6 +20,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',
@@ -1,4 +1,4 @@
import { getConfigSections } from './getConfigSections';
import { getConfigSections } from './getConfigSections';
import { ClashAPI, Tachyon } from '../../types';
import {
canUseDirectClashApi,
@@ -127,6 +127,7 @@ type ChildType =
| 'priority_level';
const DASHBOARD_SECTION_CACHE_DIR = '/var/run/tachyon/section-cache';
const CLASH_API_FETCH_TIMEOUT_MS = 5000;
function getDisplayName(section: Tachyon.ConfigSection) {
return section.label || section['.name'];
@@ -149,10 +150,16 @@ async function getClashApiProxies(
): Promise<Tachyon.MethodResponse<ClashAPI.Proxies>> {
if (canFetchClashApiDirectly()) {
const secret = getClashApiSecret(configSections);
const controller = new AbortController();
const timeoutId = setTimeout(
() => controller.abort(),
CLASH_API_FETCH_TIMEOUT_MS,
);
try {
const response = await fetch(`${getClashHttpUrl()}/proxies`, {
headers: secret ? { Authorization: `Bearer ${secret}` } : undefined,
signal: controller.signal,
});
if (response.ok) {
@@ -163,6 +170,8 @@ async function getClashApiProxies(
}
} catch (_error) {
// Fall back to rpcd below for controllers unavailable from the browser.
} finally {
clearTimeout(timeoutId);
}
}
@@ -1043,13 +1043,42 @@ describe('getDashboardSections', () => {
expect(result.success).toBe(true);
expect(fetchMock).toHaveBeenCalledWith(
'http://router.example:9090/proxies',
{
expect.objectContaining({
headers: { Authorization: 'Bearer secret' },
},
signal: expect.anything(),
}),
);
expect(mocks.getClashApiProxies).not.toHaveBeenCalled();
});
it('uses rpcd fallback when direct Clash API access times out', async () => {
vi.useFakeTimers();
try {
mocks.getConfigSections.mockResolvedValue([proxySection()]);
mocks.canUseDirectClashApi.mockReturnValue(true);
const fetchMock = vi.fn(
(_url: string, options?: RequestInit) =>
new Promise<Response>((_resolve, reject) => {
options?.signal?.addEventListener('abort', () =>
reject(new Error('aborted')),
);
}),
);
vi.stubGlobal('fetch', fetchMock);
const resultPromise = getDashboardSections();
await vi.advanceTimersByTimeAsync(5000);
const result = await resultPromise;
expect(result.success).toBe(true);
expect(mocks.getClashApiProxies).toHaveBeenCalledTimes(1);
} finally {
vi.useRealTimers();
}
});
it('uses rpcd fallback when direct Clash API access is unsafe', async () => {
mocks.getConfigSections.mockResolvedValue([proxySection()]);
const fetchMock = vi.fn().mockResolvedValue({
@@ -75,7 +75,7 @@ export async function runNftCheck() {
value: '',
},
{
state: data.rules_mangle_counters ? 'success' : 'error',
state: data.rules_mangle_counters ? 'success' : 'warning',
key: _('Rules mangle counters'),
value: '',
},
@@ -1,36 +0,0 @@
import { describe, expect, it } from 'vitest';
import { validateHttpProxyUrl } from '../validateHttpProxyUrl';
const validUrls = [
['http ipv4', 'http://127.0.0.1:80'],
['http domain', 'http://proxy.example.com:8080'],
['https auth', 'https://user:pass@example.com:443'],
['uppercase auth and host', 'https://USER:PASSWORD@Example.COM:8443'],
['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);
});
});
});
@@ -16,6 +16,14 @@ describe('validateOutboundJson', () => {
expect(result.valid).toBe(true);
});
it('accepts HTTP only as a JSON outbound', () => {
const result = validateOutboundJson(
'{"type":"http","tag":"http","server":"127.0.0.1","server_port":8080}',
);
expect(result.valid).toBe(true);
});
it('accepts a URLTest outbound with referenced outbounds', () => {
const result = validateOutboundJson(
'{"type":"urltest","tag":"auto","outbounds":["proxy-1"],"url":"https://www.gstatic.com/generate_204"}',
@@ -31,8 +31,6 @@ const validUrls = [
['socks4', 'socks4://127.0.0.1:1080'],
['socks4a', 'socks4a://example.com:1080'],
['socks5', 'socks5://user:pass@example.com:1080'],
['http proxy', 'http://127.0.0.1:80'],
['https proxy', 'https://user:pass@example.com:443'],
['hysteria2', 'hysteria2://password@example.com:443'],
['hy2', 'hy2://password@example.com:443'],
];
@@ -57,15 +55,14 @@ 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('hy2://');
});
it('rejects HTTP URLs that look like subscription URLs rather than proxy endpoints', () => {
const res = validateProxyUrl('https://example.com/subscription');
it('rejects native HTTP proxy URLs', () => {
const res = validateProxyUrl('https://user:pass@example.com:443');
expect(res.valid).toBe(false);
expect(res.message).toContain('path');
expect(res.message).not.toContain('http://');
});
it('rejects VMess configs with an invalid server host', () => {
@@ -1,90 +0,0 @@
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') };
}
@@ -5,7 +5,6 @@ import { validateVmessUrl } from './validateVmessUrl';
import { validateTrojanUrl } from './validateTrojanUrl';
import { validateSocksUrl } from './validateSocksUrl';
import { validateHysteria2Url } from './validateHysteriaUrl';
import { validateHttpProxyUrl } from './validateHttpProxyUrl';
// TODO refactor current validation and add tests
export function validateProxyUrl(url: string): ValidationResult {
@@ -31,10 +30,6 @@ 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 +40,7 @@ export function validateProxyUrl(url: string): ValidationResult {
return {
valid: false,
message: _(
'URL must start with vless://, vmess://, ss://, trojan://, socks4://, socks4a://, socks5://, http://, https://, hysteria2://, or hy2://',
'URL must start with vless://, vmess://, ss://, trojan://, socks4://, socks4a://, socks5://, hysteria2://, or hy2://',
),
};
}
+4 -2
View File
@@ -765,6 +765,8 @@ function select_dns_owner(legacy) {
}
}
let dnsmasq_failsafe_restore;
function installer_restore_dnsmasq(bin_path, legacy) {
if (path_executable(bin_path) && run_args([ bin_path, "restore_dnsmasq" ]))
return true;
@@ -1098,7 +1100,7 @@ function dnsmasq_restore_default_instance() {
}
}
function dnsmasq_failsafe_restore() {
dnsmasq_failsafe_restore = function() {
if (!uci_available())
return true;
@@ -1113,7 +1115,7 @@ function dnsmasq_failsafe_restore() {
uci_commit("dhcp");
restart_dnsmasq();
return true;
}
};
function release_version_valid(value) {
return match(as_string(value), /^[0-9]+[.][0-9]+[.][0-9]+$/) != null;
@@ -809,85 +809,6 @@ function validateHysteria2Url(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();
@@ -906,16 +827,13 @@ 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);
}
return {
valid: false,
message: _(
"URL must start with vless://, vmess://, ss://, trojan://, socks4://, socks4a://, socks5://, http://, https://, hysteria2://, or hy2://"
"URL must start with vless://, vmess://, ss://, trojan://, socks4://, socks4a://, socks5://, hysteria2://, or hy2://"
)
};
}
@@ -1206,18 +1124,8 @@ 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 HTTP_PROXY_URI_RE = /^https?:\/\/(?:[^/@\s]+(?::[^/@\s]*)?@)?[^:/?#@\s]+:(\d{1,5})$/i;
function isCopyableProxyLink(link) {
const value = (link || "").trim();
if (COPYABLE_PROXY_URI_RE.test(value)) {
return true;
}
const httpProxy = value.match(HTTP_PROXY_URI_RE);
if (!httpProxy) {
return false;
}
const port = Number(httpProxy[1]);
return Number.isInteger(port) && port >= 1 && port <= 65535;
return COPYABLE_PROXY_URI_RE.test((link || "").trim());
}
// src/icons/renderLoaderCircleIcon24.ts
@@ -3177,6 +3085,7 @@ var TachyonShellMethods = {
// src/tachyon/methods/custom/getDashboardSections.ts
var DASHBOARD_SECTION_CACHE_DIR = "/var/run/tachyon/section-cache";
var CLASH_API_FETCH_TIMEOUT_MS = 5e3;
function getDisplayName(section) {
return section.label || section[".name"];
}
@@ -3192,9 +3101,15 @@ function canFetchClashApiDirectly() {
async function getClashApiProxies(configSections) {
if (canFetchClashApiDirectly()) {
const secret = getClashApiSecret(configSections);
const controller = new AbortController();
const timeoutId = setTimeout(
() => controller.abort(),
CLASH_API_FETCH_TIMEOUT_MS
);
try {
const response = await fetch(`${getClashHttpUrl()}/proxies`, {
headers: secret ? { Authorization: `Bearer ${secret}` } : void 0
headers: secret ? { Authorization: `Bearer ${secret}` } : void 0,
signal: controller.signal
});
if (response.ok) {
return {
@@ -3203,6 +3118,8 @@ async function getClashApiProxies(configSections) {
};
}
} catch (_error) {
} finally {
clearTimeout(timeoutId);
}
}
return TachyonShellMethods.getClashApiProxies();
@@ -7992,7 +7909,7 @@ async function runNftCheck() {
value: ""
},
{
state: data.rules_mangle_counters ? "success" : "error",
state: data.rules_mangle_counters ? "success" : "warning",
key: _("Rules mangle counters"),
value: ""
},
@@ -7135,7 +7135,7 @@ function createSectionContent(section) {
"selector_proxy_links",
_("Connection URL"),
_(
"vless://, vmess://, ss://, trojan://, socks4/5://, http(s)://, hy2/hysteria2:// links",
"vless://, vmess://, ss://, trojan://, socks4/5://, hy2/hysteria2:// links",
),
);
o.depends("action", "connection");
+6 -36
View File
@@ -7,8 +7,8 @@ msgid ""
msgstr ""
"Project-Id-Version: TACHYON\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-14 08:07+0300\n"
"PO-Revision-Date: 2026-07-14 08:07+0300\n"
"POT-Creation-Date: 2026-07-15 09:45+0300\n"
"PO-Revision-Date: 2026-07-15 09:45+0300\n"
"Last-Translator: Dushnilin\n"
"Language-Team: none\n"
"Language: ru\n"
@@ -926,36 +926,6 @@ msgstr "Неверный домен"
msgid "Invalid format. Use an IP address or CIDR subnet"
msgstr "Неверный формат. Используйте IP-адрес или подсеть CIDR"
msgid "Invalid HTTP proxy URL: invalid host and port"
msgstr "Неверный URL HTTP proxy: неверный хост и порт"
msgid "Invalid HTTP proxy URL: invalid port number"
msgstr "Неверный URL HTTP proxy: неверный номер порта"
msgid "Invalid HTTP proxy URL: missing host and port"
msgstr "Неверный URL HTTP proxy: отсутствуют хост и порт"
msgid "Invalid HTTP proxy URL: missing hostname or IP"
msgstr "Неверный URL HTTP proxy: отсутствует имя хоста или IP-адрес"
msgid "Invalid HTTP proxy URL: missing port"
msgstr "Неверный URL HTTP proxy: отсутствует порт"
msgid "Invalid HTTP proxy URL: missing username"
msgstr "Неверный URL HTTP proxy: отсутствует имя пользователя"
msgid "Invalid HTTP proxy URL: must not contain spaces"
msgstr "Неверный URL HTTP proxy: не должен содержать пробелов"
msgid "Invalid HTTP proxy URL: must start with http:// or https://"
msgstr "Неверный URL HTTP proxy: должен начинаться с http:// или https://"
msgid "Invalid HTTP proxy URL: parsing failed"
msgstr "Неверный URL HTTP proxy: ошибка разбора"
msgid "Invalid HTTP proxy URL: path, query, and fragment are not supported"
msgstr "Неверный URL HTTP proxy: путь, query и fragment не поддерживаются"
msgid "Invalid HY2 URL: insecure must be 0 or 1"
msgstr "Неверный URL Hysteria2: параметр insecure должен быть 0 или 1"
@@ -2204,8 +2174,8 @@ msgstr "Исходящий"
msgid "Uploaded"
msgstr "Отдано"
msgid "URL must start with vless://, vmess://, ss://, trojan://, socks4://, socks4a://, socks5://, http://, https://, hysteria2://, or hy2://"
msgstr "URL должен начинаться с vless://, vmess://, ss://, trojan://, socks4://, socks4a://, socks5://, http://, https://, hysteria2:// или hy2://"
msgid "URL must start with vless://, vmess://, ss://, trojan://, socks4://, socks4a://, socks5://, hysteria2://, or hy2://"
msgstr ""
msgid "URL must use one of the following protocols:"
msgstr "URL должен использовать один из следующих протоколов:"
@@ -2306,8 +2276,8 @@ msgstr "Видимое имя этой секции"
msgid "VLESS flow"
msgstr "VLESS flow"
msgid "vless://, vmess://, ss://, trojan://, socks4/5://, http(s)://, hy2/hysteria2:// links"
msgstr "ссылки vless://, vmess://, ss://, trojan://, socks4/5://, http(s)://, hy2/hysteria2://"
msgid "vless://, vmess://, ss://, trojan://, socks4/5://, hy2/hysteria2:// links"
msgstr ""
msgid "WAN"
msgstr "WAN"
+7 -48
View File
@@ -7,8 +7,8 @@ msgid ""
msgstr ""
"Project-Id-Version: TACHYON\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-14 05:07+0300\n"
"PO-Revision-Date: 2026-07-14 05:07+0300\n"
"POT-Creation-Date: 2026-07-15 06:45+0300\n"
"PO-Revision-Date: 2026-07-15 06:45+0300\n"
"Last-Translator: Dushnilin <dushnilin@gmail.com>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
@@ -1064,7 +1064,7 @@ msgstr ""
msgid "Faster server search interval"
msgstr ""
#: src/tachyon/methods/custom/getDashboardSections.ts:638
#: src/tachyon/methods/custom/getDashboardSections.ts:647
msgid "Fastest"
msgstr ""
@@ -1347,46 +1347,6 @@ msgstr ""
msgid "Invalid format. Use an IP address or CIDR subnet"
msgstr ""
#: src/validators/validateHttpProxyUrl.ts:57
msgid "Invalid HTTP proxy URL: invalid host and port"
msgstr ""
#: src/validators/validateHttpProxyUrl.ts:79
msgid "Invalid HTTP proxy URL: invalid port number"
msgstr ""
#: src/validators/validateHttpProxyUrl.ts:49
msgid "Invalid HTTP proxy URL: missing host and port"
msgstr ""
#: src/validators/validateHttpProxyUrl.ts:65
msgid "Invalid HTTP proxy URL: missing hostname or IP"
msgstr ""
#: src/validators/validateHttpProxyUrl.ts:72
msgid "Invalid HTTP proxy URL: missing port"
msgstr ""
#: src/validators/validateHttpProxyUrl.ts:41
msgid "Invalid HTTP proxy URL: missing username"
msgstr ""
#: src/validators/validateHttpProxyUrl.ts:18
msgid "Invalid HTTP proxy URL: must not contain spaces"
msgstr ""
#: src/validators/validateHttpProxyUrl.ts:9
msgid "Invalid HTTP proxy URL: must start with http:// or https://"
msgstr ""
#: src/validators/validateHttpProxyUrl.ts:85
msgid "Invalid HTTP proxy URL: parsing failed"
msgstr ""
#: src/validators/validateHttpProxyUrl.ts:26
msgid "Invalid HTTP proxy URL: path, query, and fragment are not supported"
msgstr ""
#: src/validators/validateHysteriaUrl.ts:104
msgid "Invalid HY2 URL: insecure must be 0 or 1"
msgstr ""
@@ -3089,7 +3049,6 @@ msgstr ""
msgid "unique UCI identifier"
msgstr ""
#: src/tachyon/helpers/singBoxVariant.ts:28
#: src/tachyon/services/systemInfo.service.ts:10
#: src/tachyon/services/systemInfo.service.ts:11
#: src/tachyon/services/systemInfo.service.ts:12
@@ -3099,6 +3058,7 @@ msgstr ""
#: src/tachyon/services/systemInfo.service.ts:22
#: src/tachyon/services/systemInfo.service.ts:25
#: src/tachyon/services/systemInfo.service.ts:26
#: src/tachyon/helpers/singBoxVariant.ts:28
#: src/tachyon/tabs/updates/initController.ts:329
#: src/tachyon/tabs/diagnostic/checks/runInboundsCheck.ts:25
msgid "unknown"
@@ -3166,8 +3126,8 @@ msgstr ""
msgid "Uploaded"
msgstr ""
#: src/validators/validateProxyUrl.ts:47
msgid "URL must start with vless://, vmess://, ss://, trojan://, socks4://, socks4a://, socks5://, http://, https://, hysteria2://, or hy2://"
#: src/validators/validateProxyUrl.ts:42
msgid "URL must start with vless://, vmess://, ss://, trojan://, socks4://, socks4a://, socks5://, hysteria2://, or hy2://"
msgstr ""
#: src/validators/validateUrl.ts:18
@@ -3293,7 +3253,6 @@ msgstr ""
#: src/validators/validateDns.ts:29
#: src/validators/validateDomain.ts:57
#: src/validators/validateDomain.ts:76
#: src/validators/validateHttpProxyUrl.ts:89
#: src/validators/validateHysteriaUrl.ts:151
#: src/validators/validateIp.ts:25
#: src/validators/validateOutboundJson.ts:118
@@ -3338,7 +3297,7 @@ msgid "VLESS flow"
msgstr ""
#: ../luci-app-tachyon/htdocs/luci-static/resources/view/tachyon/section.js:7137
msgid "vless://, vmess://, ss://, trojan://, socks4/5://, http(s)://, hy2/hysteria2:// links"
msgid "vless://, vmess://, ss://, trojan://, socks4/5://, hy2/hysteria2:// links"
msgstr ""
#: src/tachyon/tabs/diagnostic/checks/runInboundsCheck.ts:70
+2 -1
View File
@@ -1,7 +1,8 @@
config settings 'settings'
option config_version '1.0.2'
option config_version '1.0.5'
list applied_migrations 'interface_sections'
list applied_migrations 'enable_component_checks'
list applied_migrations 'http_connection_urls'
option dns_type 'udp'
list dns_server '77.88.8.8'
list bootstrap_dns_server '77.88.8.8'
+50 -41
View File
@@ -50,22 +50,22 @@ const NFT_IP_PORT6_SET_NAME = getenv("NFT_IP_PORT6_SET_NAME") || "tachyon_ip6_po
const NFT_DISCORD_SET_NAME = getenv("NFT_DISCORD_SET_NAME") || "tachyon_discord_subnets";
const NFT_DISCORD6_SET_NAME = getenv("NFT_DISCORD6_SET_NAME") || "tachyon_discord_subnets6";
const NFT_INTERFACE_SET_NAME = getenv("NFT_INTERFACE_SET_NAME") || "tachyon_interfaces";
const NFT_FAKEIP_MARK = getenv("NFT_FAKEIP_MARK") || "0x00100000";
const NFT_FAKEIP_MARK = getenv("NFT_FAKEIP_MARK") || "0x04000000";
const SB_SERVICE_MIXED_INBOUND_ADDRESS = getenv("SB_SERVICE_MIXED_INBOUND_ADDRESS") || "127.0.0.1";
const SB_SERVICE_MIXED_INBOUND_PORT = getenv("SB_SERVICE_MIXED_INBOUND_PORT") || "4534";
const SB_VARIANT_STATE_FILE = getenv("SB_VARIANT_STATE_FILE") || "/etc/tachyon/sing-box-variant";
const GITHUB_RAW_URL = getenv("GITHUB_RAW_URL") || "https://raw.githubusercontent.com/itdoginfo/allow-domains/main";
const BUILTIN_SUBNET_URLS = {
twitter: getenv("SUBNETS_TWITTER") || GITHUB_RAW_URL + "/Subnets/IPv4/twitter.lst",
meta: getenv("SUBNETS_META") || GITHUB_RAW_URL + "/Subnets/IPv4/meta.lst",
discord: getenv("SUBNETS_DISCORD") || GITHUB_RAW_URL + "/Subnets/IPv4/discord.lst",
roblox: getenv("SUBNETS_ROBLOX") || GITHUB_RAW_URL + "/Subnets/IPv4/roblox.lst",
telegram: getenv("SUBNETS_TELERAM") || GITHUB_RAW_URL + "/Subnets/IPv4/telegram.lst",
cloudflare: getenv("SUBNETS_CLOUDFLARE") || GITHUB_RAW_URL + "/Subnets/IPv4/cloudflare.lst",
hetzner: getenv("SUBNETS_HETZNER") || GITHUB_RAW_URL + "/Subnets/IPv4/hetzner.lst",
ovh: getenv("SUBNETS_OVH") || GITHUB_RAW_URL + "/Subnets/IPv4/ovh.lst",
digitalocean: getenv("SUBNETS_DIGITALOCEAN") || GITHUB_RAW_URL + "/Subnets/IPv4/digitalocean.lst",
cloudfront: getenv("SUBNETS_CLOUDFRONT") || GITHUB_RAW_URL + "/Subnets/IPv4/cloudfront.lst"
twitter: [ getenv("SUBNETS_TWITTER") || GITHUB_RAW_URL + "/Subnets/IPv4/twitter.lst", getenv("SUBNETS_TWITTER6") || GITHUB_RAW_URL + "/Subnets/IPv6/twitter.lst" ],
meta: [ getenv("SUBNETS_META") || GITHUB_RAW_URL + "/Subnets/IPv4/meta.lst", getenv("SUBNETS_META6") || GITHUB_RAW_URL + "/Subnets/IPv6/meta.lst" ],
discord: [ getenv("SUBNETS_DISCORD") || GITHUB_RAW_URL + "/Subnets/IPv4/discord.lst", getenv("SUBNETS_DISCORD6") || GITHUB_RAW_URL + "/Subnets/IPv6/discord.lst" ],
roblox: [ getenv("SUBNETS_ROBLOX") || GITHUB_RAW_URL + "/Subnets/IPv4/roblox.lst" ],
telegram: [ getenv("SUBNETS_TELERAM") || GITHUB_RAW_URL + "/Subnets/IPv4/telegram.lst", getenv("SUBNETS_TELERAM6") || GITHUB_RAW_URL + "/Subnets/IPv6/telegram.lst" ],
cloudflare: [ getenv("SUBNETS_CLOUDFLARE") || GITHUB_RAW_URL + "/Subnets/IPv4/cloudflare.lst", getenv("SUBNETS_CLOUDFLARE6") || GITHUB_RAW_URL + "/Subnets/IPv6/cloudflare.lst" ],
hetzner: [ getenv("SUBNETS_HETZNER") || GITHUB_RAW_URL + "/Subnets/IPv4/hetzner.lst", getenv("SUBNETS_HETZNER6") || GITHUB_RAW_URL + "/Subnets/IPv6/hetzner.lst" ],
ovh: [ getenv("SUBNETS_OVH") || GITHUB_RAW_URL + "/Subnets/IPv4/ovh.lst", getenv("SUBNETS_OVH6") || GITHUB_RAW_URL + "/Subnets/IPv6/ovh.lst" ],
digitalocean: [ getenv("SUBNETS_DIGITALOCEAN") || GITHUB_RAW_URL + "/Subnets/IPv4/digitalocean.lst", getenv("SUBNETS_DIGITALOCEAN6") || GITHUB_RAW_URL + "/Subnets/IPv6/digitalocean.lst" ],
cloudfront: [ getenv("SUBNETS_CLOUDFRONT") || GITHUB_RAW_URL + "/Subnets/IPv4/cloudfront.lst", getenv("SUBNETS_CLOUDFRONT6") || GITHUB_RAW_URL + "/Subnets/IPv6/cloudfront.lst" ]
};
let rule_config = null;
let routing_rulesets_module_value = null;
@@ -2120,41 +2120,43 @@ function import_builtin_subnets_from_rule(section, settings) {
if (!singbox_rulesets_module().is_community(service))
continue;
let url = BUILTIN_SUBNET_URLS[as_string(service)];
if (url == null)
let urls = BUILTIN_SUBNET_URLS[as_string(service)];
if (type(urls) != "array")
continue;
let tmpfile = temp_path();
if (tmpfile == "") {
ok = false;
continue;
}
for (let url in urls) {
let tmpfile = temp_path();
if (tmpfile == "") {
ok = false;
continue;
}
if (!download_to_file(url, tmpfile, service_proxy_address(settings, "lists")) || !file_nonempty(tmpfile)) {
log_message("Failed to download built-in " + as_string(service) + " subnet list; skipping it until the next successful update", "warn");
remove_file(tmpfile);
continue;
}
if (!nft_module_success([
"nft-add-community-subnet-file-for-uci-section",
section_name(section),
service,
tmpfile,
NFT_TABLE_NAME,
NFT_COMMON_SET_NAME,
NFT_IP_PORT_SET_NAME,
NFT_INTERFACE_SET_NAME,
NFT_DISCORD_SET_NAME,
NFT_FAKEIP_MARK,
"5000",
NFT_COMMON6_SET_NAME,
NFT_IP_PORT6_SET_NAME,
NFT_DISCORD6_SET_NAME
]))
ok = false;
if (!download_to_file(url, tmpfile, service_proxy_address(settings, "lists")) || !file_nonempty(tmpfile)) {
log_message("Failed to download built-in " + as_string(service) + " subnet list; skipping it until the next successful update", "warn");
remove_file(tmpfile);
continue;
}
if (!nft_module_success([
"nft-add-community-subnet-file-for-uci-section",
section_name(section),
service,
tmpfile,
NFT_TABLE_NAME,
NFT_COMMON_SET_NAME,
NFT_IP_PORT_SET_NAME,
NFT_INTERFACE_SET_NAME,
NFT_DISCORD_SET_NAME,
NFT_FAKEIP_MARK,
"5000",
NFT_COMMON6_SET_NAME,
NFT_IP_PORT6_SET_NAME,
NFT_DISCORD6_SET_NAME
]))
ok = false;
remove_file(tmpfile);
}
return ok;
@@ -2859,6 +2861,11 @@ function fixture_subscription_update_section_due_status(path, section_name_value
subscription_update_section_due_status(fixture_section_by_name(data, section_name_value), timestamp_path, now);
}
function print_builtin_subnet_urls(service) {
for (let url in array_or_empty(BUILTIN_SUBNET_URLS[as_string(service)]))
print(url, "\n");
}
let mode = ARGV[0] || "";
if (mode == "json-length")
@@ -2867,6 +2874,8 @@ else if (mode == "update-is-due")
update_is_due(ARGV[1], ARGV[2], ARGV[3]);
else if (mode == "duration-to-seconds")
duration_to_seconds(ARGV[1]);
else if (mode == "builtin-subnet-urls")
print_builtin_subnet_urls(ARGV[1]);
else if (mode == "due-check-cron-schedule")
due_check_cron_schedule(ARGV[1]);
else if (mode == "list-update-cron-job")
+105 -3
View File
@@ -6,6 +6,7 @@
let fs = require("fs");
let common = require("core.common");
let uci_core = require("core.uci");
let core_url = require("core.url");
let constants_module = require("core.constants");
let singbox_constants_module = require("singbox.constants");
let domain_config = require("config.domain");
@@ -1183,9 +1184,110 @@ function migrate_enable_component_checks(ctx) {
set_option(ctx, ctx.model.settings, "component_update_check_enabled", "1");
}
function migration_port(value) {
value = as_string(value);
if (match(value, /^[0-9]+$/) == null)
return null;
value = int(value);
return value >= 1 && value <= 65535 ? value : null;
}
function unique_migration_tag(base, taken) {
base = trim(as_string(base));
if (base == "")
base = "http";
if (!taken[base])
return base;
for (let suffix = 1; suffix < 100000; suffix++) {
let candidate = base + "-" + suffix;
if (!taken[candidate])
return candidate;
}
return base + "-overflow";
}
function migrated_http_outbound(link, tag_name) {
link = core_url.strip_fragment(core_url.decode(link));
let scheme = core_url.scheme(link);
if (scheme != "http" && scheme != "https")
return null;
let host = core_url.host(link);
let port = migration_port(core_url.port(link));
let path = core_url.path(link);
if (host == "" || port == null || (path != "" && path != "/") || index(link, "?") >= 0)
return null;
let outbound = {
type: "http",
tag: tag_name,
server: host,
server_port: port
};
let userinfo = core_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 migrate_http_connection_urls(ctx) {
if (!release_at_most(ctx, "1.0.4"))
return;
for (let section in ctx.model.sections) {
let existing_jsons = option_list_values(section, "outbound_jsons");
let taken = {};
for (let value in existing_jsons) {
try {
let outbound = json(value);
let tag_name = type(outbound) == "object" ? trim(as_string(outbound.tag || "")) : "";
if (tag_name != "")
taken[tag_name] = true;
}
catch (e) {
}
}
let remaining_links = [];
let migrated_jsons = [];
for (let link in option_list_values(section, "selector_proxy_links")) {
let tag_name = unique_migration_tag(core_url.fragment(link), taken);
let outbound = migrated_http_outbound(link, tag_name);
if (outbound == null) {
push(remaining_links, link);
continue;
}
taken[tag_name] = true;
push(migrated_jsons, sprintf("%J", outbound));
}
if (length(migrated_jsons) == 0)
continue;
if (length(remaining_links) > 0)
set_list_option(ctx, section, "selector_proxy_links", remaining_links);
else
delete_option(ctx, section, "selector_proxy_links");
for (let value in existing_jsons)
push(migrated_jsons, value);
set_list_option(ctx, section, "outbound_jsons", migrated_jsons);
}
}
const MIGRATIONS = [
{ id: "interface_sections", run: migrate_interface_sections },
{ id: "enable_component_checks", run: migrate_enable_component_checks }
{ id: "enable_component_checks", run: migrate_enable_component_checks },
{ id: "http_connection_urls", run: migrate_http_connection_urls }
];
function apply_migrations(ctx) {
@@ -1215,8 +1317,8 @@ function apply_migrations(ctx) {
if (added)
set_list_option(ctx, ctx.model.settings, APPLIED_MIGRATIONS_OPTION, applied);
if (release_at_most(ctx, "1.0.1"))
set_option(ctx, ctx.model.settings, CONFIG_VERSION_OPTION, "1.0.2");
if (release_at_most(ctx, "1.0.4"))
set_option(ctx, ctx.model.settings, CONFIG_VERSION_OPTION, "1.0.5");
}
function migrate_podkop_model(model, constants) {
+12 -1
View File
@@ -15,6 +15,7 @@ let connections = require("config.connections");
const CONFIG_NAME = getenv("TACHYON_CONFIG_NAME") || "tachyon";
const DEFAULT_LATENCY_TEST_URL = "https://www.gstatic.com/generate_204";
const TAILSCALE_FWMARK_MASK = 0x00ff0000;
function as_string(value) {
return value == null ? "" : "" + value;
@@ -1658,11 +1659,21 @@ function validate_list_update_settings(settings) {
function validate_runtime_mark_ranges_context(context) {
let fakeip_mark = parse_number(context.nft_fakeip_mark);
let outbound_mark = parse_number(context.nft_outbound_mark);
if (fakeip_mark == null || outbound_mark == null)
fail_validation("Tachyon marks contain invalid numeric constants. Aborted.");
if ((fakeip_mark & TAILSCALE_FWMARK_MASK) != 0)
fail_validation("FakeIP mark overlaps Tailscale fwmark mask 0x00ff0000. Aborted.");
if ((outbound_mark & TAILSCALE_FWMARK_MASK) != 0)
fail_validation("Outbound mark overlaps Tailscale fwmark mask 0x00ff0000. Aborted.");
let ranges = [
[ "Zapret", context.zapret_route_mark_base, context.zapret_queue_range_size, fakeip_mark, "FakeIP mark " + context.nft_fakeip_mark ],
[ "Zapret", context.zapret_route_mark_base, context.zapret_queue_range_size, outbound_mark, "outbound mark " + context.nft_outbound_mark ],
[ "Zapret2", context.zapret2_route_mark_base, context.zapret2_queue_range_size, fakeip_mark, "FakeIP mark " + context.nft_fakeip_mark ],
[ "Zapret2", context.zapret2_route_mark_base, context.zapret2_queue_range_size, outbound_mark, "outbound mark " + context.nft_outbound_mark ]
[ "Zapret2", context.zapret2_route_mark_base, context.zapret2_queue_range_size, outbound_mark, "outbound mark " + context.nft_outbound_mark ],
[ "Zapret", context.zapret_route_mark_base, context.zapret_queue_range_size, TAILSCALE_FWMARK_MASK, "Tailscale fwmark mask 0x00ff0000" ],
[ "Zapret2", context.zapret2_route_mark_base, context.zapret2_queue_range_size, TAILSCALE_FWMARK_MASK, "Tailscale fwmark mask 0x00ff0000" ]
];
for (let range in ranges) {
+3 -3
View File
@@ -48,8 +48,8 @@ function constants_map() {
c.NFT_DISCORD_SET_NAME = env("NFT_DISCORD_SET_NAME", "tachyon_discord_subnets");
c.NFT_DISCORD6_SET_NAME = env("NFT_DISCORD6_SET_NAME", "tachyon_discord_subnets6");
c.NFT_INTERFACE_SET_NAME = env("NFT_INTERFACE_SET_NAME", "tachyon_interfaces");
c.NFT_FAKEIP_MARK = env("NFT_FAKEIP_MARK", "0x00100000");
c.NFT_OUTBOUND_MARK = env("NFT_OUTBOUND_MARK", "0x00200000");
c.NFT_FAKEIP_MARK = env("NFT_FAKEIP_MARK", "0x04000000");
c.NFT_OUTBOUND_MARK = env("NFT_OUTBOUND_MARK", "0x08000000");
c.SB_REQUIRED_VERSION = env("SB_REQUIRED_VERSION", "1.12.0");
c.SB_MANAGED_SERVICE_MARKER = env("SB_MANAGED_SERVICE_MARKER", "Tachyon managed sing-box service for binary variants");
@@ -124,7 +124,7 @@ function constants_map() {
c.ZAPRET2_PID_DIR = env("ZAPRET2_PID_DIR", c.ZAPRET2_STATE_DIR + "/pid");
c.ZAPRET2_CHILD_PID_DIR = env("ZAPRET2_CHILD_PID_DIR", c.ZAPRET2_STATE_DIR + "/child-pid");
c.ZAPRET2_LOG_DIR = env("ZAPRET2_LOG_DIR", c.ZAPRET2_STATE_DIR + "/log");
c.ZAPRET2_ROUTE_MARK_BASE = env("ZAPRET2_ROUTE_MARK_BASE", "0x01010000");
c.ZAPRET2_ROUTE_MARK_BASE = env("ZAPRET2_ROUTE_MARK_BASE", "0x02000000");
c.ZAPRET2_QUEUE_BASE = env("ZAPRET2_QUEUE_BASE", "4300");
c.ZAPRET2_QUEUE_RANGE_SIZE = env("ZAPRET2_QUEUE_RANGE_SIZE", "256");
c.ZAPRET2_NFQWS2_RESPAWN_DELAY = env("ZAPRET2_NFQWS2_RESPAWN_DELAY", "5");
+11 -3
View File
@@ -24,7 +24,7 @@ const CHECK_PROXY_IP_DOMAIN = getenv("CHECK_PROXY_IP_DOMAIN") || constants.CHECK
const FAKEIP_TEST_DOMAIN = getenv("FAKEIP_TEST_DOMAIN") || constants.FAKEIP_TEST_DOMAIN || "fakeip.podkop.fyi";
const RT_TABLE_NAME = getenv("RT_TABLE_NAME") || constants.RT_TABLE_NAME || "tachyon";
const NFT_TABLE_NAME = getenv("NFT_TABLE_NAME") || constants.NFT_TABLE_NAME || "TachyonTable";
const NFT_FAKEIP_MARK = getenv("NFT_FAKEIP_MARK") || constants.NFT_FAKEIP_MARK || "0x00100000";
const NFT_FAKEIP_MARK = getenv("NFT_FAKEIP_MARK") || constants.NFT_FAKEIP_MARK || "0x04000000";
const NFT_COMMON_SET_NAME = getenv("NFT_COMMON_SET_NAME") || constants.NFT_COMMON_SET_NAME || "tachyon_subnets";
const NFT_PORT_SET_NAME = getenv("NFT_PORT_SET_NAME") || constants.NFT_PORT_SET_NAME || "tachyon_ports";
const NFT_IP_PORT_SET_NAME = getenv("NFT_IP_PORT_SET_NAME") || constants.NFT_IP_PORT_SET_NAME || "tachyon_ip_ports";
@@ -1624,7 +1624,7 @@ function clash_proxy_type_map(base_url, auth) {
function clash_latency_endpoint(base_url, proxy_tag, proxy_type) {
proxy_type = as_string(proxy_type);
if (proxy_type == "URLTest" || proxy_type == "urltest")
if (lc(proxy_type) == "urltest")
return base_url + "/group/" + clash_urlencode(proxy_tag) + "/delay";
return base_url + "/proxies/" + clash_urlencode(proxy_tag) + "/delay";
}
@@ -1689,7 +1689,15 @@ function clash_api(action, arg1, arg2, arg3) {
module_success(SERVICE_UI_UC, [ "latency-progress-state", progress_path, count, total, failed ]);
let proxy_types = clash_proxy_type_map(base_url, auth);
for (let proxy_tag in proxy_tags) {
let ordered_proxy_tags = [];
for (let proxy_tag in proxy_tags)
if (lc(as_string(proxy_types[proxy_tag])) != "urltest")
push(ordered_proxy_tags, proxy_tag);
for (let proxy_tag in proxy_tags)
if (lc(as_string(proxy_types[proxy_tag])) == "urltest")
push(ordered_proxy_tags, proxy_tag);
for (let proxy_tag in ordered_proxy_tags) {
let args = [ "curl", "-G", "-s", clash_latency_endpoint(base_url, proxy_tag, proxy_types[proxy_tag]) ];
for (let item in auth) push(args, item);
push(args, "--data-urlencode");
+3 -2
View File
@@ -636,7 +636,8 @@ function section_needs_priority_sets(section) {
function nft_create_priority_chains(table) {
return nft_create_chain(table, "priority_rules", "{ }") &&
nft_create_chain(table, "priority_output_rules", "{ }");
nft_create_chain(table, "priority_output_rules", "{ }") &&
nft_add_rule(table, "priority_output_rules", [ "meta", "mark", "!=", "0", "return" ]);
}
function nft_create_priority_sets(table, sets) {
@@ -778,7 +779,7 @@ function nft_create_runtime_base(table, localv4_set, common_set, port_set, ip_po
if (!nft_add_set_elements(table, interface_set, interface))
return false;
if (!nft_create_chain(table, "mangle", "{ type filter hook prerouting priority -150; policy accept; }") ||
if (!nft_create_chain(table, "mangle", "{ type filter hook prerouting priority -149; policy accept; }") ||
!nft_create_chain(table, "mangle_output", "{ type route hook output priority -150; policy accept; }") ||
!nft_create_priority_chains(table) ||
!nft_create_chain(table, "proxy", "{ type filter hook prerouting priority -100; policy accept; }"))
+3 -3
View File
@@ -69,8 +69,8 @@ const NFT_PORT_SET_NAME = constant_value("NFT_PORT_SET_NAME", "tachyon_ports");
const NFT_IP_PORT_SET_NAME = constant_value("NFT_IP_PORT_SET_NAME", "tachyon_ip_ports");
const NFT_IP_PORT6_SET_NAME = constant_value("NFT_IP_PORT6_SET_NAME", "tachyon_ip6_ports");
const NFT_INTERFACE_SET_NAME = constant_value("NFT_INTERFACE_SET_NAME", "tachyon_interfaces");
const NFT_FAKEIP_MARK = constant_value("NFT_FAKEIP_MARK", "0x00100000");
const NFT_OUTBOUND_MARK = constant_value("NFT_OUTBOUND_MARK", "0x00200000");
const NFT_FAKEIP_MARK = constant_value("NFT_FAKEIP_MARK", "0x04000000");
const NFT_OUTBOUND_MARK = constant_value("NFT_OUTBOUND_MARK", "0x08000000");
const SB_FAKEIP_INET4_RANGE = constant_value("SB_FAKEIP_INET4_RANGE", "198.18.0.0/15");
const SB_FAKEIP_INET6_RANGE = constant_value("SB_FAKEIP_INET6_RANGE", "fc00::/18");
@@ -87,7 +87,7 @@ const ZAPRET_QUEUE_BASE = constant_value("ZAPRET_QUEUE_BASE", "4000");
const ZAPRET_DESYNC_MARK = constant_value("ZAPRET_DESYNC_MARK", "0x40000000");
const ZAPRET_DESYNC_MARK_POSTNAT = constant_value("ZAPRET_DESYNC_MARK_POSTNAT", "0x20000000");
const ZAPRET2_PROVIDER_NFQWS2_BIN = constant_value("ZAPRET2_PROVIDER_NFQWS2_BIN", "/opt/zapret2/nfq2/nfqws2");
const ZAPRET2_ROUTE_MARK_BASE = constant_value("ZAPRET2_ROUTE_MARK_BASE", "0x01010000");
const ZAPRET2_ROUTE_MARK_BASE = constant_value("ZAPRET2_ROUTE_MARK_BASE", "0x02000000");
const ZAPRET2_QUEUE_BASE = constant_value("ZAPRET2_QUEUE_BASE", "4300");
const ZAPRET2_DESYNC_MARK = constant_value("ZAPRET2_DESYNC_MARK", "0x40000000");
const ZAPRET2_DESYNC_MARK_POSTNAT = constant_value("ZAPRET2_DESYNC_MARK_POSTNAT", "0x20000000");
+1 -1
View File
@@ -31,7 +31,7 @@ const SERVICE_ACTION_SETTLE_SECONDS = getenv("TACHYON_UI_SERVICE_ACTION_SETTLE_S
const RUNTIME_STABLE_MIN_AGE = getenv("TACHYON_RUNTIME_STABLE_MIN_AGE") || "2";
const NFT_TABLE_NAME = getenv("NFT_TABLE_NAME") || "TachyonTable";
const RT_TABLE_NAME = getenv("RT_TABLE_NAME") || "tachyon";
const NFT_FAKEIP_MARK = getenv("NFT_FAKEIP_MARK") || "0x00100000";
const NFT_FAKEIP_MARK = getenv("NFT_FAKEIP_MARK") || "0x04000000";
const SB_DNS_INBOUND_ADDRESS = getenv("SB_DNS_INBOUND_ADDRESS") || "127.0.0.42";
const ZAPRET_PROVIDER_NFQWS_BIN = getenv("ZAPRET_PROVIDER_NFQWS_BIN") || "/opt/zapret/nfq/nfqws";
const ZAPRET2_PROVIDER_NFQWS2_BIN = getenv("ZAPRET2_PROVIDER_NFQWS2_BIN") || "/opt/zapret2/nfq2/nfqws2";
+2 -2
View File
@@ -21,7 +21,7 @@ const SERVICE_MIXED_INBOUND_ADDRESS = "127.0.0.1";
const SERVICE_MIXED_INBOUND_PORT = 4534;
const DIRECT_OUTBOUND_TAG = "direct-out";
const BYPASS_OUTBOUND_TAG = "bypass-out";
const OUTBOUND_MARK = 2097152;
const OUTBOUND_MARK = 134217728;
const FAKEIP_INET4_RANGE = "198.18.0.0/15";
const FAKEIP_INET6_RANGE = "fc00::/18";
@@ -32,7 +32,7 @@ const FAKEIP_TEST_DOMAIN = "fakeip.podkop.fyi";
const TMP_SING_BOX_FOLDER = "/tmp/sing-box";
const TMP_RULESET_FOLDER = TMP_SING_BOX_FOLDER + "/rulesets";
const ZAPRET_ROUTE_MARK_BASE = 0x01000000;
const ZAPRET2_ROUTE_MARK_BASE = 0x01010000;
const ZAPRET2_ROUTE_MARK_BASE = 0x02000000;
const BYEDPI_LISTEN_ADDRESS = "127.0.0.1";
const BYEDPI_PORT_BASE = 1080;
+1 -29
View File
@@ -498,7 +498,7 @@ function supported_subscription_outbound(outbound) {
if (t == "direct" || t == "selector" || t == "urltest" || t == "dns" || t == "block")
return false;
return t == "vless" || t == "vmess" || t == "trojan" || t == "shadowsocks" ||
t == "socks" || t == "http" || t == "hysteria2";
t == "socks" || t == "hysteria2";
}
function outbound_uses_xhttp(outbound) {
@@ -1747,32 +1747,6 @@ 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)
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);
@@ -2021,8 +1995,6 @@ 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")
+1 -1
View File
@@ -32,7 +32,7 @@ const NFT_IP_PORT6_SET_NAME = getenv("NFT_IP_PORT6_SET_NAME") || "tachyon_ip6_po
const NFT_INTERFACE_SET_NAME = getenv("NFT_INTERFACE_SET_NAME") || "tachyon_interfaces";
const NFT_LOCALV4_SET_NAME = getenv("NFT_LOCALV4_SET_NAME") || "localv4";
const NFT_LOCALV6_SET_NAME = getenv("NFT_LOCALV6_SET_NAME") || "localv6";
const NFT_FAKEIP_MARK = getenv("NFT_FAKEIP_MARK") || "0x00100000";
const NFT_FAKEIP_MARK = getenv("NFT_FAKEIP_MARK") || "0x04000000";
const SB_SERVICE_MIXED_INBOUND_ADDRESS = getenv("SB_SERVICE_MIXED_INBOUND_ADDRESS") || "127.0.0.1";
const SB_SERVICE_MIXED_INBOUND_PORT = getenv("SB_SERVICE_MIXED_INBOUND_PORT") || "4534";
const SB_VARIANT_STATE_FILE = getenv("SB_VARIANT_STATE_FILE") || "/etc/tachyon/sing-box-variant";
+15 -12
View File
@@ -292,9 +292,23 @@ function read_json_file(path) {
return data == null ? null : json_decode_text(data);
}
function reality_public_key_valid(value) {
return match(as_string(value), /^[A-Za-z0-9_-]{43}$/) != null;
}
function validate_subscription(path) {
let value = read_json_file(path);
return type(value) == "object" && type(value.outbounds) == "array" && length(value.outbounds) > 0;
if (type(value) != "object" || type(value.outbounds) != "array" || length(value.outbounds) == 0)
return false;
for (let outbound in value.outbounds) {
let tls = type(outbound) == "object" && type(outbound.tls) == "object" ? outbound.tls : {};
let reality = tls.reality;
if (type(reality) == "object" && reality.enabled !== false && !reality_public_key_valid(reality.public_key))
return false;
}
return true;
}
function write_json_file(path, value) {
@@ -1474,17 +1488,6 @@ 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;
}
@@ -0,0 +1,6 @@
#!/bin/sh
# Rebuild Tachyon nftables rules when firewall reloads
if /etc/init.d/tachyon status | grep -q "running"; then
logger -t tachyon "Firewall reload/restart detected. Restoring nftables rules."
/usr/bin/tachyon reload_firewall >/dev/null 2>&1
fi
+115 -7
View File
@@ -34,8 +34,12 @@ grep -Fq '{ id: "interface_sections", run: migrate_interface_sections }' "$MIGRA
fail "interface section migration must have a stable named marker"
grep -Fq '{ id: "enable_component_checks", run: migrate_enable_component_checks }' "$MIGRATION" ||
fail "component check migration must have a stable named marker"
grep -Fq '{ id: "http_connection_urls", run: migrate_http_connection_urls }' "$MIGRATION" ||
fail "HTTP connection URL migration must have a stable named marker"
grep -Fq 'release_at_most(ctx, "1.0.1")' "$MIGRATION" ||
fail "release-specific migrations must use the source release only as a condition"
grep -Fq 'release_at_most(ctx, "1.0.4")' "$MIGRATION" ||
fail "HTTP connection URLs must only migrate from Forkop 1.0.4 and below"
eval "$(ucode -L "$TACHYON_LIB" "$TACHYON_LIB/core/constants.uc" shell-env)"
export ZAPRET_LEGACY_DEFAULT_NFQWS_OPT ZAPRET_DEFAULT_NFQWS_OPT
@@ -169,9 +173,9 @@ const urltests = childByType('urltest');
assert(JSON.stringify(config.settings.dns_server) === JSON.stringify(['9.9.9.9']), 'legacy main DNS scalar migrated to ordered list');
assert(JSON.stringify(config.settings.bootstrap_dns_server) === JSON.stringify(['1.1.1.1']), 'legacy Bootstrap DNS scalar migrated to ordered list');
assert(config.settings.config_version === '1.0.2', 'legacy config should be marked at the current schema version');
assert(config.settings.config_version === '1.0.5', 'legacy config should be marked at the current schema version');
assert(config.settings.component_update_check_enabled === '1', 'component update checks should be enabled during migration');
assert(JSON.stringify(config.settings.applied_migrations) === JSON.stringify(['interface_sections', 'enable_component_checks']), 'legacy config should record named migrations');
assert(JSON.stringify(config.settings.applied_migrations) === JSON.stringify(['interface_sections', 'enable_component_checks', 'http_connection_urls']), 'legacy config should record named migrations');
function assert(condition, message) {
if (!condition) {
@@ -388,9 +392,9 @@ function assert(condition, message) {
}
assert(out.changed === true, '1.0.1 config should require migration');
assert(out.config.settings.config_version === '1.0.2', 'config schema version should advance to 1.0.2');
assert(out.config.settings.config_version === '1.0.5', 'config schema version should advance to 1.0.5');
assert(out.config.settings.component_update_check_enabled === '1', 'updates from 1.0.1 and below should enable component update checks');
assert(JSON.stringify(out.config.settings.applied_migrations) === JSON.stringify(['interface_sections', 'enable_component_checks']), 'named migrations should be recorded');
assert(JSON.stringify(out.config.settings.applied_migrations) === JSON.stringify(['interface_sections', 'enable_component_checks', 'http_connection_urls']), 'named migrations should be recorded');
assert(!Object.prototype.hasOwnProperty.call(section, 'interfaces'), 'parent interface list should be removed');
assert(JSON.stringify(interfaces.map(item => item.name)) === JSON.stringify(['awg0', 'tun0']), 'interfaces should keep their order');
for (const item of interfaces) {
@@ -427,7 +431,105 @@ function assert(condition, message) {
}
assert(out.config.settings.component_update_check_enabled === '0', '1.0.2 config must preserve an explicitly disabled component check');
assert(JSON.stringify(out.config.settings.applied_migrations) === JSON.stringify(['interface_sections', 'enable_component_checks']), 'newer configs should mark skipped migrations');
assert(out.config.settings.config_version === '1.0.5', '1.0.2 config should advance through the HTTP URL migration schema');
assert(JSON.stringify(out.config.settings.applied_migrations) === JSON.stringify(['interface_sections', 'enable_component_checks', 'http_connection_urls']), 'newer configs should mark skipped migrations');
NODE
cat >"$WORK_DIR/forkop-1.0.4-http.json" <<'JSON'
{
"settings": {
".name": "settings",
".type": "settings",
"config_version": "1.0.4"
},
"section": [
{
".name": "main",
".type": "section",
"action": "connection",
"selector_proxy_links": [
"https://user:p%2540ss@[2001:db8::1]:8443/#Secure%20HTTP",
"vless://00000000-0000-4000-8000-000000000001@example.com:443",
"http://proxy.example:8080",
"https://invalid.example/no-port"
],
"outbound_jsons": [
"{\"type\":\"direct\",\"tag\":\"http\"}"
]
}
]
}
JSON
TACHYON_LIB="$TACHYON_LIB" ucode -L "$TACHYON_LIB" "$MIGRATION" migrate-fixture "$WORK_DIR/forkop-1.0.4-http.json" >"$WORK_DIR/forkop-1.0.4-http-output.json"
node - "$WORK_DIR/forkop-1.0.4-http-output.json" <<'NODE'
const fs = require('fs');
const out = JSON.parse(fs.readFileSync(process.argv[2], 'utf8'));
const section = out.config.section[0];
const jsonOutbounds = section.outbound_jsons.map(JSON.parse);
function assert(condition, message) {
if (!condition) {
console.error(message);
process.exit(1);
}
}
assert(out.config.settings.config_version === '1.0.5', '1.0.4 config should advance to schema 1.0.5');
assert(JSON.stringify(section.selector_proxy_links) === JSON.stringify([
'vless://00000000-0000-4000-8000-000000000001@example.com:443',
'https://invalid.example/no-port',
]), 'only valid native HTTP proxy links should leave Connection URLs');
assert(jsonOutbounds.length === 3, 'migrated HTTP outbounds should precede the existing JSON list');
assert(JSON.stringify(jsonOutbounds[0]) === JSON.stringify({
type: 'http',
tag: 'Secure HTTP',
server: '2001:db8::1',
server_port: 8443,
username: 'user',
password: 'p@ss',
tls: { enabled: true },
}), 'HTTPS URL should preserve its name, credentials, IPv6 server, port, and TLS');
assert(JSON.stringify(jsonOutbounds[1]) === JSON.stringify({
type: 'http',
tag: 'http-1',
server: 'proxy.example',
server_port: 8080,
}), 'unnamed HTTP URL should receive a unique http tag');
assert(jsonOutbounds[2].type === 'direct' && jsonOutbounds[2].tag === 'http', 'existing JSON outbounds should remain unchanged');
assert(JSON.stringify(out.config.settings.applied_migrations) === JSON.stringify(['interface_sections', 'enable_component_checks', 'http_connection_urls']), 'HTTP URL migration should be recorded');
NODE
cat >"$WORK_DIR/forkop-1.0.5-http.json" <<'JSON'
{
"settings": {
".name": "settings",
".type": "settings",
"config_version": "1.0.5"
},
"section": [
{
".name": "main",
".type": "section",
"action": "connection",
"selector_proxy_links": [ "http://proxy.example:8080" ]
}
]
}
JSON
TACHYON_LIB="$TACHYON_LIB" ucode -L "$TACHYON_LIB" "$MIGRATION" migrate-fixture "$WORK_DIR/forkop-1.0.5-http.json" >"$WORK_DIR/forkop-1.0.5-http-output.json"
node - "$WORK_DIR/forkop-1.0.5-http-output.json" <<'NODE'
const fs = require('fs');
const out = JSON.parse(fs.readFileSync(process.argv[2], 'utf8'));
const section = out.config.section[0];
if (JSON.stringify(section.selector_proxy_links) !== JSON.stringify(['http://proxy.example:8080']) || section.outbound_jsons) {
console.error('1.0.5 and newer configs must not run the legacy HTTP URL conversion');
process.exit(1);
}
NODE
cat >"$WORK_DIR/runtime-migrate.state" <<'EOF_UCI'
@@ -498,6 +600,7 @@ tachyon.main=section
tachyon.main.enabled=1
tachyon.main.action=connection
tachyon.main.interfaces=awg0
tachyon.main.selector_proxy_links=http://proxy.example:8080
EOF_UCI
: >"$WORK_DIR/runtime-version.log"
TACHYON_UCI_STATE_FILE="$WORK_DIR/runtime-version.state" \
@@ -506,12 +609,17 @@ TACHYON_CONFIG_NAME="tachyon" \
TACHYON_INTERNAL_CONFIG_TRIGGER_GUARD="$WORK_DIR/internal-config-change" \
ucode -L "$TACHYON_LIB" "$MIGRATION" migrate
grep -Fxq 'tachyon.settings.config_version=1.0.2' "$WORK_DIR/runtime-version.state" ||
grep -Fxq 'tachyon.settings.config_version=1.0.5' "$WORK_DIR/runtime-version.state" ||
fail "runtime version migration must advance config_version"
grep -Fxq 'tachyon.settings.component_update_check_enabled=1' "$WORK_DIR/runtime-version.state" ||
fail "runtime version migration must enable component update checks"
grep -Fq 'tachyon.settings.applied_migrations=interface_sections enable_component_checks' "$WORK_DIR/runtime-version.state" ||
grep -Fq 'tachyon.settings.applied_migrations=interface_sections enable_component_checks http_connection_urls' "$WORK_DIR/runtime-version.state" ||
fail "runtime migration must record stable migration names"
grep -Eq '^tachyon\.main\.outbound_jsons=\{ "type": "http", "tag": "http", "server": "proxy\.example", "server_port": 8080 \}$' "$WORK_DIR/runtime-version.state" ||
fail "runtime migration must convert native HTTP proxy links to JSON outbounds"
if grep -Fq 'tachyon.main.selector_proxy_links=' "$WORK_DIR/runtime-version.state"; then
fail "runtime migration must remove converted HTTP connection URLs"
fi
grep -Eq '^tachyon\.cfg[0-9a-f]+=section_interface$' "$WORK_DIR/runtime-version.state" ||
fail "runtime version migration must create interface child settings"
if grep -Fq 'tachyon.main.interfaces=' "$WORK_DIR/runtime-version.state"; then
+46
View File
@@ -0,0 +1,46 @@
#!/usr/bin/env sh
# Capture current IP, netmask, and gateway of eth0 (Docker's configuration) safely
ETH0_IP=$(ip -o -4 addr show eth0 | head -n 1 | awk '{print $4}' | cut -d/ -f1)
ETH0_NETMASK_LEN=$(ip -o -4 addr show eth0 | head -n 1 | awk '{print $4}' | cut -d/ -f2)
GATEWAY=$(ip -4 route show default | head -n 1 | awk '{print $3}')
echo "=== Entrypoint: Captured IP=${ETH0_IP}/${ETH0_NETMASK_LEN}, Gateway=${GATEWAY} ==="
if [ -n "$ETH0_IP" ] && [ -n "$GATEWAY" ]; then
# Directly overwrite /etc/config/network with a clean static config on eth0
cat <<EOF > /etc/config/network
config interface 'loopback'
option device 'lo'
option proto 'static'
option ipaddr '127.0.0.1'
option netmask '255.0.0.0'
config globals 'globals'
option ula_prefix 'fd1d:8f1c:37e7::/48'
config interface 'lan'
option device 'eth0'
option proto 'static'
option ipaddr '${ETH0_IP}/${ETH0_NETMASK_LEN}'
option gateway '${GATEWAY}'
list dns '${GATEWAY}'
EOF
echo "=== Entrypoint: Network config written directly to /etc/config/network ==="
else
echo "=== Entrypoint: Failed to capture network settings, keeping default config ==="
fi
# Disable firewall service to avoid blocking rulesets
if [ -e /etc/init.d/firewall ]; then
/etc/init.d/firewall disable
fi
# Disable opkg signature check to bypass transient sig download failures
if [ -e /etc/opkg.conf ]; then
sed -i 's/option check_signature/# option check_signature/g' /etc/opkg.conf
echo "=== Entrypoint: Disabled opkg signature check ==="
fi
# Execute original OpenWrt init
exec /sbin/init
+21 -3
View File
@@ -117,7 +117,10 @@ mkdir -p "$fake_bin"
cat >"$fake_bin/curl" <<'SH'
#!/usr/bin/env sh
printf '%s\n' "$*" >>"$FAKE_CURL_LOG"
printf '%s\n' '{"delay":1}'
case "$*" in
*'/proxies'|*'/proxies '*) printf '%s\n' '{"proxies":{"urltest":{"type":"URLTest"},"provider-urltest":{"type":"urltest"},"proxy-a":{"type":"VLESS"},"proxy-b":{"type":"Trojan"}}}' ;;
*) printf '%s\n' '{"delay":1}' ;;
esac
SH
chmod +x "$fake_bin/curl"
uci_state="$WORK_DIR/uci-state.txt"
@@ -143,16 +146,31 @@ TACHYON_UCI_STATE_FILE="$uci_state" \
TACHYON_LIB="$TACHYON_LIB" \
TACHYON_UI_LATENCY_ACTION_DIR="$latency_action_dir" \
PATH="$fake_bin:$PATH" \
ucode -L "$TACHYON_LIB" "$DIAGNOSTICS_RUNTIME" clash-api get_proxy_latencies '["proxy-a","proxy-b"]' 5000 "$latency_state" >/dev/null ||
ucode -L "$TACHYON_LIB" "$DIAGNOSTICS_RUNTIME" clash-api get_proxy_latencies '["urltest","proxy-a","provider-urltest","proxy-b"]' 5000 "$latency_state" >/dev/null ||
fail "clash-api get_proxy_latencies should update latency progress"
JOB_STATE="$latency_state" node - <<'NODE'
const fs = require("fs");
const value = JSON.parse(fs.readFileSync(process.env.JOB_STATE, "utf8"));
if (!value.progress || value.progress.completed !== 2 || value.progress.total !== 2 || value.progress.failed !== 0) {
if (!value.progress || value.progress.completed !== 4 || value.progress.total !== 4 || value.progress.failed !== 0) {
console.error("latency progress after proxy list mismatch");
process.exit(1);
}
NODE
expected_latency_paths=(
'/proxies/proxy-a/delay'
'/proxies/proxy-b/delay'
'/group/urltest/delay'
'/group/provider-urltest/delay'
)
for index in "${!expected_latency_paths[@]}"; do
sed -n "$((index + 2))p" "$WORK_DIR/fake-curl-latencies.log" |
grep -Fq "${expected_latency_paths[$index]}" || {
echo "=== Content of fake-curl-latencies.log: ==="
cat "$WORK_DIR/fake-curl-latencies.log"
echo "=== End of Content ==="
fail "bulk latency must test ordinary proxies before URLTest groups"
}
done
firewall_rules="$(cat <<'EOF'
firewall.@rule[0]=rule
+156
View File
@@ -0,0 +1,156 @@
#!/usr/bin/env bash
set -eo pipefail
export MSYS_NO_PATHCONV=1
# Check if docker or docker.exe command is available
if command -v docker.exe >/dev/null 2>&1; then
DOCKER_BIN="docker.exe"
elif command -v docker >/dev/null 2>&1; then
DOCKER_BIN="docker"
else
echo "Docker is not available. Skipping E2E test."
exit 0
fi
# Determine script directory
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR/.."
if command -v cygpath >/dev/null 2>&1; then
ROOT_DIR="$(cygpath -w "$PWD")"
elif pwd -W >/dev/null 2>&1; then
ROOT_DIR="$(pwd -W)"
else
ROOT_DIR="$(pwd)"
fi
echo "=== Building Tachyon Package inside Ubuntu Container ==="
VERSION="1.0.0"
BUILD_OUT="$ROOT_DIR/build-out"
mkdir -p "$BUILD_OUT"
"$DOCKER_BIN" run --rm \
-v "$ROOT_DIR:/work" \
-w /work \
-e SDK_CACHE_DIR="/work/.wsl-build/sdk-cache" \
ubuntu:22.04 \
bash -c "apt-get update && apt-get install -y sudo && ./build.sh $VERSION /work/build-out"
echo "=== Starting OpenWrt Container ==="
CONTAINER_NAME="tachyon-e2e-test"
"$DOCKER_BIN" rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
# Run with privileged flag to allow nftables and networking modifications
"$DOCKER_BIN" run -d \
--name "$CONTAINER_NAME" \
--privileged \
-e SB_REQUIRED_VERSION="1.11.0" \
-v "$ROOT_DIR:/work" \
openwrt/rootfs:x86-64-23.05.5 \
/bin/sh /work/tests/container_entrypoint.sh
cleanup() {
echo "=== Container logs (stdout/stderr) ==="
"$DOCKER_BIN" logs "$CONTAINER_NAME" || true
echo "=== OpenWrt system logs ==="
"$DOCKER_BIN" exec "$CONTAINER_NAME" logread || true
echo "=== Keeping Container Alive for Debugging ==="
# "$DOCKER_BIN" rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
}
trap cleanup EXIT
echo "Waiting for OpenWrt to initialize..."
sleep 5
echo "=== Installing dependencies inside container ==="
for i in 1 2 3; do
echo "opkg update attempt $i..."
"$DOCKER_BIN" exec "$CONTAINER_NAME" opkg update && break || sleep 5
done
for i in 1 2 3 4 5; do
echo "opkg install attempt $i..."
"$DOCKER_BIN" exec "$CONTAINER_NAME" opkg install ucode ucode-mod-fs ucode-mod-uci curl ca-bundle bind-dig nftables ip-full coreutils-base64 sing-box bash node git git-http && break || sleep 5
done
"$DOCKER_BIN" exec "$CONTAINER_NAME" git config --global --add safe.directory '*' || true
echo "=== Installing official sing-box 1.12.0 binary ==="
for i in 1 2 3; do
echo "Downloading sing-box attempt $i..."
"$DOCKER_BIN" exec "$CONTAINER_NAME" curl -sSL -o /tmp/sing-box.tar.gz https://github.com/SagerNet/sing-box/releases/download/v1.12.0/sing-box-1.12.0-linux-amd64.tar.gz && break || sleep 5
done
"$DOCKER_BIN" exec "$CONTAINER_NAME" tar -zxf /tmp/sing-box.tar.gz -C /tmp
"$DOCKER_BIN" exec "$CONTAINER_NAME" cp /tmp/sing-box-1.12.0-linux-amd64/sing-box /usr/sbin/sing-box
"$DOCKER_BIN" exec "$CONTAINER_NAME" cp /tmp/sing-box-1.12.0-linux-amd64/sing-box /usr/bin/sing-box
"$DOCKER_BIN" exec "$CONTAINER_NAME" chmod +x /usr/sbin/sing-box /usr/bin/sing-box
"$DOCKER_BIN" exec "$CONTAINER_NAME" rm -rf /tmp/sing-box.tar.gz /tmp/sing-box-1.12.0-linux-amd64
echo "=== Installing Tachyon IPK ==="
"$DOCKER_BIN" exec "$CONTAINER_NAME" opkg install /work/build-out/tachyon_1.0.0.ipk
echo "=== Configuring Tachyon ==="
"$DOCKER_BIN" exec "$CONTAINER_NAME" uci set tachyon.settings.enabled='1'
"$DOCKER_BIN" exec "$CONTAINER_NAME" uci set tachyon.settings.log_level='debug'
# Create a test bypass section
"$DOCKER_BIN" exec "$CONTAINER_NAME" uci set tachyon.test_sec='section'
"$DOCKER_BIN" exec "$CONTAINER_NAME" uci set tachyon.test_sec.label='TestSection'
"$DOCKER_BIN" exec "$CONTAINER_NAME" uci set tachyon.test_sec.enabled='1'
"$DOCKER_BIN" exec "$CONTAINER_NAME" uci set tachyon.test_sec.action='bypass'
"$DOCKER_BIN" exec "$CONTAINER_NAME" uci add_list tachyon.test_sec.domain='ip.podkop.fyi'
"$DOCKER_BIN" exec "$CONTAINER_NAME" uci commit tachyon
echo "=== Starting services ==="
"$DOCKER_BIN" exec "$CONTAINER_NAME" /etc/init.d/sing-box enable
"$DOCKER_BIN" exec "$CONTAINER_NAME" /etc/init.d/sing-box start
"$DOCKER_BIN" exec "$CONTAINER_NAME" /etc/init.d/tachyon enable
"$DOCKER_BIN" exec "$CONTAINER_NAME" /etc/init.d/tachyon start
echo "Waiting for services to stabilize..."
sleep 5
echo "=== Checking running services ==="
"$DOCKER_BIN" exec "$CONTAINER_NAME" /etc/init.d/tachyon status || echo "Tachyon status non-zero, continuing..."
"$DOCKER_BIN" exec "$CONTAINER_NAME" pgrep -l sing-box || echo "sing-box process not found"
echo "=== Running connection test via mixed port ==="
# Test routing of the proxy port
PROXY_TEST_HTTP_CODE=$("$DOCKER_BIN" exec "$CONTAINER_NAME" curl -s -o /dev/null -w "%{http_code}" -x http://127.0.0.1:4534 --connect-timeout 10 http://ip.podkop.fyi || echo "failed")
echo "HTTP proxy status: $PROXY_TEST_HTTP_CODE"
echo "=== Running transparent interception test ==="
# Running direct curl to test transparent proxying via nftables
"$DOCKER_BIN" exec "$CONTAINER_NAME" curl -s -I --connect-timeout 10 http://ip.podkop.fyi || echo "curl failed"
echo "=== Checking system logs ==="
LOGS=$("$DOCKER_BIN" exec "$CONTAINER_NAME" logread)
echo "$LOGS" | tail -n 100
echo "=== Checking nftables rules ==="
"$DOCKER_BIN" exec "$CONTAINER_NAME" nft list ruleset || echo "nft failed"
# Verification: check if logs contain trace of bypass or redirect
if echo "$LOGS" | grep -Fiq "ip.podkop.fyi"; then
echo "SUCCESS: Traffic to ip.podkop.fyi was intercepted/processed!"
else
echo "WARNING: Domain ip.podkop.fyi not found in system logs. Checking sing-box status."
if "$DOCKER_BIN" exec "$CONTAINER_NAME" pgrep -f sing-box >/dev/null; then
echo "sing-box is running, routing table was loaded."
else
echo "FAIL: sing-box is not running."
exit 1
fi
fi
echo "=== Normalizing line endings for test scripts ==="
"$DOCKER_BIN" exec "$CONTAINER_NAME" sh -c "sed -i 's/\r$//' /work/tests/*.sh"
echo "=== Running unit and integration tests inside OpenWrt container ==="
for f in tests/*.sh; do
if [ "$f" != "tests/docker_e2e_test.sh" ] && [ "$f" != "tests/container_entrypoint.sh" ]; then
echo "Running $f inside container..."
"$DOCKER_BIN" exec -w /work -e SB_REQUIRED_VERSION="1.11.0" "$CONTAINER_NAME" bash "$f"
fi
done
echo "=== E2E and Unit Tests Passed Successfully ==="
+6 -2
View File
@@ -90,6 +90,8 @@ fi
pending_file="$WORK_DIR/reload.pending"
if TACHYON_PENDING_RELOAD_FILE="$pending_file" \
TACHYON_INTERNAL_CONFIG_TRIGGER_GUARD="$guard_file" \
TACHYON_CONFIG_FILE="$config_file" \
initd_ucode reload-begin-fixture on_config_change 123 0 1 start >/dev/null 2>&1; then
fail "queued config-change reload should not run immediately while service is starting"
fi
@@ -100,6 +102,8 @@ grep -Fq 'reason=on_config_change' "$pending_file" ||
rm -f "$pending_file"
if TACHYON_PENDING_RELOAD_FILE="$pending_file" \
TACHYON_INTERNAL_CONFIG_TRIGGER_GUARD="$guard_file" \
TACHYON_CONFIG_FILE="$config_file" \
initd_ucode reload-begin-fixture pending 123 1 1 start >/dev/null 2>&1; then
fail "pending reload should not run while another service action is active"
fi
@@ -243,8 +247,8 @@ grep -Fq 'schedule_start_retry(START_RETRY_PID_FILE, START_RETRY_DELAY_SECONDS)'
fail "failed service start must schedule a retry even if WAN is already up"
grep -Fq 'start_retry_pending(START_RETRY_FILE)' "$INITD_UC" ||
fail "WAN retry must be gated by a failed-start marker"
service_enabled_line="$(grep -nF 'function service_is_enabled()' "$INITD_UC" | head -n1 | cut -d: -f1)"
retry_start_line="$(grep -nF 'function retry_start_on_wan_up(' "$INITD_UC" | head -n1 | cut -d: -f1)"
service_enabled_line="$(awk '/function service_is_enabled\(\)/ { print NR; exit }' "$INITD_UC")"
retry_start_line="$(awk '/function retry_start_on_wan_up\(/ { print NR; exit }' "$INITD_UC")"
[ -n "$service_enabled_line" ] && [ -n "$retry_start_line" ] && [ "$service_enabled_line" -lt "$retry_start_line" ] ||
fail "service_is_enabled must be declared before retry_start_on_wan_up for ucode runtime calls"
grep -Fq 'trigger-plan' "$INITD" ||
+2
View File
@@ -374,6 +374,7 @@ cat >"$WORK_DIR/legacy-bin" <<'SH'
printf '%s\n' "$*" >> "$TACHYON_INSTALLER_BIN_LOG"
case "$1" in
get_status) printf '%s\n' '{"running":1}' ;;
restore_dnsmasq) exit "${TACHYON_INSTALLER_FAIL_BACKEND_RESTORE:-0}" ;;
esac
exit 0
SH
@@ -405,6 +406,7 @@ TACHYON_INSTALLER_FAKE_LEGACY_BACKEND="$LEGACY_BACKEND" \
TACHYON_INSTALLER_OPKG_LOG="$WORK_DIR/opkg.log" \
TACHYON_INSTALLER_INIT_LOG="$WORK_DIR/init.log" \
TACHYON_INSTALLER_BIN_LOG="$WORK_DIR/bin.log" \
TACHYON_INSTALLER_FAIL_BACKEND_RESTORE=1 \
TACHYON_UCI_STATE_FILE="$WORK_DIR/empty-uci.state" \
ucode "$helper" installer-cleanup-legacy >"$state"
+4 -4
View File
@@ -71,12 +71,12 @@ grep -Fq '"dashboard_include_groups"' <<<"$dashboard_options" ||
fail "dashboard include filters must support URLTest and Priority groups"
grep -Fq '"dashboard_exclude_groups"' <<<"$dashboard_options" ||
fail "dashboard exclude filters must support URLTest and Priority groups"
include_group_line="$(grep -n '"dashboard_include_groups"' <<<"$dashboard_options" | head -n1 | cut -d: -f1)"
include_proxy_line="$(grep -n 'includeProxyParameterOptions' <<<"$dashboard_options" | tail -n1 | cut -d: -f1)"
include_group_line="$(awk '/"dashboard_include_groups"/ { print NR; exit }' <<<"$dashboard_options")"
include_proxy_line="$(awk '/includeProxyParameterOptions/ { last = NR } END { print last }' <<<"$dashboard_options")"
[[ "$include_group_line" -lt "$include_proxy_line" ]] ||
fail "dashboard include group selector must precede the proxy-parameter toggle"
exclude_group_line="$(grep -n '"dashboard_exclude_groups"' <<<"$dashboard_options" | head -n1 | cut -d: -f1)"
exclude_proxy_line="$(grep -n 'excludeProxyParameterOptions' <<<"$dashboard_options" | tail -n1 | cut -d: -f1)"
exclude_group_line="$(awk '/"dashboard_exclude_groups"/ { print NR; exit }' <<<"$dashboard_options")"
exclude_proxy_line="$(awk '/excludeProxyParameterOptions/ { last = NR } END { print last }' <<<"$dashboard_options")"
[[ "$exclude_group_line" -lt "$exclude_proxy_line" ]] ||
fail "dashboard exclude group selector must precede the proxy-parameter toggle"
grep -Fq 'const liveValues = currentLiveDynamicListValues(section_id, typeName);' "$SECTION_JS" ||
+10
View File
@@ -5,6 +5,9 @@ ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
SECTION_JS="$ROOT_DIR/luci-app-tachyon/htdocs/luci-static/resources/view/tachyon/section.js"
SOURCE_PO="$ROOT_DIR/fe-app-tachyon/locales/tachyon.ru.po"
PACKAGE_PO="$ROOT_DIR/luci-app-tachyon/po/ru/tachyon.po"
SOURCE_POT="$ROOT_DIR/fe-app-tachyon/locales/tachyon.pot"
PACKAGE_POT="$ROOT_DIR/luci-app-tachyon/po/templates/tachyon.pot"
CALLS_JSON="$ROOT_DIR/fe-app-tachyon/locales/calls.json"
fail() {
printf 'FAIL: %s\n' "$1" >&2
@@ -17,6 +20,13 @@ fi
grep -Fq '_("Close")' "$SECTION_JS" ||
fail "Tachyon section settings modal must expose a Close action"
if grep -Fq 'http(s)://, hy2/hysteria2:// links' \
"$SECTION_JS" "$SOURCE_PO" "$PACKAGE_PO" "$SOURCE_POT" "$PACKAGE_POT" "$CALLS_JSON"; then
fail "Connection URL help must not advertise removed HTTP proxy links"
fi
grep -Fq 'socks4/5://, hy2/hysteria2:// links' "$SECTION_JS" ||
fail "Connection URL help must list the remaining native proxy links"
for po in "$SOURCE_PO" "$PACKAGE_PO"; do
awk '
$0 == "msgid \"Close\"" {
+18 -4
View File
@@ -44,10 +44,16 @@ assert_mark_range_no_overlap() {
eval "$(ucode -L "$TACHYON_LIB" "$TACHYON_LIB/core/constants.uc" shell-env)"
TAILSCALE_FWMARK_MASK=0x00ff0000
assert_no_overlap "FakeIP" "$((NFT_FAKEIP_MARK))" "$((TAILSCALE_FWMARK_MASK))" "Tailscale"
assert_no_overlap "outbound" "$((NFT_OUTBOUND_MARK))" "$((TAILSCALE_FWMARK_MASK))" "Tailscale"
assert_mark_range_no_overlap "Zapret" "$((ZAPRET_ROUTE_MARK_BASE))" "$ZAPRET_QUEUE_RANGE_SIZE" "$((NFT_FAKEIP_MARK))" "FakeIP"
assert_mark_range_no_overlap "Zapret" "$((ZAPRET_ROUTE_MARK_BASE))" "$ZAPRET_QUEUE_RANGE_SIZE" "$((NFT_OUTBOUND_MARK))" "outbound"
assert_mark_range_no_overlap "Zapret2" "$((ZAPRET2_ROUTE_MARK_BASE))" "$ZAPRET2_QUEUE_RANGE_SIZE" "$((NFT_FAKEIP_MARK))" "FakeIP"
assert_mark_range_no_overlap "Zapret2" "$((ZAPRET2_ROUTE_MARK_BASE))" "$ZAPRET2_QUEUE_RANGE_SIZE" "$((NFT_OUTBOUND_MARK))" "outbound"
assert_mark_range_no_overlap "Zapret" "$((ZAPRET_ROUTE_MARK_BASE))" "$ZAPRET_QUEUE_RANGE_SIZE" "$((TAILSCALE_FWMARK_MASK))" "Tailscale"
assert_mark_range_no_overlap "Zapret2" "$((ZAPRET2_ROUTE_MARK_BASE))" "$ZAPRET2_QUEUE_RANGE_SIZE" "$((TAILSCALE_FWMARK_MASK))" "Tailscale"
cat >"$WORK_DIR/fixture.json" <<'JSON'
{
@@ -57,6 +63,8 @@ JSON
context_json() {
local zapret2_base="${1:-$ZAPRET2_ROUTE_MARK_BASE}"
local fakeip_mark="${2:-$NFT_FAKEIP_MARK}"
local outbound_mark="${3:-$NFT_OUTBOUND_MARK}"
cat <<JSON
{
@@ -72,16 +80,22 @@ context_json() {
"zapret_queue_range_size": "$ZAPRET_QUEUE_RANGE_SIZE",
"zapret2_route_mark_base": "$zapret2_base",
"zapret2_queue_range_size": "$ZAPRET2_QUEUE_RANGE_SIZE",
"nft_fakeip_mark": "$NFT_FAKEIP_MARK",
"nft_outbound_mark": "$NFT_OUTBOUND_MARK"
"nft_fakeip_mark": "$fakeip_mark",
"nft_outbound_mark": "$outbound_mark"
}
JSON
}
TACHYON_LIB="$TACHYON_LIB" ucode -L "$TACHYON_LIB" "$VALIDATOR" validate-runtime-fixture "$WORK_DIR/fixture.json" "$(context_json)"
if TACHYON_LIB="$TACHYON_LIB" ucode -L "$TACHYON_LIB" "$VALIDATOR" validate-runtime-fixture "$WORK_DIR/fixture.json" "$(context_json "0x01100000")" >/dev/null 2>&1; then
fail "legacy Zapret2 route mark base should overlap FakeIP mark"
if TACHYON_LIB="$TACHYON_LIB" ucode -L "$TACHYON_LIB" "$VALIDATOR" validate-runtime-fixture "$WORK_DIR/fixture.json" "$(context_json "0x01010000")" >/dev/null 2>&1; then
fail "legacy Zapret2 route mark base should overlap Tailscale fwmark mask"
fi
if TACHYON_LIB="$TACHYON_LIB" ucode -L "$TACHYON_LIB" "$VALIDATOR" validate-runtime-fixture "$WORK_DIR/fixture.json" "$(context_json "" "0x00100000")" >/dev/null 2>&1; then
fail "legacy FakeIP mark should overlap Tailscale fwmark mask"
fi
if TACHYON_LIB="$TACHYON_LIB" ucode -L "$TACHYON_LIB" "$VALIDATOR" validate-runtime-fixture "$WORK_DIR/fixture.json" "$(context_json "" "" "0x00200000")" >/dev/null 2>&1; then
fail "legacy outbound mark should overlap Tailscale fwmark mask"
fi
printf 'mark range checks passed\n'
+4 -3
View File
@@ -52,8 +52,8 @@ assert_line_before() {
local first_line
local second_line
first_line="$(grep -Fn "$first" "$file" | head -n 1 | cut -d: -f1 || true)"
second_line="$(grep -Fn "$second" "$file" | head -n 1 | cut -d: -f1 || true)"
first_line="$(awk -v pat="$first" 'index($0, pat) { print NR; exit }' "$file")"
second_line="$(awk -v pat="$second" 'index($0, pat) { print NR; exit }' "$file")"
[ -n "$first_line" ] || fail "$label: missing first line '$first'"
[ -n "$second_line" ] || fail "$label: missing second line '$second'"
[ "$first_line" -lt "$second_line" ] || fail "$label: expected first line before second"
@@ -223,9 +223,10 @@ assert_contains "$NFT_LOG" '::/128,::1/128,64:ff9b::/96' "runtime localv6 elemen
assert_contains "$NFT_LOG" $'nft\tadd\tset\tinet\tTachyonTable\ttachyon_interfaces\t{ type ifname; flags interval; }' "runtime interface set"
assert_contains "$NFT_LOG" $'nft\tadd\telement\tinet\tTachyonTable\ttachyon_interfaces\t{ br-lan }' "runtime br-lan interface"
assert_contains "$NFT_LOG" $'nft\tadd\telement\tinet\tTachyonTable\ttachyon_interfaces\t{ tun0 }' "runtime tun0 interface"
assert_contains "$NFT_LOG" $'nft\tadd\tchain\tinet\tTachyonTable\tmangle\t{ type filter hook prerouting priority -150; policy accept; }' "runtime mangle chain"
assert_contains "$NFT_LOG" $'nft\tadd\tchain\tinet\tTachyonTable\tmangle\t{ type filter hook prerouting priority -149; policy accept; }' "runtime mangle chain runs after Tailscale connmark restore"
assert_contains "$NFT_LOG" $'nft\tadd\tchain\tinet\tTachyonTable\tpriority_rules\t{ }' "runtime priority chain"
assert_contains "$NFT_LOG" $'nft\tadd\tchain\tinet\tTachyonTable\tpriority_output_rules\t{ }' "runtime priority output chain"
assert_contains "$NFT_LOG" $'nft\tadd\trule\tinet\tTachyonTable\tpriority_output_rules\tmeta\tmark\t!=\t0\treturn' "runtime priority output preserves provider marks"
assert_contains "$NFT_LOG" $'nft\tadd\trule\tinet\tTachyonTable\tmangle\tjump\tpriority_rules' "runtime priority jump"
assert_contains "$NFT_LOG" $'nft\tadd\trule\tinet\tTachyonTable\tmangle\tiifname\t@tachyon_interfaces\tip\tdaddr\t@tachyon_subnets\tmeta\tl4proto\ttcp\tmeta\tmark\tset\t0x00100000\tcounter' "runtime common tcp rule"
assert_contains "$NFT_LOG" $'nft\tadd\trule\tinet\tTachyonTable\tmangle\tiifname\t@tachyon_interfaces\tip6\tdaddr\t@tachyon_subnets6\tmeta\tl4proto\ttcp\tmeta\tmark\tset\t0x00100000\tcounter' "runtime common6 tcp rule"
+3 -1
View File
@@ -64,12 +64,14 @@ grep -Fq 'APK_INTERNAL_VERSION="$RELEASE_VERSION"' "$BUILD_SCRIPT" ||
fail "build.sh must use the exact three-part release version for APK metadata"
grep -Fq "option component_update_check_enabled '1'" "$TACHYON_CONFIG" ||
fail "new installations must enable component update checks by default"
grep -Fq "option config_version '1.0.2'" "$TACHYON_CONFIG" ||
grep -Fq "option config_version '1.0.5'" "$TACHYON_CONFIG" ||
fail "new installations must start at the current configuration schema version"
grep -Fq "list applied_migrations 'interface_sections'" "$TACHYON_CONFIG" ||
fail "new installations must mark the interface section migration as applied"
grep -Fq "list applied_migrations 'enable_component_checks'" "$TACHYON_CONFIG" ||
fail "new installations must mark the component check migration as applied"
grep -Fq "list applied_migrations 'http_connection_urls'" "$TACHYON_CONFIG" ||
fail "new installations must mark the HTTP connection URL migration as applied"
grep -Fq '/usr/lib/tachyon/config/migration.uc migrate' "$TACHYON_MAKEFILE" ||
fail "OpenWrt package postinst must run configuration migrations"
[ "$(grep -Fc '/usr/lib/tachyon/config/migration.uc migrate' "$BUILD_SCRIPT")" -ge 3 ] ||
+5 -1
View File
@@ -131,8 +131,12 @@ TACHYON_SING_BOX_BIN="$WORK_DIR/missing-sing-box-bin" \
TACHYON_SING_BOX_CRONET="$WORK_DIR/missing-cronet" \
TACHYON_RT_TABLES="$WORK_DIR/rt_tables_dont_touch" \
ucode -L "$TACHYON_LIB" "$PACKAGE_UC" prerm
[ ! -s "$WORK_DIR/restore-dont-touch.log" ] ||
if grep -Fxq 'restore_dnsmasq' "$WORK_DIR/restore-dont-touch.log"; then
echo "=== Content of restore-dont-touch.log: ==="
cat "$WORK_DIR/restore-dont-touch.log"
echo "=== End of Content ==="
fail "package prerm must skip dnsmasq restore when dont_touch_dhcp is enabled"
fi
cat >"$WORK_DIR/restore.state" <<'EOF_UCI'
tachyon.settings=settings
+1 -1
View File
@@ -7,7 +7,7 @@ find tachyon/files/usr/lib -name "*.uc" -print0 | xargs -0 -n1 ucode -S -c -o /d
echo "=== Running Tests ==="
for f in tests/*.sh; do
if [ "$f" != "tests/run_all.sh" ]; then
if [ "$f" != "tests/run_all.sh" ] && [ "$f" != "tests/docker_e2e_test.sh" ] && [ "$f" != "tests/container_entrypoint.sh" ]; then
echo "Running: $f"
bash "$f"
fi
+3 -3
View File
@@ -87,10 +87,10 @@ grep -Fq '"wait-tachyon-stable-start"' "$LIFECYCLE_UC" ||
fail "service/lifecycle.uc must verify stable runtime after sing-box start/reload"
grep -Fq 'SING_BOX_START_STABLE_MIN_AGE' "$LIFECYCLE_UC" ||
fail "service/lifecycle.uc must use a dedicated sing-box start stability window"
sing_box_start_line="$(grep -nF 'command_success_from_args([ "/etc/init.d/sing-box", "start" ])' "$LIFECYCLE_UC" | head -n1 | cut -d: -f1)"
sing_box_start_line="$(awk '/command_success_from_args\(\[ "\/etc\/init.d\/sing-box", "start" \]\)/ { print NR; exit }' "$LIFECYCLE_UC")"
early_stable_line="$(awk -v start="$sing_box_start_line" 'NR > start && /"wait-tachyon-stable-start"/ { print NR; exit }' "$LIFECYCLE_UC")"
start_stable_min_age_line="$(awk -v start="$sing_box_start_line" 'NR > start && /SING_BOX_START_STABLE_MIN_AGE/ { print NR; exit }' "$LIFECYCLE_UC")"
deferred_bootstrap_line="$(grep -nF '"run-deferred-bootstrap"' "$LIFECYCLE_UC" | head -n1 | cut -d: -f1)"
deferred_bootstrap_line="$(awk '/"run-deferred-bootstrap"/ { print NR; exit }' "$LIFECYCLE_UC")"
[ -n "$sing_box_start_line" ] ||
fail "service/lifecycle.uc must start sing-box through init.d"
[ -n "$early_stable_line" ] ||
@@ -99,7 +99,7 @@ deferred_bootstrap_line="$(grep -nF '"run-deferred-bootstrap"' "$LIFECYCLE_UC" |
fail "service/lifecycle.uc must verify sing-box stability before deferred bootstrap work"
[ -n "$start_stable_min_age_line" ] && [ "$start_stable_min_age_line" -lt "$deferred_bootstrap_line" ] ||
fail "service/lifecycle.uc must use the dedicated sing-box start stability window before deferred bootstrap work"
sing_box_reload_line="$(grep -nF '"reload-sing-box-runtime"' "$LIFECYCLE_UC" | head -n1 | cut -d: -f1)"
sing_box_reload_line="$(awk '/"reload-sing-box-runtime"/ { print NR; exit }' "$LIFECYCLE_UC")"
reload_stable_min_age_line="$(awk -v start="$sing_box_reload_line" 'NR > start && /SING_BOX_START_STABLE_MIN_AGE/ { print NR; exit }' "$LIFECYCLE_UC")"
[ -n "$sing_box_reload_line" ] ||
fail "service/lifecycle.uc must reload sing-box through service/state.uc"
+2 -2
View File
@@ -144,7 +144,7 @@ if sed -n '/^function process_age_seconds(pid) {$/,/^}$/p' "$STATE_UC" | grep -F
fi
mkdir -p "$WORK_DIR/stable-start-bin"
cp "$(command -v sleep)" "$WORK_DIR/stable-start-bin/sing-box"
cp "$(command -v ucode)" "$WORK_DIR/stable-start-bin/sing-box"
cat >"$WORK_DIR/stable-start-bin/ubus" <<'SH'
#!/usr/bin/env bash
set -eo pipefail
@@ -169,7 +169,7 @@ exec "${REAL_UCODE:?}" "$@"
SH
chmod 0755 "$WORK_DIR/stable-start-bin/ubus" "$WORK_DIR/stable-start-bin/nft" "$WORK_DIR/stable-start-bin/ucode"
"$WORK_DIR/stable-start-bin/sing-box" 30 &
"$WORK_DIR/stable-start-bin/sing-box" -e "system('sleep 30')" &
sing_box_pid=$!
printf '%s\n' "$sing_box_pid" >"$WORK_DIR/sing-box.pid"
if ! PATH="$WORK_DIR/stable-start-bin:$PATH" \
+1 -1
View File
@@ -22,7 +22,7 @@ lifecycle_ucode() {
}
json_flatten() {
tr -d '[:space:]'
tr -d ' \t\r\n'
}
cat >"$WORK_DIR/before.json" <<'JSON'
+2 -2
View File
@@ -76,8 +76,8 @@ require_pattern 'function abort_reload(status, runtime_changed)' \
"reload failures must share one cleanup decision owner"
require_pattern 'return abort_reload(status, true);' \
"reload failures after runtime mutation must clean the partial runtime"
cleanup_function_line="$(grep -nF 'function cleanup_failed_runtime()' "$LIFECYCLE_UC" | head -n1 | cut -d: -f1)"
abort_function_line="$(grep -nF 'function abort_reload(status, runtime_changed)' "$LIFECYCLE_UC" | head -n1 | cut -d: -f1)"
cleanup_function_line="$(awk '/function cleanup_failed_runtime\(\)/ { print NR; exit }' "$LIFECYCLE_UC")"
abort_function_line="$(awk '/function abort_reload\(status, runtime_changed\)/ { print NR; exit }' "$LIFECYCLE_UC")"
[ -n "$cleanup_function_line" ] && [ -n "$abort_function_line" ] &&
[ "$cleanup_function_line" -lt "$abort_function_line" ] ||
fail "cleanup_failed_runtime must be declared before abort_reload for ucode runtime calls"
+2 -1
View File
@@ -45,7 +45,8 @@ expected_shell_scripts="$(
printf '%s\n' \
'luci-app-tachyon/root/etc/uci-defaults/50_luci-tachyon' \
'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/tachyon/firewall.include' |
LC_ALL=C sort
)"
+38 -5
View File
@@ -421,9 +421,9 @@ cat >"$WORK_DIR/urltest-filter-fixture.json" <<'JSON'
".type": "section",
"enabled": "1",
"action": "proxy",
"selector_proxy_links": [
"http://127.0.0.1:18081#Keep",
"http://127.0.0.1:18082#Drop"
"outbound_jsons": [
"{\"type\":\"http\",\"tag\":\"Keep\",\"server\":\"127.0.0.1\",\"server_port\":18081}",
"{\"type\":\"http\",\"tag\":\"Drop\",\"server\":\"127.0.0.1\",\"server_port\":18082}"
],
"urltest_enabled": "1",
"urltest_filter_mode": "include",
@@ -434,6 +434,25 @@ cat >"$WORK_DIR/urltest-filter-fixture.json" <<'JSON'
}
JSON
cat >"$WORK_DIR/manual-http-fixture.json" <<'JSON'
{
"settings": {
".name": "settings",
".type": "settings",
"dns_server": "1.1.1.1"
},
"section": [
{
".name": "proxy",
".type": "section",
"enabled": "1",
"action": "connection",
"selector_proxy_links": [ "http://127.0.0.1:8080" ]
}
]
}
JSON
cat >"$WORK_DIR/provider-actions-fixture.json" <<'JSON'
{
"settings": {
@@ -908,6 +927,12 @@ cat >"$WORK_DIR/subscriptions/grouped-subscription-1.json" <<'JSON'
{
"type": "direct",
"tag": "provider-direct"
},
{
"type": "http",
"tag": "provider-http",
"server": "127.0.0.3",
"server_port": 8080
}
]
}
@@ -989,6 +1014,12 @@ 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/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"
@@ -1213,12 +1244,13 @@ assert(index(sprintf("%J", dns_list), "198.51.100.0/24") < 0, "DNS action drops
let urltest = cfg("urltest");
let urltest_out = outbound(urltest, "proxy-urltest-out");
assert(urltest_out && length(urltest_out.outbounds) == 1 && urltest_out.outbounds[0] == "proxy-1-out", "URLTest include filter");
assert(urltest_out && length(urltest_out.outbounds) == 1 && urltest_out.outbounds[0] == "Keep", "URLTest include filter");
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 providers = cfg("providers");
assert(outbound(providers, "zap-out").routing_mark == 0x01000001, "Zapret mark");
assert(outbound(providers, "zap2-out").routing_mark == 0x01010001, "Zapret2 mark");
assert(outbound(providers, "zap2-out").routing_mark == 0x02000001, "Zapret2 mark");
assert(outbound(providers, "bye-out").type == "socks" && outbound(providers, "bye-out").server_port == 1080, "ByeDPI outbound");
let manual = cfg("manual");
@@ -1297,6 +1329,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");
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");
+18
View File
@@ -67,6 +67,18 @@ assert_alpn() {
UUID='00000000-0000-4000-8000-000000000001'
BASE_QUERY='encryption=none&security=tls&fp=chrome&alpn=h2%2Chttp%2F1.1&sni=example.com'
cat >"$WORK_DIR/valid-reality.json" <<'JSON'
{"outbounds":[{"type":"vless","tag":"valid-reality","tls":{"enabled":true,"reality":{"enabled":true,"public_key":"jNXHt1yRo0vDuchQlIP6Z0ZvjT3KtzVI-T4E7RoLJS0"}}}]}
JSON
ucode "$PARSER" validate-subscription "$WORK_DIR/valid-reality.json" ||
fail "valid REALITY public key must pass subscription validation"
cat >"$WORK_DIR/invalid-reality.json" <<'JSON'
{"outbounds":[{"type":"vless","tag":"invalid-reality","tls":{"enabled":true,"reality":{"enabled":true,"public_key":"abc"}}}]}
JSON
if ucode "$PARSER" validate-subscription "$WORK_DIR/invalid-reality.json"; then
fail "invalid REALITY public key must fail before cache promotion"
fi
ws_output="$(normalize_link "vless-ws" "vless://$UUID@example.com:443?type=ws&$BASE_QUERY&path=%2Fws#vless-ws")"
assert_contains "$ws_output" '"transport": { "type": "ws"' "vless-ws"
assert_contains "$ws_output" '"alpn": [ "http/1.1" ]' "vless-ws"
@@ -117,10 +129,16 @@ proxies:
alpn: [h2, http/1.1]
ws-opts:
path: /ws
- name: clash-http
type: http
server: proxy.example
port: 8080
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"
xray_input="$WORK_DIR/xray.json"
xray_output="$WORK_DIR/xray-normalized.json"
+2 -1
View File
@@ -215,7 +215,8 @@ ui_ucode cleanup-action-dir-fixture "$cleanup_dir"
old_state="$cleanup_dir/old.json"
printf '%s\n' '{"running":false}' >"$old_state"
touch -d '2 hours ago' "$old_state"
two_hours_ago="$(( $(date +%s) - 7200 ))"
touch -d "@$two_hours_ago" "$old_state"
TACHYON_UI_ACTION_FINISHED_TTL_MINUTES=60 ui_ucode cleanup-action-dir-fixture "$cleanup_dir"
[ ! -e "$old_state" ] ||
fail "old finished action state should be cleaned without find"
+6
View File
@@ -86,6 +86,12 @@ assert_eq "0 0 * * *" \
assert_eq "*/30 * * * * /usr/bin/tachyon list_update_if_due # list" \
"$(updates_ucode list-update-cron-job 30m /usr/bin/tachyon '# list')" \
"list update cron job"
assert_eq $'https://raw.githubusercontent.com/itdoginfo/allow-domains/main/Subnets/IPv4/telegram.lst\nhttps://raw.githubusercontent.com/itdoginfo/allow-domains/main/Subnets/IPv6/telegram.lst' \
"$(updates_ucode builtin-subnet-urls telegram)" \
"Telegram built-in subnet families"
assert_eq 'https://raw.githubusercontent.com/itdoginfo/allow-domains/main/Subnets/IPv4/roblox.lst' \
"$(updates_ucode builtin-subnet-urls roblox)" \
"Roblox available subnet family"
assert_eq "0 */2 * * * /usr/bin/tachyon subscription_update_if_due # subscription" \
"$(updates_ucode subscription-update-cron-job 7200 /usr/bin/tachyon '# subscription')" \
"subscription update cron job"