feat(subscription): add crypt4 decryption, dual HWID headers, and diagnosis symptom collapsing

This commit is contained in:
Dushnilin
2026-09-17 18:04:23 +03:00
parent e610ea9ca3
commit b556f2539b
9 changed files with 671 additions and 62 deletions
+2 -2
View File
@@ -7,8 +7,8 @@ msgid ""
msgstr ""
"Project-Id-Version: TACHYON\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-09-17 12:45+0300\n"
"PO-Revision-Date: 2026-09-17 12:45+0300\n"
"POT-Creation-Date: 2026-09-17 15:04+0300\n"
"PO-Revision-Date: 2026-09-17 15:04+0300\n"
"Last-Translator: Dushnilin <dushnilin@gmail.com>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
+11 -11
View File
@@ -7,8 +7,8 @@ msgid ""
msgstr ""
"Project-Id-Version: TACHYON\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-09-17 15:45+0300\n"
"PO-Revision-Date: 2026-09-17 15:45+0300\n"
"POT-Creation-Date: 2026-09-17 18:04+0300\n"
"PO-Revision-Date: 2026-09-17 18:04+0300\n"
"Last-Translator: Dushnilin\n"
"Language-Team: none\n"
"Language: ru\n"
@@ -273,10 +273,10 @@ msgid "Add URLs or local paths to .lst lists containing domains. IP entries are
msgstr "Добавьте URL или локальные пути к спискам .lst, содержащим домены. IP-записи игнорируются."
msgid "Add URLs or local paths to .srs / .json / .lst / .txt lists. Only domain rules are supported."
msgstr ""
msgstr "Укажите URL или локальные пути к спискам .srs / .json / .lst / .txt. Поддерживаются только правила для доменов."
msgid "Add URLs or local paths to .srs / .json / .lst / .txt lists. Subnets are ignored by default."
msgstr ""
msgstr "Укажите URL или локальные пути к спискам .srs / .json / .lst / .txt. Подсети по умолчанию игнорируются."
msgid "Add URLTest / Priority group"
msgstr "Добавить группу URLTest / Приоритет"
@@ -1395,7 +1395,7 @@ msgid "Direct connection via ISP"
msgstr "Прямое подключение через провайдера"
msgid "Direct Proxy"
msgstr "Прямой прокси"
msgstr "Direct Proxy"
msgid "Direct WAN"
msgstr "Прямой WAN"
@@ -1440,7 +1440,7 @@ msgid "DNS Blocking"
msgstr "Блокировка DNS"
msgid "DNS Cache Size"
msgstr ""
msgstr "Размер кэша DNS"
msgid "DNS Check Interval"
msgstr "Интервал проверки DNS"
@@ -1797,7 +1797,7 @@ msgid "Enabled (always prioritize)"
msgstr "Включено (всегда повышать приоритет)"
msgid "Enables high-capacity local DNS caching in dnsmasq to accelerate DNS queries and absorb transient latency during service restarts."
msgstr ""
msgstr "Включает емкий локальный кэш DNS в dnsmasq для ускорения запросов и устранения задержек при перезапуске служб."
msgid "Enables the background daemon that polls Telegram for commands and sends notifications."
msgstr "Включает фоновый демон, который опрашивает Telegram на наличие команд и отправляет уведомления."
@@ -3102,7 +3102,7 @@ msgid "Local Address"
msgstr "Локальный адрес"
msgid "Local DNS Cache"
msgstr ""
msgstr "Локальный кэш DNS"
msgid "Local SOCKS proxy desynchronizer (ciadpi)"
msgstr "Локальный десинхронизатор SOCKS-прокси (ciadpi)"
@@ -3480,7 +3480,7 @@ msgid "Notify on Subscription Updates"
msgstr "Уведомлять об обновлениях подписок"
msgid "Number of cached DNS domain entries in dnsmasq (default: 10000)."
msgstr ""
msgstr "Количество кэшируемых доменных записей в dnsmasq (по умолчанию: 10000)."
msgid "Number of consecutive failed checks required before switching DNS servers."
msgstr "Количество последовательных неудачных проверок до переключения DNS-серверов."
@@ -4287,7 +4287,7 @@ msgid "Rule Name"
msgstr "Название правила"
msgid "Rule set must be an HTTP(S) URL or a local .srs / .json / .lst / .txt path"
msgstr ""
msgstr "Набор правил должен быть URL HTTP(S) или локальным путем к .srs / .json / .lst / .txt"
msgid "Rule set settings"
msgstr "Настройки набора правил"
@@ -5997,7 +5997,7 @@ msgid "Add URLs or local paths to .srs / .json lists. Subnets are ignored by def
msgstr "Добавьте URL или локальные пути к спискам .srs / .json. Подсети игнорируются по умолчанию."
msgid "Plain .lst / .txt lists belong in \"Domain and IP lists\", not \"Rule sets\""
msgstr ""
msgstr "Текстовые списки .lst / .txt относятся к «Спискам доменов и IP», а не к «Наборам правил»"
msgid "Rule set must be an HTTP(S) URL or a local .srs / .json path"
msgstr "Набор правил должен быть HTTP(S) URL или локальным путём .srs / .json"
+11 -11
View File
@@ -7,8 +7,8 @@ msgid ""
msgstr ""
"Project-Id-Version: TACHYON\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-09-17 15:45+0300\n"
"PO-Revision-Date: 2026-09-17 15:45+0300\n"
"POT-Creation-Date: 2026-09-17 18:04+0300\n"
"PO-Revision-Date: 2026-09-17 18:04+0300\n"
"Last-Translator: Dushnilin\n"
"Language-Team: none\n"
"Language: ru\n"
@@ -273,10 +273,10 @@ msgid "Add URLs or local paths to .lst lists containing domains. IP entries are
msgstr "Добавьте URL или локальные пути к спискам .lst, содержащим домены. IP-записи игнорируются."
msgid "Add URLs or local paths to .srs / .json / .lst / .txt lists. Only domain rules are supported."
msgstr ""
msgstr "Укажите URL или локальные пути к спискам .srs / .json / .lst / .txt. Поддерживаются только правила для доменов."
msgid "Add URLs or local paths to .srs / .json / .lst / .txt lists. Subnets are ignored by default."
msgstr ""
msgstr "Укажите URL или локальные пути к спискам .srs / .json / .lst / .txt. Подсети по умолчанию игнорируются."
msgid "Add URLTest / Priority group"
msgstr "Добавить группу URLTest / Приоритет"
@@ -1395,7 +1395,7 @@ msgid "Direct connection via ISP"
msgstr "Прямое подключение через провайдера"
msgid "Direct Proxy"
msgstr "Прямой прокси"
msgstr "Direct Proxy"
msgid "Direct WAN"
msgstr "Прямой WAN"
@@ -1440,7 +1440,7 @@ msgid "DNS Blocking"
msgstr "Блокировка DNS"
msgid "DNS Cache Size"
msgstr ""
msgstr "Размер кэша DNS"
msgid "DNS Check Interval"
msgstr "Интервал проверки DNS"
@@ -1797,7 +1797,7 @@ msgid "Enabled (always prioritize)"
msgstr "Включено (всегда повышать приоритет)"
msgid "Enables high-capacity local DNS caching in dnsmasq to accelerate DNS queries and absorb transient latency during service restarts."
msgstr ""
msgstr "Включает емкий локальный кэш DNS в dnsmasq для ускорения запросов и устранения задержек при перезапуске служб."
msgid "Enables the background daemon that polls Telegram for commands and sends notifications."
msgstr "Включает фоновый демон, который опрашивает Telegram на наличие команд и отправляет уведомления."
@@ -3102,7 +3102,7 @@ msgid "Local Address"
msgstr "Локальный адрес"
msgid "Local DNS Cache"
msgstr ""
msgstr "Локальный кэш DNS"
msgid "Local SOCKS proxy desynchronizer (ciadpi)"
msgstr "Локальный десинхронизатор SOCKS-прокси (ciadpi)"
@@ -3480,7 +3480,7 @@ msgid "Notify on Subscription Updates"
msgstr "Уведомлять об обновлениях подписок"
msgid "Number of cached DNS domain entries in dnsmasq (default: 10000)."
msgstr ""
msgstr "Количество кэшируемых доменных записей в dnsmasq (по умолчанию: 10000)."
msgid "Number of consecutive failed checks required before switching DNS servers."
msgstr "Количество последовательных неудачных проверок до переключения DNS-серверов."
@@ -4287,7 +4287,7 @@ msgid "Rule Name"
msgstr "Название правила"
msgid "Rule set must be an HTTP(S) URL or a local .srs / .json / .lst / .txt path"
msgstr ""
msgstr "Набор правил должен быть URL HTTP(S) или локальным путем к .srs / .json / .lst / .txt"
msgid "Rule set settings"
msgstr "Настройки набора правил"
@@ -5997,7 +5997,7 @@ msgid "Add URLs or local paths to .srs / .json lists. Subnets are ignored by def
msgstr "Добавьте URL или локальные пути к спискам .srs / .json. Подсети игнорируются по умолчанию."
msgid "Plain .lst / .txt lists belong in \"Domain and IP lists\", not \"Rule sets\""
msgstr ""
msgstr "Текстовые списки .lst / .txt относятся к «Спискам доменов и IP», а не к «Наборам правил»"
msgid "Rule set must be an HTTP(S) URL or a local .srs / .json path"
msgstr "Набор правил должен быть HTTP(S) URL или локальным путём .srs / .json"
+2 -2
View File
@@ -7,8 +7,8 @@ msgid ""
msgstr ""
"Project-Id-Version: TACHYON\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-09-17 12:45+0300\n"
"PO-Revision-Date: 2026-09-17 12:45+0300\n"
"POT-Creation-Date: 2026-09-17 15:04+0300\n"
"PO-Revision-Date: 2026-09-17 15:04+0300\n"
"Last-Translator: Dushnilin <dushnilin@gmail.com>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
+80 -33
View File
@@ -4692,17 +4692,52 @@ function local_rule_doctor(pre_res, pre_verify) {
}
}
// ── 1. End-to-end live verification ──
let wan_cause_added = false;
// ── 1. End-to-end live verification & root-cause correlation ──
let wan_failed = false;
let singbox_failed = false;
for (let c in verify.checks) {
if (c.status == "fail") {
if (c.name == "WAN interface" || c.name == "Default gateway")
wan_failed = true;
if (c.name == "sing-box process")
singbox_failed = true;
}
}
let wan_cause = null;
let singbox_cause = null;
if (wan_failed) {
wan_cause = {
probability: 95,
cause: lang == "en" ? "WAN default gateway or Internet uplink is unreachable" : "Шлюз по умолчанию или внешний интернет недоступен",
fix: "fix_wan_interface",
symptoms: []
};
push(causes, wan_cause);
add_fix("fix_wan_interface");
} else if (singbox_failed) {
singbox_cause = {
probability: 95,
cause: lang == "en" ? "sing-box process is stopped or non-functional" : "Процесс sing-box остановлен или не функционирует",
fix: "start_singbox",
symptoms: []
};
push(causes, singbox_cause);
add_fix("start_singbox");
}
for (let c in verify.checks) {
if (c.status != "fail") continue;
if (c.name == "sing-box process") {
push(causes, {
probability: 95,
cause: lang == "en" ? "sing-box process is stopped or non-functional" : "Процесс sing-box остановлен или не функционирует",
fix: "start_singbox"
});
add_fix("start_singbox");
if (!singbox_cause && !wan_cause) {
push(causes, {
probability: 95,
cause: lang == "en" ? "sing-box process is stopped or non-functional" : "Процесс sing-box остановлен или не функционирует",
fix: "start_singbox"
});
add_fix("start_singbox");
}
} else if (c.name == "LAN DNS via dnsmasq") {
push(causes, {
probability: 85,
@@ -4711,31 +4746,37 @@ function local_rule_doctor(pre_res, pre_verify) {
});
add_fix("fix_dnsmasq");
} else if (c.name == "Proxy DNS via sing-box") {
push(causes, {
probability: 85,
cause: lang == "en" ? "Proxy DNS via sing-box failed to respond" : "Прокси-DNS через sing-box (127.0.0.42) не отвечает",
fix: "clear_dns_cache"
});
add_fix("clear_dns_cache");
if (cfg.dns_type != "doh") {
add_fix("switch_to_doh");
if (wan_cause) {
push(wan_cause.symptoms, lang == "en" ? "Proxy DNS unavailable (WAN down)" : "Прокси-DNS недоступен (нет связи с WAN)");
} else if (singbox_cause) {
push(singbox_cause.symptoms, lang == "en" ? "Proxy DNS not answering (sing-box stopped)" : "Прокси-DNS не отвечает (sing-box остановлен)");
} else {
push(causes, {
probability: 85,
cause: lang == "en" ? "Proxy DNS via sing-box failed to respond" : "Прокси-DNS через sing-box (127.0.0.42) не отвечает",
fix: "clear_dns_cache"
});
add_fix("clear_dns_cache");
if (cfg.dns_type != "doh") {
add_fix("switch_to_doh");
}
}
} else if ((c.name == "WAN interface" || c.name == "Default gateway") && !wan_cause_added) {
wan_cause_added = true;
push(causes, {
probability: 90,
cause: lang == "en" ? "WAN default gateway or Internet uplink is unreachable" : "Шлюз по умолчанию или внешний интернет недоступен",
fix: "fix_wan_interface"
});
add_fix("fix_wan_interface");
} else if (c.name == "WAN interface" || c.name == "Default gateway") {
// Already added as primary root cause if wan_failed
} else if (c.name == "HTTP via proxy") {
push(causes, {
probability: 88,
cause: lang == "en" ? "HTTP through the proxy fails end-to-end (node offline or blocked)" : "HTTP через прокси не проходит (узел офлайн или заблокирован)",
fix: "update_subscriptions"
});
add_fix("update_subscriptions");
add_fix("restart_zapret");
if (wan_cause) {
push(wan_cause.symptoms, lang == "en" ? "HTTP through proxy unreachable (WAN down)" : "HTTP через прокси недоступен (нет связи с WAN)");
} else if (singbox_cause) {
push(singbox_cause.symptoms, lang == "en" ? "HTTP through proxy fails (sing-box stopped)" : "HTTP через прокси не проходит (sing-box остановлен)");
} else {
push(causes, {
probability: 88,
cause: lang == "en" ? "HTTP through the proxy fails end-to-end (node offline or blocked)" : "HTTP через прокси не проходит (узел офлайн или заблокирован)",
fix: "update_subscriptions"
});
add_fix("update_subscriptions");
add_fix("restart_zapret");
}
}
}
@@ -4856,7 +4897,10 @@ function local_rule_doctor(pre_res, pre_verify) {
} else {
push(report_lines, "#### Root Cause Analysis:");
for (let c in causes) {
push(report_lines, sprintf("- [%d%% Probability] %s", c.probability, c.cause));
let line = sprintf("- [%d%% Probability] %s", c.probability, c.cause);
if (c.symptoms && length(c.symptoms) > 0)
line += " (Symptoms: " + join("; ", c.symptoms) + ")";
push(report_lines, line);
}
}
} else {
@@ -4873,7 +4917,10 @@ function local_rule_doctor(pre_res, pre_verify) {
} else {
push(report_lines, "#### Анализ возможных причин сбоя:");
for (let c in causes) {
push(report_lines, sprintf("- [%d%% вероятность] %s", c.probability, c.cause));
let line = sprintf("- [%d%% вероятность] %s", c.probability, c.cause);
if (c.symptoms && length(c.symptoms) > 0)
line += " (Симптомы: " + join("; ", c.symptoms) + ")";
push(report_lines, line);
}
}
}
+26 -1
View File
@@ -14,6 +14,7 @@ let command_output_from_args = common.command_output_from_args;
let command_success_from_args = common.command_success_from_args;
let connections = require("config.connections");
let subscription_share_link = require("subscription.share_link");
let crypt4 = require("subscription.crypt4");
function core_url_module_or_null() {
try {
@@ -1535,6 +1536,22 @@ function get_subscription_hwid(custom_hwid) {
}
function download_subscription(url, filepath, http_proxy_address, headers_filepath, effective_user_agent, effective_hwid, device_headers, allow_insecure) {
if (crypt4.is_crypt4(url)) {
let dec = crypt4.decrypt(url, effective_hwid, generate_hwid());
if (dec != null && dec != "") {
let stamp = clock();
let suffix = sprintf(".part.%d.%d", stamp[0], stamp[1]);
let tmpfile = filepath + suffix;
if (fs.writefile(tmpfile, dec)) {
move_file(tmpfile, filepath);
if (headers_filepath != "")
unlink_path(headers_filepath);
return 0;
}
}
return 1;
}
let retries = 3;
let wait_seconds = 2;
let timeout = 15;
@@ -1579,9 +1596,11 @@ function download_subscription(url, filepath, http_proxy_address, headers_filepa
push(args, "-o");
push(args, tmpfile);
let hwid_val = get_subscription_hwid(effective_hwid);
let request_headers = [
"User-Agent: " + get_subscription_user_agent(effective_user_agent),
"X-HWID: " + get_subscription_hwid(effective_hwid)
"X-HWID: " + hwid_val,
"HWID: " + hwid_val
];
for (let header in device_request_headers(device_headers))
push(request_headers, header);
@@ -1593,6 +1612,12 @@ function download_subscription(url, filepath, http_proxy_address, headers_filepa
let status = command_status_from_args(args);
if (status == 0 && file_nonempty(tmpfile)) {
let downloaded_body = fs.readfile(tmpfile);
if (downloaded_body != null && crypt4.is_crypt4(downloaded_body)) {
let dec = crypt4.decrypt(downloaded_body, effective_hwid, generate_hwid());
if (dec != null && dec != "")
fs.writefile(tmpfile, dec);
}
move_file(tmpfile, filepath);
if (headers_filepath != "") {
if (file_nonempty(headers_tmpfile))
@@ -0,0 +1,355 @@
#!/usr/bin/env ucode
let fs = require("fs");
let common = require("core.common");
let as_string = common.as_string;
let shell_quote = common.shell_quote;
function trim(str) {
return replace(as_string(str), /^[\s\r\n]+|[\s\r\n]+$/g, "");
}
function is_crypt4(value) {
value = lc(trim(value));
return match(value, /^(happ:\/\/)?crypt4\//) != null;
}
function strip_prefix(value) {
value = trim(value);
if (match(value, /^(happ:\/\/)?crypt4\//i))
return replace(value, /^(happ:\/\/)?crypt4\//i, "");
return value;
}
function hex_to_bytes(h) {
let res = "";
for (let i = 0; i < length(h); i += 2)
res += chr(hex(substr(h, i, 2)));
return res;
}
function sha256_hex(str) {
let p = fs.popen("printf %s " + shell_quote(str) + " | sha256sum 2>/dev/null", "r");
if (!p)
return "";
let out = trim(p.read("all") || "");
p.close();
return split(out, /[ \t]+/)[0];
}
// Inverted S-Box for AES
let InvSbox = [
0x52, 0x09, 0x6a, 0xd5, 0x30, 0x36, 0xa5, 0x38, 0xbf, 0x40, 0xa3, 0x9e, 0x81, 0xf3, 0xd7, 0xfb,
0x7c, 0xe3, 0x39, 0x82, 0x9b, 0x2f, 0xff, 0x87, 0x34, 0x8e, 0x43, 0x44, 0xc4, 0xde, 0xe9, 0xcb,
0x54, 0x7b, 0x94, 0x32, 0xa6, 0xc2, 0x23, 0x3d, 0xee, 0x4c, 0x95, 0x0b, 0x42, 0xfa, 0xc3, 0x4e,
0x08, 0x2e, 0xa1, 0x66, 0x28, 0xd9, 0x24, 0xb2, 0x76, 0x5b, 0xa2, 0x49, 0x6d, 0x8b, 0xd1, 0x25,
0x72, 0xf8, 0xf6, 0x64, 0x86, 0x68, 0x98, 0x16, 0xd4, 0xa4, 0x5c, 0xcc, 0x5d, 0x65, 0xb6, 0x92,
0x6c, 0x70, 0x48, 0x50, 0xfd, 0xed, 0xb9, 0xda, 0x5e, 0x15, 0x46, 0x57, 0xa7, 0x8d, 0x9d, 0x84,
0x90, 0xd8, 0xab, 0x00, 0x8c, 0xbc, 0xd3, 0x0a, 0xf7, 0xe4, 0x58, 0x05, 0xb8, 0xb3, 0x45, 0x06,
0xd0, 0x2c, 0x1e, 0x8f, 0xca, 0x3f, 0x0f, 0x02, 0xc1, 0xaf, 0xbd, 0x03, 0x01, 0x13, 0x8a, 0x6b,
0x3a, 0x91, 0x11, 0x41, 0x4f, 0x67, 0xdc, 0xea, 0x97, 0xf2, 0xcf, 0xce, 0xf0, 0xb4, 0xe6, 0x73,
0x96, 0xac, 0x74, 0x22, 0xe7, 0xad, 0x35, 0x85, 0xe2, 0xf9, 0x37, 0xe8, 0x1c, 0x75, 0xdf, 0x6e,
0x47, 0xf1, 0x1a, 0x71, 0x1d, 0x29, 0xc5, 0x89, 0x6f, 0xb7, 0x62, 0x0e, 0xaa, 0x18, 0xbe, 0x1b,
0xfc, 0x56, 0x3e, 0x4b, 0xc6, 0xd2, 0x79, 0x20, 0x9a, 0xdb, 0xc0, 0xfe, 0x78, 0xcd, 0x5a, 0xf4,
0x1f, 0xdd, 0xa8, 0x33, 0x88, 0x07, 0xc7, 0x31, 0xb1, 0x12, 0x10, 0x59, 0x27, 0x80, 0xec, 0x5f,
0x60, 0x51, 0x7f, 0xa9, 0x19, 0xb5, 0x4a, 0x0d, 0x2d, 0xe5, 0x7a, 0x9f, 0x93, 0xc9, 0x9c, 0xef,
0xa0, 0xe0, 0x3b, 0x4d, 0xae, 0x2a, 0xf5, 0xb0, 0xc8, 0xeb, 0xbb, 0x3c, 0x83, 0x53, 0x99, 0x61,
0x17, 0x2b, 0x04, 0x7e, 0xba, 0x77, 0xd6, 0x26, 0xe1, 0x69, 0x14, 0x63, 0x55, 0x21, 0x0c, 0x7d
];
// Forward S-box for key expansion
let Sbox = [
0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5, 0x30, 0x01, 0x67, 0x2b, 0xfe, 0xd7, 0xab, 0x76,
0xca, 0x82, 0xc9, 0x7d, 0xfa, 0x59, 0x47, 0xf0, 0xad, 0xd4, 0xa2, 0xaf, 0x9c, 0xa4, 0x72, 0xc0,
0xb7, 0xfd, 0x93, 0x26, 0x36, 0x3f, 0xf7, 0xcc, 0x34, 0xa5, 0xe5, 0xf1, 0x71, 0xd8, 0x31, 0x15,
0x04, 0xc7, 0x23, 0xc3, 0x18, 0x96, 0x05, 0x9a, 0x07, 0x12, 0x80, 0xe2, 0xeb, 0x27, 0xb2, 0x75,
0x09, 0x83, 0x2c, 0x1a, 0x1b, 0x6e, 0x5a, 0xa0, 0x52, 0x3b, 0xd6, 0xb3, 0x29, 0xe3, 0x2f, 0x84,
0x53, 0xd1, 0x00, 0xed, 0x20, 0xfc, 0xb1, 0x5b, 0x6a, 0xcb, 0xbe, 0x39, 0x4a, 0x4c, 0x58, 0xcf,
0xd0, 0xef, 0xaa, 0xfb, 0x43, 0x4d, 0x33, 0x85, 0x45, 0xf9, 0x02, 0x7f, 0x50, 0x3c, 0x9f, 0xa8,
0x51, 0xa3, 0x40, 0x8f, 0x92, 0x9d, 0x38, 0xf5, 0xbc, 0xb6, 0xda, 0x21, 0x10, 0xff, 0xf3, 0xd2,
0xcd, 0x0c, 0x13, 0xec, 0x5f, 0x97, 0x44, 0x17, 0xc4, 0xa7, 0x7e, 0x3d, 0x64, 0x5d, 0x19, 0x73,
0x60, 0x81, 0x4f, 0xdc, 0x22, 0x2a, 0x90, 0x88, 0x46, 0xee, 0xb8, 0x14, 0xde, 0x5e, 0x0b, 0xdb,
0xe0, 0x32, 0x3a, 0x0a, 0x49, 0x06, 0x24, 0x5c, 0xc2, 0xd3, 0xac, 0x62, 0x91, 0x95, 0xe4, 0x79,
0xe7, 0xc8, 0x37, 0x6d, 0x8d, 0xd5, 0x4e, 0xa9, 0x6c, 0x56, 0xf4, 0xea, 0x65, 0x7a, 0xae, 0x08,
0xba, 0x78, 0x25, 0x2e, 0x1c, 0xa6, 0xb4, 0xc6, 0xe8, 0xdd, 0x74, 0x1f, 0x4b, 0xbd, 0x8b, 0x8a,
0x70, 0x3e, 0xb5, 0x66, 0x48, 0x03, 0xf6, 0x0e, 0x61, 0x35, 0x57, 0xb9, 0x86, 0xc1, 0x1d, 0x9e,
0xe1, 0xf8, 0x98, 0x11, 0x69, 0xd9, 0x8e, 0x94, 0x9b, 0x1e, 0x87, 0xe9, 0xce, 0x55, 0x28, 0xdf,
0x8c, 0xa1, 0x89, 0x0d, 0xbf, 0xe6, 0x42, 0x68, 0x41, 0x99, 0x2d, 0x0f, 0xb0, 0x54, 0xbb, 0x16
];
let Rcon = [0x00, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36];
let Mul9 = [], Mul11 = [], Mul13 = [], Mul14 = [];
function gmul(a, b) {
let p = 0;
for (let c = 0; c < 8; c++) {
if ((b & 1) != 0) p ^= a;
let hi = (a & 0x80);
a = (a << 1) & 0xff;
if (hi != 0) a ^= 0x1b;
b >>= 1;
}
return p;
}
for (let i = 0; i < 256; i++) {
push(Mul9, gmul(i, 9));
push(Mul11, gmul(i, 11));
push(Mul13, gmul(i, 13));
push(Mul14, gmul(i, 14));
}
function key_expansion(key_bytes) {
let Nk = length(key_bytes) / 4;
let Nr = Nk + 6;
let w = [];
for (let i = 0; i < length(key_bytes); i += 4)
push(w, [ord(key_bytes, i), ord(key_bytes, i + 1), ord(key_bytes, i + 2), ord(key_bytes, i + 3)]);
for (let i = Nk; i < 4 * (Nr + 1); i++) {
let temp = [w[i - 1][0], w[i - 1][1], w[i - 1][2], w[i - 1][3]];
if (i % Nk == 0) {
let t0 = temp[0];
temp[0] = Sbox[temp[1]] ^ Rcon[i / Nk];
temp[1] = Sbox[temp[2]];
temp[2] = Sbox[temp[3]];
temp[3] = Sbox[t0];
} else if (Nk > 6 && (i % Nk == 4)) {
temp[0] = Sbox[temp[0]];
temp[1] = Sbox[temp[1]];
temp[2] = Sbox[temp[2]];
temp[3] = Sbox[temp[3]];
}
push(w, [w[i - Nk][0] ^ temp[0], w[i - Nk][1] ^ temp[1], w[i - Nk][2] ^ temp[2], w[i - Nk][3] ^ temp[3]]);
}
return { w, Nr };
}
function inv_cipher_block(block, exp) {
let w = exp.w;
let Nr = exp.Nr;
let s = [];
for (let r = 0; r < 4; r++) {
s[r] = [];
for (let c = 0; c < 4; c++)
s[r][c] = ord(block, r + 4 * c);
}
for (let c = 0; c < 4; c++)
for (let r = 0; r < 4; r++)
s[r][c] ^= w[Nr * 4 + c][r];
for (let round = Nr - 1; round >= 1; round--) {
let t1 = s[1][3]; s[1][3] = s[1][2]; s[1][2] = s[1][1]; s[1][1] = s[1][0]; s[1][0] = t1;
let t20 = s[2][0], t21 = s[2][1]; s[2][0] = s[2][2]; s[2][1] = s[2][3]; s[2][2] = t20; s[2][3] = t21;
let t3 = s[3][0]; s[3][0] = s[3][1]; s[3][1] = s[3][2]; s[3][2] = s[3][3]; s[3][3] = t3;
for (let r = 0; r < 4; r++)
for (let c = 0; c < 4; c++)
s[r][c] = InvSbox[s[r][c]];
for (let c = 0; c < 4; c++)
for (let r = 0; r < 4; r++)
s[r][c] ^= w[round * 4 + c][r];
for (let c = 0; c < 4; c++) {
let s0 = s[0][c], s1 = s[1][c], s2 = s[2][c], s3 = s[3][c];
s[0][c] = Mul14[s0] ^ Mul11[s1] ^ Mul13[s2] ^ Mul9[s3];
s[1][c] = Mul9[s0] ^ Mul14[s1] ^ Mul11[s2] ^ Mul13[s3];
s[2][c] = Mul13[s0] ^ Mul9[s1] ^ Mul14[s2] ^ Mul11[s3];
s[3][c] = Mul11[s0] ^ Mul13[s1] ^ Mul9[s2] ^ Mul14[s3];
}
}
let t1 = s[1][3]; s[1][3] = s[1][2]; s[1][2] = s[1][1]; s[1][1] = s[1][0]; s[1][0] = t1;
let t20 = s[2][0], t21 = s[2][1]; s[2][0] = s[2][2]; s[2][1] = s[2][3]; s[2][2] = t20; s[2][3] = t21;
let t3 = s[3][0]; s[3][0] = s[3][1]; s[3][1] = s[3][2]; s[3][2] = s[3][3]; s[3][3] = t3;
for (let r = 0; r < 4; r++)
for (let c = 0; c < 4; c++)
s[r][c] = InvSbox[s[r][c]];
for (let c = 0; c < 4; c++)
for (let r = 0; r < 4; r++)
s[r][c] ^= w[c][r];
let out = "";
for (let c = 0; c < 4; c++)
for (let r = 0; r < 4; r++)
out += chr(s[r][c]);
return out;
}
function aes_cbc_decrypt(ciphertext, key_bytes, iv) {
if (length(ciphertext) % 16 != 0 || length(key_bytes) < 16 || length(iv) != 16)
return null;
let exp = key_expansion(key_bytes);
let plaintext = "";
let prev = iv;
for (let i = 0; i < length(ciphertext); i += 16) {
let block = substr(ciphertext, i, 16);
let dec = inv_cipher_block(block, exp);
let xored = "";
for (let j = 0; j < 16; j++)
xored += chr(ord(dec, j) ^ ord(prev, j));
plaintext += xored;
prev = block;
}
// PKCS#7 unpadding validation
let pad = ord(plaintext, length(plaintext) - 1);
if (pad < 1 || pad > 16 || length(plaintext) < pad)
return null;
for (let i = length(plaintext) - pad; i < length(plaintext); i++) {
if (ord(plaintext, i) != pad)
return null;
}
return substr(plaintext, 0, length(plaintext) - pad);
}
function is_valid_plaintext(str) {
if (str == null || length(str) == 0)
return false;
let len = length(str);
for (let i = 0; i < len; i++) {
let ch = ord(substr(str, i, 1));
if (ch < 32 && ch != 9 && ch != 10 && ch != 13)
return false;
}
return true;
}
function try_openssl_decrypt(cipher, key_hex, iv_hex, cipher_type) {
let tmp_in = "/tmp/tachyon-c4-in." + clock()[0] + "." + clock()[1];
if (fs.writefile(tmp_in, cipher) == null)
return null;
let cmd = "openssl enc -d -" + cipher_type + " -in " + shell_quote(tmp_in) + " -K " + shell_quote(key_hex) + " -iv " + shell_quote(iv_hex) + " 2>/dev/null";
let p = fs.popen(cmd, "r");
let out = p ? p.read("all") : null;
let code = p ? p.close() : -1;
fs.unlink(tmp_in);
if (code != 0 || out == null || length(out) == 0 || !is_valid_plaintext(out))
return null;
return out;
}
function get_system_device_hwid() {
let mac = trim(as_string(fs.readfile("/sys/class/net/eth0/address")));
if (mac == "")
mac = trim(as_string(fs.readfile("/sys/class/net/br-lan/address")));
if (mac == "") {
let mid = trim(as_string(fs.readfile("/etc/machine-id")));
if (mid != "") return mid;
}
if (mac == "") return "";
let p = fs.popen("printf %s " + shell_quote(mac) + " | md5sum 2>/dev/null", "r");
if (!p) return "";
let out = trim(p.read("all") || "");
p.close();
let hash = split(out, /[ \t]+/)[0];
if (length(hash) >= 16)
return substr(hash, 0, 4) + "-" + substr(hash, 4, 4) + "-" + substr(hash, 8, 4) + "-" + substr(hash, 12, 4);
return "";
}
function decrypt_crypt4(payload, ...secret_keys) {
payload = trim(as_string(payload));
if (match(payload, /^(happ:\/\/)?crypt4\//i))
payload = replace(payload, /^(happ:\/\/)?crypt4\//i, "");
payload = replace(payload, /[\r\n\t ]/g, "");
payload = replace(payload, /-/g, "+");
payload = replace(payload, /_/g, "/");
let rem = length(payload) % 4;
if (rem == 1)
return null;
if (rem > 1) {
for (let i = 0; i < 4 - rem; i++)
payload += "=";
}
let binary = b64dec(payload);
if (binary == null || length(binary) < 32)
return null;
let iv = substr(binary, 0, 16);
let cipher = substr(binary, 16);
if (length(cipher) % 16 != 0)
return null;
let keys = [];
let seen_keys = {};
function add_key(k) {
if (type(k) == "array") {
for (let item in k)
add_key(item);
} else {
k = trim(as_string(k));
if (k != "" && !seen_keys[k]) {
seen_keys[k] = true;
push(keys, k);
}
}
}
for (let k in secret_keys)
add_key(k);
let dev_hwid = get_system_device_hwid();
if (dev_hwid != "")
add_key(dev_hwid);
add_key("HappDefaultSalt");
let iv_hex = common.bytes_to_hex(iv);
for (let k in keys) {
let h = sha256_hex(k);
if (length(h) != 64)
continue;
// 1. Try OpenSSL fast path if available
let open_res = try_openssl_decrypt(cipher, h, iv_hex, "aes-256-cbc");
if (open_res != null)
return open_res;
open_res = try_openssl_decrypt(cipher, substr(h, 0, 32), iv_hex, "aes-128-cbc");
if (open_res != null)
return open_res;
// 2. Pure ucode fallback
let full_key = hex_to_bytes(h);
let res = aes_cbc_decrypt(cipher, full_key, iv);
if (res != null && is_valid_plaintext(res))
return res;
res = aes_cbc_decrypt(cipher, substr(full_key, 0, 16), iv);
if (res != null && is_valid_plaintext(res))
return res;
}
return null;
}
function module_exports() {
return {
is_crypt4,
strip_prefix,
decrypt: decrypt_crypt4
};
}
if ((sourcepath(1) != null && sourcepath(1) != "") || ARGV[0] == null)
return module_exports();
let mode = ARGV[0] || "";
if (mode == "is-crypt4")
exit(is_crypt4(ARGV[1]) ? 0 : 1);
else if (mode == "decrypt") {
let dec = decrypt_crypt4(ARGV[1], ARGV[2], ARGV[3]);
if (dec == null)
exit(1);
print(dec);
}
+62 -2
View File
@@ -2,6 +2,7 @@
let fs = require("fs");
let common = require("core.common");
let crypt4 = require("subscription.crypt4");
let as_string = common.as_string;
let object_or_empty = common.object_or_empty;
let array_or_empty = common.array_or_empty;
@@ -31,7 +32,8 @@ function is_supported_share_link(line) {
starts_with(line, "socks4a://") ||
starts_with(line, "socks5://") ||
starts_with(line, "http://") ||
starts_with(line, "https://");
starts_with(line, "https://") ||
crypt4.is_crypt4(line);
}
function split_csv(value) {
@@ -1786,6 +1788,30 @@ function normalize_uri_list_data(data, output_file) {
line = replace(line, /\r/g, "");
line = trim(line);
if (line != "" && !starts_with(line, "#")) {
if (crypt4.is_crypt4(line)) {
let decrypted = crypt4.decrypt(line);
if (decrypted != null && decrypted != "") {
for (let dec_line in split(decrypted, "\n")) {
dec_line = trim(dec_line);
if (dec_line != "" && !starts_with(dec_line, "#")) {
let outbound = parse_share_link(dec_line);
if (outbound) {
if (outbound_is_unsupported_stub(outbound)) {
skipped++;
continue;
}
if (added > 0)
output.write(",");
output.write(sprintf("%J", outbound));
added++;
} else {
skipped++;
}
}
}
continue;
}
}
let outbound = parse_share_link(line);
if (outbound) {
if (outbound_is_unsupported_stub(outbound)) {
@@ -2202,6 +2228,30 @@ function normalize_uri_list_stream(input, output_file, strip_metadata) {
if (strip_metadata && line_no <= 20 && is_metadata_preamble_line(line))
continue;
if (line != "" && !starts_with(line, "#")) {
if (crypt4.is_crypt4(line)) {
let decrypted = crypt4.decrypt(line);
if (decrypted != null && decrypted != "") {
for (let dec_line in split(decrypted, "\n")) {
dec_line = trim(dec_line);
if (dec_line != "" && !starts_with(dec_line, "#")) {
let outbound = parse_share_link(dec_line);
if (outbound) {
if (outbound_is_unsupported_stub(outbound)) {
skipped++;
continue;
}
if (added > 0)
output.write(",");
output.write(sprintf("%J", outbound));
added++;
} else {
skipped++;
}
}
}
continue;
}
}
let outbound = parse_share_link(line);
if (outbound) {
if (outbound_is_unsupported_stub(outbound)) {
@@ -2243,6 +2293,10 @@ function file_looks_like_uri_list(input_file) {
line = trim(line);
if (line == "" || starts_with(line, "#") || is_metadata_preamble_line(line))
continue;
if (crypt4.is_crypt4(line)) {
input.close();
return false;
}
if (is_supported_share_link(line)) {
input.close();
return true;
@@ -3137,6 +3191,12 @@ function normalize_content_data(data, output_file, depth) {
data = replace(data, /\r/g, "");
data = strip_metadata_preamble_data(data);
if (depth < 2 && crypt4.is_crypt4(data)) {
let decrypted = crypt4.decrypt(data);
if (decrypted != null && decrypted != "")
return normalize_content_data(decrypted, output_file, depth + 1);
}
let first = first_non_ws_char(data);
if (first == "{" || first == "[") {
let decoded_json = json_decode_text(data);
@@ -3231,7 +3291,7 @@ function parse_subscription_source_entry(entry) {
if (index(entry, "|") >= 0)
return subscription_source_entry_result(false, "", "", "Configure User-Agent in the subscription item settings");
if (!(substr(entry, 0, 7) == "http://" || substr(entry, 0, 8) == "https://"))
if (!(substr(entry, 0, 7) == "http://" || substr(entry, 0, 8) == "https://" || crypt4.is_crypt4(entry)))
return subscription_source_entry_result(false, "", "", "Subscription URL must start with http:// or https://");
return subscription_source_entry_result(true, entry, "", "");
+122
View File
@@ -0,0 +1,122 @@
#!/usr/bin/env bash
set -eo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
TACHYON_LIB="$ROOT_DIR/tachyon/files/usr/lib"
CRYPT4="$ROOT_DIR/tachyon/files/usr/lib/subscription/crypt4.uc"
PARSER="$ROOT_DIR/tachyon/files/usr/lib/subscription/parser.uc"
CACHE="$ROOT_DIR/tachyon/files/usr/lib/subscription/cache.uc"
RUNTIME="$ROOT_DIR/tachyon/files/usr/lib/diagnostics/runtime.uc"
WORK_DIR="$(mktemp -d)"
cleanup() {
rm -rf "$WORK_DIR"
}
trap cleanup EXIT
fail() {
printf 'FAIL: %s\n' "$1" >&2
exit 1
}
ucode_run() {
command ucode -L "$TACHYON_LIB" "$@"
}
# 1. CLI is-crypt4 tests
ucode_run "$CRYPT4" is-crypt4 "happ://crypt4/dGVzdA==" || fail "happ://crypt4 should be recognized"
ucode_run "$CRYPT4" is-crypt4 "crypt4/dGVzdA==" || fail "crypt4/ should be recognized"
if ucode_run "$CRYPT4" is-crypt4 "https://example.com/sub.txt" 2>/dev/null; then
fail "https:// URL should not be recognized as crypt4"
fi
# 2. End-to-end Decryption tests (AES-256-CBC)
PLAINTEXT="vless://00000000-0000-4000-8000-000000000001@example.com:443?encryption=none&security=tls&sni=example.com#HappNode"
SECRET="my-custom-device-hwid"
KEY_HEX="$(printf '%s' "$SECRET" | sha256sum | awk '{print $1}')"
IV_HEX="000102030405060708090a0b0c0d0e0f"
# Encrypt with OpenSSL using file redirection to preserve null bytes
printf '\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f' >"$WORK_DIR/iv.bin"
printf '%s' "$PLAINTEXT" | openssl enc -aes-256-cbc -K "$KEY_HEX" -iv "$IV_HEX" >"$WORK_DIR/cipher.bin" 2>/dev/null
cat "$WORK_DIR/iv.bin" "$WORK_DIR/cipher.bin" >"$WORK_DIR/payload.bin"
B64_PAYLOAD="$(base64 <"$WORK_DIR/payload.bin" | tr -d '\r\n ')"
CRYPT4_URL="happ://crypt4/${B64_PAYLOAD}"
# Decrypt using crypt4 CLI
DECRYPTED="$(ucode_run "$CRYPT4" decrypt "$CRYPT4_URL" "$SECRET")"
[ "$DECRYPTED" = "$PLAINTEXT" ] || fail "Decrypted plaintext mismatch: got '$DECRYPTED', expected '$PLAINTEXT'"
# 3. Default salt decryption ("HappDefaultSalt")
SALT_HEX="$(printf '%s' "HappDefaultSalt" | sha256sum | awk '{print $1}')"
printf '%s' "$PLAINTEXT" | openssl enc -aes-256-cbc -K "$SALT_HEX" -iv "$IV_HEX" >"$WORK_DIR/salt_cipher.bin" 2>/dev/null
cat "$WORK_DIR/iv.bin" "$WORK_DIR/salt_cipher.bin" >"$WORK_DIR/salt_payload.bin"
SALT_B64="$(base64 <"$WORK_DIR/salt_payload.bin" | tr -d '\r\n ')"
SALT_CRYPT4_URL="crypt4/${SALT_B64}"
SALT_DECRYPTED="$(ucode_run "$CRYPT4" decrypt "$SALT_CRYPT4_URL")"
[ "$SALT_DECRYPTED" = "$PLAINTEXT" ] || fail "Default salt decrypt mismatch: got '$SALT_DECRYPTED'"
# 4. Corrupted / invalid input tests
if ucode_run "$CRYPT4" decrypt "crypt4/invalid_base64_!@#$" 2>/dev/null; then
fail "corrupted base64 should return failure"
fi
# 5. Parser integration
cat >"$WORK_DIR/test_parser.uc" <<'UCODE'
let parser = require("subscription.parser");
// URL source entry validation
let entry = parser.parse_subscription_source_entry("happ://crypt4/dGVzdA==");
if (!entry.valid)
exit(1);
let entry2 = parser.parse_subscription_source_entry("crypt4/dGVzdA==");
if (!entry2.valid)
exit(2);
exit(0);
UCODE
ucode_run "$WORK_DIR/test_parser.uc" || fail "parser source entry validation for crypt4 failed"
# 6. Normalize content validation with encrypted file
ENCRYPTED_FILE="$WORK_DIR/sub_encrypted.txt"
NORMALIZED_OUT="$WORK_DIR/normalized.json"
printf '%s' "$SALT_CRYPT4_URL" >"$ENCRYPTED_FILE"
cat >"$WORK_DIR/test_normalize.uc" <<'UCODE'
let parser = require("subscription.parser");
let enc_file = ARGV[0];
let out_file = ARGV[1];
let ok = parser.normalize_content_validated(enc_file, out_file);
if (!ok) exit(1);
UCODE
ucode_run "$WORK_DIR/test_normalize.uc" "$ENCRYPTED_FILE" "$NORMALIZED_OUT" || fail "normalize_content_validated failed on crypt4"
grep -Fq "example.com" "$NORMALIZED_OUT" || fail "normalized output missing expected server domain"
grep -Fq "HappNode" "$NORMALIZED_OUT" || fail "normalized output missing server tag"
# 7. Cache download_subscription with crypt4 URL
CACHE_OUT="$WORK_DIR/cache_sub.txt"
ucode_run "$CACHE" download-subscription "$CRYPT4_URL" "$CACHE_OUT" "" "" "" "$SECRET" || fail "download_subscription failed for crypt4 URL"
[ -s "$CACHE_OUT" ] || fail "download_subscription produced empty cache output"
grep -Fq "HappNode" "$CACHE_OUT" || fail "download_subscription did not properly decrypt content"
# 8. Dual HWID request headers check in cache.uc
grep -Fq '"X-HWID: " + hwid_val' "$CACHE" || fail "missing X-HWID header in cache.uc"
grep -Fq '"HWID: " + hwid_val' "$CACHE" || fail "missing HWID header in cache.uc"
# 9. Diagnostics symptom collapsing test
DOCTOR_OUT="$(ucode_run "$RUNTIME" ai-doctor 2>/dev/null)" || fail "ai-doctor failed to run"
echo "$DOCTOR_OUT" | grep -Fq '"success": true' || fail "ai-doctor report missing success flag"
echo "$DOCTOR_OUT" | grep -Fq '"model": "rule_engine_v2"' || fail "ai-doctor report missing rule_engine_v2 model"
# Verify symptom collapsing: when WAN or sing-box fails, downstream proxy updates are collapsed/suppressed
if echo "$DOCTOR_OUT" | grep -Fq '"update_subscriptions"'; then
fail "update_subscriptions should be suppressed when WAN/core service is unreachable"
fi
printf 'All Happ Crypt4, HWID, and symptom collapsing checks passed!\n'