Merge: объединить изменения из upstream forkop в tachyon, разрешить конфликты и исправить тесты E2E
This commit is contained in:
@@ -13,8 +13,6 @@ sh <(wget -O - https://raw.githubusercontent.com/Dushnilin/tachyon/main/install.
|
|||||||
|
|
||||||
> [!NOTE]
|
> [!NOTE]
|
||||||
> Конфигурация от **Forkop** (или **Podkop Plus**) полностью совместима и автоматически перенесется и переименуется в `/etc/config/tachyon` во время установки!
|
> Конфигурация от **Forkop** (или **Podkop Plus**) полностью совместима и автоматически перенесется и переименуется в `/etc/config/tachyon` во время установки!
|
||||||
|
|
||||||
|
|
||||||
### Что нового в этом форке
|
### Что нового в этом форке
|
||||||
|
|
||||||
* **Поддержка подписок**: автоматическое управление узлами и их регулярное обновление.
|
* **Поддержка подписок**: автоматическое управление узлами и их регулярное обновление.
|
||||||
|
|||||||
@@ -1762,7 +1762,7 @@
|
|||||||
"call": "Fastest",
|
"call": "Fastest",
|
||||||
"key": "Fastest",
|
"key": "Fastest",
|
||||||
"places": [
|
"places": [
|
||||||
"src/tachyon/methods/custom/getDashboardSections.ts:638"
|
"src/tachyon/methods/custom/getDashboardSections.ts:647"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -2239,76 +2239,6 @@
|
|||||||
"src/validators/validateSubnet.ts:10"
|
"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",
|
"call": "Invalid HY2 URL: insecure must be 0 or 1",
|
||||||
"key": "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",
|
"call": "unknown",
|
||||||
"key": "unknown",
|
"key": "unknown",
|
||||||
"places": [
|
"places": [
|
||||||
"src/tachyon/helpers/singBoxVariant.ts:28",
|
|
||||||
"src/tachyon/services/systemInfo.service.ts:10",
|
"src/tachyon/services/systemInfo.service.ts:10",
|
||||||
"src/tachyon/services/systemInfo.service.ts:11",
|
"src/tachyon/services/systemInfo.service.ts:11",
|
||||||
"src/tachyon/services/systemInfo.service.ts:12",
|
"src/tachyon/services/systemInfo.service.ts:12",
|
||||||
@@ -5228,6 +5157,7 @@
|
|||||||
"src/tachyon/services/systemInfo.service.ts:22",
|
"src/tachyon/services/systemInfo.service.ts:22",
|
||||||
"src/tachyon/services/systemInfo.service.ts:25",
|
"src/tachyon/services/systemInfo.service.ts:25",
|
||||||
"src/tachyon/services/systemInfo.service.ts:26",
|
"src/tachyon/services/systemInfo.service.ts:26",
|
||||||
|
"src/tachyon/helpers/singBoxVariant.ts:28",
|
||||||
"src/tachyon/tabs/updates/initController.ts:329",
|
"src/tachyon/tabs/updates/initController.ts:329",
|
||||||
"src/tachyon/tabs/diagnostic/checks/runInboundsCheck.ts:25"
|
"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://",
|
"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://, http://, https://, hysteria2://, or hy2://",
|
"key": "URL must start with vless://, vmess://, ss://, trojan://, socks4://, socks4a://, socks5://, hysteria2://, or hy2://",
|
||||||
"places": [
|
"places": [
|
||||||
"src/validators/validateProxyUrl.ts:47"
|
"src/validators/validateProxyUrl.ts:42"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -5545,7 +5475,6 @@
|
|||||||
"src/validators/validateDns.ts:29",
|
"src/validators/validateDns.ts:29",
|
||||||
"src/validators/validateDomain.ts:57",
|
"src/validators/validateDomain.ts:57",
|
||||||
"src/validators/validateDomain.ts:76",
|
"src/validators/validateDomain.ts:76",
|
||||||
"src/validators/validateHttpProxyUrl.ts:89",
|
|
||||||
"src/validators/validateHysteriaUrl.ts:151",
|
"src/validators/validateHysteriaUrl.ts:151",
|
||||||
"src/validators/validateIp.ts:25",
|
"src/validators/validateIp.ts:25",
|
||||||
"src/validators/validateOutboundJson.ts:118",
|
"src/validators/validateOutboundJson.ts:118",
|
||||||
@@ -5610,8 +5539,8 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"call": "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://, http(s)://, hy2/hysteria2:// links",
|
"key": "vless://, vmess://, ss://, trojan://, socks4/5://, hy2/hysteria2:// links",
|
||||||
"places": [
|
"places": [
|
||||||
"../luci-app-tachyon/htdocs/luci-static/resources/view/tachyon/section.js:7137"
|
"../luci-app-tachyon/htdocs/luci-static/resources/view/tachyon/section.js:7137"
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: TACHYON\n"
|
"Project-Id-Version: TACHYON\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2026-07-14 05:07+0300\n"
|
"POT-Creation-Date: 2026-07-15 06:45+0300\n"
|
||||||
"PO-Revision-Date: 2026-07-14 05:07+0300\n"
|
"PO-Revision-Date: 2026-07-15 06:45+0300\n"
|
||||||
"Last-Translator: Dushnilin <dushnilin@gmail.com>\n"
|
"Last-Translator: Dushnilin <dushnilin@gmail.com>\n"
|
||||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||||
"Language: \n"
|
"Language: \n"
|
||||||
@@ -1064,7 +1064,7 @@ msgstr ""
|
|||||||
msgid "Faster server search interval"
|
msgid "Faster server search interval"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/tachyon/methods/custom/getDashboardSections.ts:638
|
#: src/tachyon/methods/custom/getDashboardSections.ts:647
|
||||||
msgid "Fastest"
|
msgid "Fastest"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -1347,46 +1347,6 @@ msgstr ""
|
|||||||
msgid "Invalid format. Use an IP address or CIDR subnet"
|
msgid "Invalid format. Use an IP address or CIDR subnet"
|
||||||
msgstr ""
|
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
|
#: src/validators/validateHysteriaUrl.ts:104
|
||||||
msgid "Invalid HY2 URL: insecure must be 0 or 1"
|
msgid "Invalid HY2 URL: insecure must be 0 or 1"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -3089,7 +3049,6 @@ msgstr ""
|
|||||||
msgid "unique UCI identifier"
|
msgid "unique UCI identifier"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/tachyon/helpers/singBoxVariant.ts:28
|
|
||||||
#: src/tachyon/services/systemInfo.service.ts:10
|
#: src/tachyon/services/systemInfo.service.ts:10
|
||||||
#: src/tachyon/services/systemInfo.service.ts:11
|
#: src/tachyon/services/systemInfo.service.ts:11
|
||||||
#: src/tachyon/services/systemInfo.service.ts:12
|
#: 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:22
|
||||||
#: src/tachyon/services/systemInfo.service.ts:25
|
#: src/tachyon/services/systemInfo.service.ts:25
|
||||||
#: src/tachyon/services/systemInfo.service.ts:26
|
#: src/tachyon/services/systemInfo.service.ts:26
|
||||||
|
#: src/tachyon/helpers/singBoxVariant.ts:28
|
||||||
#: src/tachyon/tabs/updates/initController.ts:329
|
#: src/tachyon/tabs/updates/initController.ts:329
|
||||||
#: src/tachyon/tabs/diagnostic/checks/runInboundsCheck.ts:25
|
#: src/tachyon/tabs/diagnostic/checks/runInboundsCheck.ts:25
|
||||||
msgid "unknown"
|
msgid "unknown"
|
||||||
@@ -3166,8 +3126,8 @@ msgstr ""
|
|||||||
msgid "Uploaded"
|
msgid "Uploaded"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/validators/validateProxyUrl.ts:47
|
#: src/validators/validateProxyUrl.ts:42
|
||||||
msgid "URL must start with vless://, vmess://, ss://, trojan://, socks4://, socks4a://, socks5://, http://, https://, hysteria2://, or hy2://"
|
msgid "URL must start with vless://, vmess://, ss://, trojan://, socks4://, socks4a://, socks5://, hysteria2://, or hy2://"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/validators/validateUrl.ts:18
|
#: src/validators/validateUrl.ts:18
|
||||||
@@ -3293,7 +3253,6 @@ msgstr ""
|
|||||||
#: src/validators/validateDns.ts:29
|
#: src/validators/validateDns.ts:29
|
||||||
#: src/validators/validateDomain.ts:57
|
#: src/validators/validateDomain.ts:57
|
||||||
#: src/validators/validateDomain.ts:76
|
#: src/validators/validateDomain.ts:76
|
||||||
#: src/validators/validateHttpProxyUrl.ts:89
|
|
||||||
#: src/validators/validateHysteriaUrl.ts:151
|
#: src/validators/validateHysteriaUrl.ts:151
|
||||||
#: src/validators/validateIp.ts:25
|
#: src/validators/validateIp.ts:25
|
||||||
#: src/validators/validateOutboundJson.ts:118
|
#: src/validators/validateOutboundJson.ts:118
|
||||||
@@ -3338,7 +3297,7 @@ msgid "VLESS flow"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: ../luci-app-tachyon/htdocs/luci-static/resources/view/tachyon/section.js:7137
|
#: ../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 ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/tachyon/tabs/diagnostic/checks/runInboundsCheck.ts:70
|
#: src/tachyon/tabs/diagnostic/checks/runInboundsCheck.ts:70
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: TACHYON\n"
|
"Project-Id-Version: TACHYON\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2026-07-14 08:07+0300\n"
|
"POT-Creation-Date: 2026-07-15 09:45+0300\n"
|
||||||
"PO-Revision-Date: 2026-07-14 08:07+0300\n"
|
"PO-Revision-Date: 2026-07-15 09:45+0300\n"
|
||||||
"Last-Translator: Dushnilin\n"
|
"Last-Translator: Dushnilin\n"
|
||||||
"Language-Team: none\n"
|
"Language-Team: none\n"
|
||||||
"Language: ru\n"
|
"Language: ru\n"
|
||||||
@@ -926,36 +926,6 @@ msgstr "Неверный домен"
|
|||||||
msgid "Invalid format. Use an IP address or CIDR subnet"
|
msgid "Invalid format. Use an IP address or CIDR subnet"
|
||||||
msgstr "Неверный формат. Используйте IP-адрес или подсеть CIDR"
|
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"
|
msgid "Invalid HY2 URL: insecure must be 0 or 1"
|
||||||
msgstr "Неверный URL Hysteria2: параметр insecure должен быть 0 или 1"
|
msgstr "Неверный URL Hysteria2: параметр insecure должен быть 0 или 1"
|
||||||
|
|
||||||
@@ -2204,8 +2174,8 @@ msgstr "Исходящий"
|
|||||||
msgid "Uploaded"
|
msgid "Uploaded"
|
||||||
msgstr "Отдано"
|
msgstr "Отдано"
|
||||||
|
|
||||||
msgid "URL must start with vless://, vmess://, ss://, trojan://, socks4://, socks4a://, socks5://, http://, https://, hysteria2://, or hy2://"
|
msgid "URL must start with vless://, vmess://, ss://, trojan://, socks4://, socks4a://, socks5://, hysteria2://, or hy2://"
|
||||||
msgstr "URL должен начинаться с vless://, vmess://, ss://, trojan://, socks4://, socks4a://, socks5://, http://, https://, hysteria2:// или hy2://"
|
msgstr ""
|
||||||
|
|
||||||
msgid "URL must use one of the following protocols:"
|
msgid "URL must use one of the following protocols:"
|
||||||
msgstr "URL должен использовать один из следующих протоколов:"
|
msgstr "URL должен использовать один из следующих протоколов:"
|
||||||
@@ -2306,8 +2276,8 @@ msgstr "Видимое имя этой секции"
|
|||||||
msgid "VLESS flow"
|
msgid "VLESS flow"
|
||||||
msgstr "VLESS flow"
|
msgstr "VLESS flow"
|
||||||
|
|
||||||
msgid "vless://, vmess://, ss://, trojan://, socks4/5://, http(s)://, hy2/hysteria2:// links"
|
msgid "vless://, vmess://, ss://, trojan://, socks4/5://, hy2/hysteria2:// links"
|
||||||
msgstr "ссылки vless://, vmess://, ss://, trojan://, socks4/5://, http(s)://, hy2/hysteria2://"
|
msgstr ""
|
||||||
|
|
||||||
msgid "WAN"
|
msgid "WAN"
|
||||||
msgstr "WAN"
|
msgstr "WAN"
|
||||||
|
|||||||
@@ -1,19 +1,6 @@
|
|||||||
const COPYABLE_PROXY_URI_RE =
|
const COPYABLE_PROXY_URI_RE =
|
||||||
/^(vless|vmess|trojan|ss|ssr|hysteria2|hy2|tuic|socks4|socks4a|socks5):\/\//i;
|
/^(vless|vmess|trojan|ss|ssr|hysteria2|hy2|tuic|socks4|socks4a|socks5):\/\//i;
|
||||||
const HTTP_PROXY_URI_RE =
|
|
||||||
/^https?:\/\/(?:[^/@\s]+(?::[^/@\s]*)?@)?[^:/?#@\s]+:(\d{1,5})$/i;
|
|
||||||
|
|
||||||
export function isCopyableProxyLink(link?: string) {
|
export function isCopyableProxyLink(link?: string) {
|
||||||
const value = (link || '').trim();
|
return COPYABLE_PROXY_URI_RE.test((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;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,8 +13,6 @@ const copyableLinks = [
|
|||||||
'socks4://example.com:1080',
|
'socks4://example.com:1080',
|
||||||
'socks4a://example.com:1080',
|
'socks4a://example.com:1080',
|
||||||
'socks5://user:pass@example.com:1080',
|
'socks5://user:pass@example.com:1080',
|
||||||
'http://example.com:80',
|
|
||||||
'https://user:pass@example.com:443',
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const nonCopyableLinks = [
|
const nonCopyableLinks = [
|
||||||
@@ -22,6 +20,8 @@ const nonCopyableLinks = [
|
|||||||
'direct',
|
'direct',
|
||||||
'block',
|
'block',
|
||||||
'urltest',
|
'urltest',
|
||||||
|
'http://example.com:80',
|
||||||
|
'https://user:pass@example.com:443',
|
||||||
'https://example.com/subscription',
|
'https://example.com/subscription',
|
||||||
'https://example.com',
|
'https://example.com',
|
||||||
'http://example.com:99999',
|
'http://example.com:99999',
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { getConfigSections } from './getConfigSections';
|
import { getConfigSections } from './getConfigSections';
|
||||||
import { ClashAPI, Tachyon } from '../../types';
|
import { ClashAPI, Tachyon } from '../../types';
|
||||||
import {
|
import {
|
||||||
canUseDirectClashApi,
|
canUseDirectClashApi,
|
||||||
@@ -127,6 +127,7 @@ type ChildType =
|
|||||||
| 'priority_level';
|
| 'priority_level';
|
||||||
|
|
||||||
const DASHBOARD_SECTION_CACHE_DIR = '/var/run/tachyon/section-cache';
|
const DASHBOARD_SECTION_CACHE_DIR = '/var/run/tachyon/section-cache';
|
||||||
|
const CLASH_API_FETCH_TIMEOUT_MS = 5000;
|
||||||
|
|
||||||
function getDisplayName(section: Tachyon.ConfigSection) {
|
function getDisplayName(section: Tachyon.ConfigSection) {
|
||||||
return section.label || section['.name'];
|
return section.label || section['.name'];
|
||||||
@@ -149,10 +150,16 @@ async function getClashApiProxies(
|
|||||||
): Promise<Tachyon.MethodResponse<ClashAPI.Proxies>> {
|
): Promise<Tachyon.MethodResponse<ClashAPI.Proxies>> {
|
||||||
if (canFetchClashApiDirectly()) {
|
if (canFetchClashApiDirectly()) {
|
||||||
const secret = getClashApiSecret(configSections);
|
const secret = getClashApiSecret(configSections);
|
||||||
|
const controller = new AbortController();
|
||||||
|
const timeoutId = setTimeout(
|
||||||
|
() => controller.abort(),
|
||||||
|
CLASH_API_FETCH_TIMEOUT_MS,
|
||||||
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${getClashHttpUrl()}/proxies`, {
|
const response = await fetch(`${getClashHttpUrl()}/proxies`, {
|
||||||
headers: secret ? { Authorization: `Bearer ${secret}` } : undefined,
|
headers: secret ? { Authorization: `Bearer ${secret}` } : undefined,
|
||||||
|
signal: controller.signal,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
@@ -163,6 +170,8 @@ async function getClashApiProxies(
|
|||||||
}
|
}
|
||||||
} catch (_error) {
|
} catch (_error) {
|
||||||
// Fall back to rpcd below for controllers unavailable from the browser.
|
// 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(result.success).toBe(true);
|
||||||
expect(fetchMock).toHaveBeenCalledWith(
|
expect(fetchMock).toHaveBeenCalledWith(
|
||||||
'http://router.example:9090/proxies',
|
'http://router.example:9090/proxies',
|
||||||
{
|
expect.objectContaining({
|
||||||
headers: { Authorization: 'Bearer secret' },
|
headers: { Authorization: 'Bearer secret' },
|
||||||
},
|
signal: expect.anything(),
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
expect(mocks.getClashApiProxies).not.toHaveBeenCalled();
|
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 () => {
|
it('uses rpcd fallback when direct Clash API access is unsafe', async () => {
|
||||||
mocks.getConfigSections.mockResolvedValue([proxySection()]);
|
mocks.getConfigSections.mockResolvedValue([proxySection()]);
|
||||||
const fetchMock = vi.fn().mockResolvedValue({
|
const fetchMock = vi.fn().mockResolvedValue({
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ export async function runNftCheck() {
|
|||||||
value: '',
|
value: '',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
state: data.rules_mangle_counters ? 'success' : 'error',
|
state: data.rules_mangle_counters ? 'success' : 'warning',
|
||||||
key: _('Rules mangle counters'),
|
key: _('Rules mangle counters'),
|
||||||
value: '',
|
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);
|
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', () => {
|
it('accepts a URLTest outbound with referenced outbounds', () => {
|
||||||
const result = validateOutboundJson(
|
const result = validateOutboundJson(
|
||||||
'{"type":"urltest","tag":"auto","outbounds":["proxy-1"],"url":"https://www.gstatic.com/generate_204"}',
|
'{"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'],
|
['socks4', 'socks4://127.0.0.1:1080'],
|
||||||
['socks4a', 'socks4a://example.com:1080'],
|
['socks4a', 'socks4a://example.com:1080'],
|
||||||
['socks5', 'socks5://user:pass@example.com:1080'],
|
['socks5', 'socks5://user:pass@example.com:1080'],
|
||||||
['http proxy', 'http://127.0.0.1:80'],
|
|
||||||
['https proxy', 'https://user:pass@example.com:443'],
|
|
||||||
['hysteria2', 'hysteria2://password@example.com:443'],
|
['hysteria2', 'hysteria2://password@example.com:443'],
|
||||||
['hy2', 'hy2://password@example.com:443'],
|
['hy2', 'hy2://password@example.com:443'],
|
||||||
];
|
];
|
||||||
@@ -57,15 +55,14 @@ describe('validateProxyUrl', () => {
|
|||||||
expect(res.valid).toBe(false);
|
expect(res.valid).toBe(false);
|
||||||
expect(res.message).toContain('vmess://');
|
expect(res.message).toContain('vmess://');
|
||||||
expect(res.message).toContain('socks4a://');
|
expect(res.message).toContain('socks4a://');
|
||||||
expect(res.message).toContain('http://');
|
|
||||||
expect(res.message).toContain('hy2://');
|
expect(res.message).toContain('hy2://');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('rejects HTTP URLs that look like subscription URLs rather than proxy endpoints', () => {
|
it('rejects native HTTP proxy URLs', () => {
|
||||||
const res = validateProxyUrl('https://example.com/subscription');
|
const res = validateProxyUrl('https://user:pass@example.com:443');
|
||||||
|
|
||||||
expect(res.valid).toBe(false);
|
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', () => {
|
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 { validateTrojanUrl } from './validateTrojanUrl';
|
||||||
import { validateSocksUrl } from './validateSocksUrl';
|
import { validateSocksUrl } from './validateSocksUrl';
|
||||||
import { validateHysteria2Url } from './validateHysteriaUrl';
|
import { validateHysteria2Url } from './validateHysteriaUrl';
|
||||||
import { validateHttpProxyUrl } from './validateHttpProxyUrl';
|
|
||||||
|
|
||||||
// TODO refactor current validation and add tests
|
// TODO refactor current validation and add tests
|
||||||
export function validateProxyUrl(url: string): ValidationResult {
|
export function validateProxyUrl(url: string): ValidationResult {
|
||||||
@@ -31,10 +30,6 @@ export function validateProxyUrl(url: string): ValidationResult {
|
|||||||
return validateSocksUrl(trimmedUrl);
|
return validateSocksUrl(trimmedUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (/^https?:\/\//.test(trimmedUrl)) {
|
|
||||||
return validateHttpProxyUrl(trimmedUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
if (
|
||||||
trimmedUrl.startsWith('hysteria2://') ||
|
trimmedUrl.startsWith('hysteria2://') ||
|
||||||
trimmedUrl.startsWith('hy2://')
|
trimmedUrl.startsWith('hy2://')
|
||||||
@@ -45,7 +40,7 @@ export function validateProxyUrl(url: string): ValidationResult {
|
|||||||
return {
|
return {
|
||||||
valid: false,
|
valid: false,
|
||||||
message: _(
|
message: _(
|
||||||
'URL must start with vless://, vmess://, ss://, trojan://, socks4://, socks4a://, socks5://, http://, https://, hysteria2://, or hy2://',
|
'URL must start with vless://, vmess://, ss://, trojan://, socks4://, socks4a://, socks5://, hysteria2://, or hy2://',
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-2
@@ -765,6 +765,8 @@ function select_dns_owner(legacy) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let dnsmasq_failsafe_restore;
|
||||||
|
|
||||||
function installer_restore_dnsmasq(bin_path, legacy) {
|
function installer_restore_dnsmasq(bin_path, legacy) {
|
||||||
if (path_executable(bin_path) && run_args([ bin_path, "restore_dnsmasq" ]))
|
if (path_executable(bin_path) && run_args([ bin_path, "restore_dnsmasq" ]))
|
||||||
return true;
|
return true;
|
||||||
@@ -1098,7 +1100,7 @@ function dnsmasq_restore_default_instance() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function dnsmasq_failsafe_restore() {
|
dnsmasq_failsafe_restore = function() {
|
||||||
if (!uci_available())
|
if (!uci_available())
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
@@ -1113,7 +1115,7 @@ function dnsmasq_failsafe_restore() {
|
|||||||
uci_commit("dhcp");
|
uci_commit("dhcp");
|
||||||
restart_dnsmasq();
|
restart_dnsmasq();
|
||||||
return true;
|
return true;
|
||||||
}
|
};
|
||||||
|
|
||||||
function release_version_valid(value) {
|
function release_version_valid(value) {
|
||||||
return match(as_string(value), /^[0-9]+[.][0-9]+[.][0-9]+$/) != null;
|
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
|
// src/validators/validateProxyUrl.ts
|
||||||
function validateProxyUrl(url) {
|
function validateProxyUrl(url) {
|
||||||
const trimmedUrl = url.trim();
|
const trimmedUrl = url.trim();
|
||||||
@@ -906,16 +827,13 @@ function validateProxyUrl(url) {
|
|||||||
if (/^socks(4|4a|5):\/\//.test(trimmedUrl)) {
|
if (/^socks(4|4a|5):\/\//.test(trimmedUrl)) {
|
||||||
return validateSocksUrl(trimmedUrl);
|
return validateSocksUrl(trimmedUrl);
|
||||||
}
|
}
|
||||||
if (/^https?:\/\//.test(trimmedUrl)) {
|
|
||||||
return validateHttpProxyUrl(trimmedUrl);
|
|
||||||
}
|
|
||||||
if (trimmedUrl.startsWith("hysteria2://") || trimmedUrl.startsWith("hy2://")) {
|
if (trimmedUrl.startsWith("hysteria2://") || trimmedUrl.startsWith("hy2://")) {
|
||||||
return validateHysteria2Url(trimmedUrl);
|
return validateHysteria2Url(trimmedUrl);
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
valid: false,
|
valid: false,
|
||||||
message: _(
|
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
|
// src/helpers/isCopyableProxyLink.ts
|
||||||
var COPYABLE_PROXY_URI_RE = /^(vless|vmess|trojan|ss|ssr|hysteria2|hy2|tuic|socks4|socks4a|socks5):\/\//i;
|
var COPYABLE_PROXY_URI_RE = /^(vless|vmess|trojan|ss|ssr|hysteria2|hy2|tuic|socks4|socks4a|socks5):\/\//i;
|
||||||
var HTTP_PROXY_URI_RE = /^https?:\/\/(?:[^/@\s]+(?::[^/@\s]*)?@)?[^:/?#@\s]+:(\d{1,5})$/i;
|
|
||||||
function isCopyableProxyLink(link) {
|
function isCopyableProxyLink(link) {
|
||||||
const value = (link || "").trim();
|
return COPYABLE_PROXY_URI_RE.test((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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// src/icons/renderLoaderCircleIcon24.ts
|
// src/icons/renderLoaderCircleIcon24.ts
|
||||||
@@ -3177,6 +3085,7 @@ var TachyonShellMethods = {
|
|||||||
|
|
||||||
// src/tachyon/methods/custom/getDashboardSections.ts
|
// src/tachyon/methods/custom/getDashboardSections.ts
|
||||||
var DASHBOARD_SECTION_CACHE_DIR = "/var/run/tachyon/section-cache";
|
var DASHBOARD_SECTION_CACHE_DIR = "/var/run/tachyon/section-cache";
|
||||||
|
var CLASH_API_FETCH_TIMEOUT_MS = 5e3;
|
||||||
function getDisplayName(section) {
|
function getDisplayName(section) {
|
||||||
return section.label || section[".name"];
|
return section.label || section[".name"];
|
||||||
}
|
}
|
||||||
@@ -3192,9 +3101,15 @@ function canFetchClashApiDirectly() {
|
|||||||
async function getClashApiProxies(configSections) {
|
async function getClashApiProxies(configSections) {
|
||||||
if (canFetchClashApiDirectly()) {
|
if (canFetchClashApiDirectly()) {
|
||||||
const secret = getClashApiSecret(configSections);
|
const secret = getClashApiSecret(configSections);
|
||||||
|
const controller = new AbortController();
|
||||||
|
const timeoutId = setTimeout(
|
||||||
|
() => controller.abort(),
|
||||||
|
CLASH_API_FETCH_TIMEOUT_MS
|
||||||
|
);
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${getClashHttpUrl()}/proxies`, {
|
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) {
|
if (response.ok) {
|
||||||
return {
|
return {
|
||||||
@@ -3203,6 +3118,8 @@ async function getClashApiProxies(configSections) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
} catch (_error) {
|
} catch (_error) {
|
||||||
|
} finally {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return TachyonShellMethods.getClashApiProxies();
|
return TachyonShellMethods.getClashApiProxies();
|
||||||
@@ -7992,7 +7909,7 @@ async function runNftCheck() {
|
|||||||
value: ""
|
value: ""
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
state: data.rules_mangle_counters ? "success" : "error",
|
state: data.rules_mangle_counters ? "success" : "warning",
|
||||||
key: _("Rules mangle counters"),
|
key: _("Rules mangle counters"),
|
||||||
value: ""
|
value: ""
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -7135,7 +7135,7 @@ function createSectionContent(section) {
|
|||||||
"selector_proxy_links",
|
"selector_proxy_links",
|
||||||
_("Connection URL"),
|
_("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");
|
o.depends("action", "connection");
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: TACHYON\n"
|
"Project-Id-Version: TACHYON\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2026-07-14 08:07+0300\n"
|
"POT-Creation-Date: 2026-07-15 09:45+0300\n"
|
||||||
"PO-Revision-Date: 2026-07-14 08:07+0300\n"
|
"PO-Revision-Date: 2026-07-15 09:45+0300\n"
|
||||||
"Last-Translator: Dushnilin\n"
|
"Last-Translator: Dushnilin\n"
|
||||||
"Language-Team: none\n"
|
"Language-Team: none\n"
|
||||||
"Language: ru\n"
|
"Language: ru\n"
|
||||||
@@ -926,36 +926,6 @@ msgstr "Неверный домен"
|
|||||||
msgid "Invalid format. Use an IP address or CIDR subnet"
|
msgid "Invalid format. Use an IP address or CIDR subnet"
|
||||||
msgstr "Неверный формат. Используйте IP-адрес или подсеть CIDR"
|
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"
|
msgid "Invalid HY2 URL: insecure must be 0 or 1"
|
||||||
msgstr "Неверный URL Hysteria2: параметр insecure должен быть 0 или 1"
|
msgstr "Неверный URL Hysteria2: параметр insecure должен быть 0 или 1"
|
||||||
|
|
||||||
@@ -2204,8 +2174,8 @@ msgstr "Исходящий"
|
|||||||
msgid "Uploaded"
|
msgid "Uploaded"
|
||||||
msgstr "Отдано"
|
msgstr "Отдано"
|
||||||
|
|
||||||
msgid "URL must start with vless://, vmess://, ss://, trojan://, socks4://, socks4a://, socks5://, http://, https://, hysteria2://, or hy2://"
|
msgid "URL must start with vless://, vmess://, ss://, trojan://, socks4://, socks4a://, socks5://, hysteria2://, or hy2://"
|
||||||
msgstr "URL должен начинаться с vless://, vmess://, ss://, trojan://, socks4://, socks4a://, socks5://, http://, https://, hysteria2:// или hy2://"
|
msgstr ""
|
||||||
|
|
||||||
msgid "URL must use one of the following protocols:"
|
msgid "URL must use one of the following protocols:"
|
||||||
msgstr "URL должен использовать один из следующих протоколов:"
|
msgstr "URL должен использовать один из следующих протоколов:"
|
||||||
@@ -2306,8 +2276,8 @@ msgstr "Видимое имя этой секции"
|
|||||||
msgid "VLESS flow"
|
msgid "VLESS flow"
|
||||||
msgstr "VLESS flow"
|
msgstr "VLESS flow"
|
||||||
|
|
||||||
msgid "vless://, vmess://, ss://, trojan://, socks4/5://, http(s)://, hy2/hysteria2:// links"
|
msgid "vless://, vmess://, ss://, trojan://, socks4/5://, hy2/hysteria2:// links"
|
||||||
msgstr "ссылки vless://, vmess://, ss://, trojan://, socks4/5://, http(s)://, hy2/hysteria2://"
|
msgstr ""
|
||||||
|
|
||||||
msgid "WAN"
|
msgid "WAN"
|
||||||
msgstr "WAN"
|
msgstr "WAN"
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: TACHYON\n"
|
"Project-Id-Version: TACHYON\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2026-07-14 05:07+0300\n"
|
"POT-Creation-Date: 2026-07-15 06:45+0300\n"
|
||||||
"PO-Revision-Date: 2026-07-14 05:07+0300\n"
|
"PO-Revision-Date: 2026-07-15 06:45+0300\n"
|
||||||
"Last-Translator: Dushnilin <dushnilin@gmail.com>\n"
|
"Last-Translator: Dushnilin <dushnilin@gmail.com>\n"
|
||||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||||
"Language: \n"
|
"Language: \n"
|
||||||
@@ -1064,7 +1064,7 @@ msgstr ""
|
|||||||
msgid "Faster server search interval"
|
msgid "Faster server search interval"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/tachyon/methods/custom/getDashboardSections.ts:638
|
#: src/tachyon/methods/custom/getDashboardSections.ts:647
|
||||||
msgid "Fastest"
|
msgid "Fastest"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@@ -1347,46 +1347,6 @@ msgstr ""
|
|||||||
msgid "Invalid format. Use an IP address or CIDR subnet"
|
msgid "Invalid format. Use an IP address or CIDR subnet"
|
||||||
msgstr ""
|
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
|
#: src/validators/validateHysteriaUrl.ts:104
|
||||||
msgid "Invalid HY2 URL: insecure must be 0 or 1"
|
msgid "Invalid HY2 URL: insecure must be 0 or 1"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
@@ -3089,7 +3049,6 @@ msgstr ""
|
|||||||
msgid "unique UCI identifier"
|
msgid "unique UCI identifier"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/tachyon/helpers/singBoxVariant.ts:28
|
|
||||||
#: src/tachyon/services/systemInfo.service.ts:10
|
#: src/tachyon/services/systemInfo.service.ts:10
|
||||||
#: src/tachyon/services/systemInfo.service.ts:11
|
#: src/tachyon/services/systemInfo.service.ts:11
|
||||||
#: src/tachyon/services/systemInfo.service.ts:12
|
#: 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:22
|
||||||
#: src/tachyon/services/systemInfo.service.ts:25
|
#: src/tachyon/services/systemInfo.service.ts:25
|
||||||
#: src/tachyon/services/systemInfo.service.ts:26
|
#: src/tachyon/services/systemInfo.service.ts:26
|
||||||
|
#: src/tachyon/helpers/singBoxVariant.ts:28
|
||||||
#: src/tachyon/tabs/updates/initController.ts:329
|
#: src/tachyon/tabs/updates/initController.ts:329
|
||||||
#: src/tachyon/tabs/diagnostic/checks/runInboundsCheck.ts:25
|
#: src/tachyon/tabs/diagnostic/checks/runInboundsCheck.ts:25
|
||||||
msgid "unknown"
|
msgid "unknown"
|
||||||
@@ -3166,8 +3126,8 @@ msgstr ""
|
|||||||
msgid "Uploaded"
|
msgid "Uploaded"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/validators/validateProxyUrl.ts:47
|
#: src/validators/validateProxyUrl.ts:42
|
||||||
msgid "URL must start with vless://, vmess://, ss://, trojan://, socks4://, socks4a://, socks5://, http://, https://, hysteria2://, or hy2://"
|
msgid "URL must start with vless://, vmess://, ss://, trojan://, socks4://, socks4a://, socks5://, hysteria2://, or hy2://"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/validators/validateUrl.ts:18
|
#: src/validators/validateUrl.ts:18
|
||||||
@@ -3293,7 +3253,6 @@ msgstr ""
|
|||||||
#: src/validators/validateDns.ts:29
|
#: src/validators/validateDns.ts:29
|
||||||
#: src/validators/validateDomain.ts:57
|
#: src/validators/validateDomain.ts:57
|
||||||
#: src/validators/validateDomain.ts:76
|
#: src/validators/validateDomain.ts:76
|
||||||
#: src/validators/validateHttpProxyUrl.ts:89
|
|
||||||
#: src/validators/validateHysteriaUrl.ts:151
|
#: src/validators/validateHysteriaUrl.ts:151
|
||||||
#: src/validators/validateIp.ts:25
|
#: src/validators/validateIp.ts:25
|
||||||
#: src/validators/validateOutboundJson.ts:118
|
#: src/validators/validateOutboundJson.ts:118
|
||||||
@@ -3338,7 +3297,7 @@ msgid "VLESS flow"
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: ../luci-app-tachyon/htdocs/luci-static/resources/view/tachyon/section.js:7137
|
#: ../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 ""
|
msgstr ""
|
||||||
|
|
||||||
#: src/tachyon/tabs/diagnostic/checks/runInboundsCheck.ts:70
|
#: src/tachyon/tabs/diagnostic/checks/runInboundsCheck.ts:70
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
config settings 'settings'
|
config settings 'settings'
|
||||||
option config_version '1.0.2'
|
option config_version '1.0.5'
|
||||||
list applied_migrations 'interface_sections'
|
list applied_migrations 'interface_sections'
|
||||||
list applied_migrations 'enable_component_checks'
|
list applied_migrations 'enable_component_checks'
|
||||||
|
list applied_migrations 'http_connection_urls'
|
||||||
option dns_type 'udp'
|
option dns_type 'udp'
|
||||||
list dns_server '77.88.8.8'
|
list dns_server '77.88.8.8'
|
||||||
list bootstrap_dns_server '77.88.8.8'
|
list bootstrap_dns_server '77.88.8.8'
|
||||||
|
|||||||
@@ -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_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_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_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_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_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 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 GITHUB_RAW_URL = getenv("GITHUB_RAW_URL") || "https://raw.githubusercontent.com/itdoginfo/allow-domains/main";
|
||||||
const BUILTIN_SUBNET_URLS = {
|
const BUILTIN_SUBNET_URLS = {
|
||||||
twitter: getenv("SUBNETS_TWITTER") || GITHUB_RAW_URL + "/Subnets/IPv4/twitter.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",
|
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",
|
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",
|
roblox: [ getenv("SUBNETS_ROBLOX") || GITHUB_RAW_URL + "/Subnets/IPv4/roblox.lst" ],
|
||||||
telegram: getenv("SUBNETS_TELERAM") || GITHUB_RAW_URL + "/Subnets/IPv4/telegram.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",
|
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",
|
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",
|
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",
|
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"
|
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 rule_config = null;
|
||||||
let routing_rulesets_module_value = 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))
|
if (!singbox_rulesets_module().is_community(service))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
let url = BUILTIN_SUBNET_URLS[as_string(service)];
|
let urls = BUILTIN_SUBNET_URLS[as_string(service)];
|
||||||
if (url == null)
|
if (type(urls) != "array")
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
let tmpfile = temp_path();
|
for (let url in urls) {
|
||||||
if (tmpfile == "") {
|
let tmpfile = temp_path();
|
||||||
ok = false;
|
if (tmpfile == "") {
|
||||||
continue;
|
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);
|
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;
|
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);
|
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] || "";
|
let mode = ARGV[0] || "";
|
||||||
|
|
||||||
if (mode == "json-length")
|
if (mode == "json-length")
|
||||||
@@ -2867,6 +2874,8 @@ else if (mode == "update-is-due")
|
|||||||
update_is_due(ARGV[1], ARGV[2], ARGV[3]);
|
update_is_due(ARGV[1], ARGV[2], ARGV[3]);
|
||||||
else if (mode == "duration-to-seconds")
|
else if (mode == "duration-to-seconds")
|
||||||
duration_to_seconds(ARGV[1]);
|
duration_to_seconds(ARGV[1]);
|
||||||
|
else if (mode == "builtin-subnet-urls")
|
||||||
|
print_builtin_subnet_urls(ARGV[1]);
|
||||||
else if (mode == "due-check-cron-schedule")
|
else if (mode == "due-check-cron-schedule")
|
||||||
due_check_cron_schedule(ARGV[1]);
|
due_check_cron_schedule(ARGV[1]);
|
||||||
else if (mode == "list-update-cron-job")
|
else if (mode == "list-update-cron-job")
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
let fs = require("fs");
|
let fs = require("fs");
|
||||||
let common = require("core.common");
|
let common = require("core.common");
|
||||||
let uci_core = require("core.uci");
|
let uci_core = require("core.uci");
|
||||||
|
let core_url = require("core.url");
|
||||||
let constants_module = require("core.constants");
|
let constants_module = require("core.constants");
|
||||||
let singbox_constants_module = require("singbox.constants");
|
let singbox_constants_module = require("singbox.constants");
|
||||||
let domain_config = require("config.domain");
|
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");
|
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 = [
|
const MIGRATIONS = [
|
||||||
{ id: "interface_sections", run: migrate_interface_sections },
|
{ 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) {
|
function apply_migrations(ctx) {
|
||||||
@@ -1215,8 +1317,8 @@ function apply_migrations(ctx) {
|
|||||||
if (added)
|
if (added)
|
||||||
set_list_option(ctx, ctx.model.settings, APPLIED_MIGRATIONS_OPTION, applied);
|
set_list_option(ctx, ctx.model.settings, APPLIED_MIGRATIONS_OPTION, applied);
|
||||||
|
|
||||||
if (release_at_most(ctx, "1.0.1"))
|
if (release_at_most(ctx, "1.0.4"))
|
||||||
set_option(ctx, ctx.model.settings, CONFIG_VERSION_OPTION, "1.0.2");
|
set_option(ctx, ctx.model.settings, CONFIG_VERSION_OPTION, "1.0.5");
|
||||||
}
|
}
|
||||||
|
|
||||||
function migrate_podkop_model(model, constants) {
|
function migrate_podkop_model(model, constants) {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ let connections = require("config.connections");
|
|||||||
|
|
||||||
const CONFIG_NAME = getenv("TACHYON_CONFIG_NAME") || "tachyon";
|
const CONFIG_NAME = getenv("TACHYON_CONFIG_NAME") || "tachyon";
|
||||||
const DEFAULT_LATENCY_TEST_URL = "https://www.gstatic.com/generate_204";
|
const DEFAULT_LATENCY_TEST_URL = "https://www.gstatic.com/generate_204";
|
||||||
|
const TAILSCALE_FWMARK_MASK = 0x00ff0000;
|
||||||
|
|
||||||
function as_string(value) {
|
function as_string(value) {
|
||||||
return value == null ? "" : "" + value;
|
return value == null ? "" : "" + value;
|
||||||
@@ -1658,11 +1659,21 @@ function validate_list_update_settings(settings) {
|
|||||||
function validate_runtime_mark_ranges_context(context) {
|
function validate_runtime_mark_ranges_context(context) {
|
||||||
let fakeip_mark = parse_number(context.nft_fakeip_mark);
|
let fakeip_mark = parse_number(context.nft_fakeip_mark);
|
||||||
let outbound_mark = parse_number(context.nft_outbound_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 = [
|
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, 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 ],
|
[ "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, 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) {
|
for (let range in ranges) {
|
||||||
|
|||||||
@@ -48,8 +48,8 @@ function constants_map() {
|
|||||||
c.NFT_DISCORD_SET_NAME = env("NFT_DISCORD_SET_NAME", "tachyon_discord_subnets");
|
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_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_INTERFACE_SET_NAME = env("NFT_INTERFACE_SET_NAME", "tachyon_interfaces");
|
||||||
c.NFT_FAKEIP_MARK = env("NFT_FAKEIP_MARK", "0x00100000");
|
c.NFT_FAKEIP_MARK = env("NFT_FAKEIP_MARK", "0x04000000");
|
||||||
c.NFT_OUTBOUND_MARK = env("NFT_OUTBOUND_MARK", "0x00200000");
|
c.NFT_OUTBOUND_MARK = env("NFT_OUTBOUND_MARK", "0x08000000");
|
||||||
|
|
||||||
c.SB_REQUIRED_VERSION = env("SB_REQUIRED_VERSION", "1.12.0");
|
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");
|
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_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_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_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_BASE = env("ZAPRET2_QUEUE_BASE", "4300");
|
||||||
c.ZAPRET2_QUEUE_RANGE_SIZE = env("ZAPRET2_QUEUE_RANGE_SIZE", "256");
|
c.ZAPRET2_QUEUE_RANGE_SIZE = env("ZAPRET2_QUEUE_RANGE_SIZE", "256");
|
||||||
c.ZAPRET2_NFQWS2_RESPAWN_DELAY = env("ZAPRET2_NFQWS2_RESPAWN_DELAY", "5");
|
c.ZAPRET2_NFQWS2_RESPAWN_DELAY = env("ZAPRET2_NFQWS2_RESPAWN_DELAY", "5");
|
||||||
|
|||||||
@@ -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 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 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_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_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_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";
|
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) {
|
function clash_latency_endpoint(base_url, proxy_tag, proxy_type) {
|
||||||
proxy_type = as_string(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 + "/group/" + clash_urlencode(proxy_tag) + "/delay";
|
||||||
return base_url + "/proxies/" + 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 ]);
|
module_success(SERVICE_UI_UC, [ "latency-progress-state", progress_path, count, total, failed ]);
|
||||||
|
|
||||||
let proxy_types = clash_proxy_type_map(base_url, auth);
|
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]) ];
|
let args = [ "curl", "-G", "-s", clash_latency_endpoint(base_url, proxy_tag, proxy_types[proxy_tag]) ];
|
||||||
for (let item in auth) push(args, item);
|
for (let item in auth) push(args, item);
|
||||||
push(args, "--data-urlencode");
|
push(args, "--data-urlencode");
|
||||||
|
|||||||
@@ -636,7 +636,8 @@ function section_needs_priority_sets(section) {
|
|||||||
|
|
||||||
function nft_create_priority_chains(table) {
|
function nft_create_priority_chains(table) {
|
||||||
return nft_create_chain(table, "priority_rules", "{ }") &&
|
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) {
|
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))
|
if (!nft_add_set_elements(table, interface_set, interface))
|
||||||
return false;
|
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_chain(table, "mangle_output", "{ type route hook output priority -150; policy accept; }") ||
|
||||||
!nft_create_priority_chains(table) ||
|
!nft_create_priority_chains(table) ||
|
||||||
!nft_create_chain(table, "proxy", "{ type filter hook prerouting priority -100; policy accept; }"))
|
!nft_create_chain(table, "proxy", "{ type filter hook prerouting priority -100; policy accept; }"))
|
||||||
|
|||||||
@@ -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_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_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_INTERFACE_SET_NAME = constant_value("NFT_INTERFACE_SET_NAME", "tachyon_interfaces");
|
||||||
const NFT_FAKEIP_MARK = constant_value("NFT_FAKEIP_MARK", "0x00100000");
|
const NFT_FAKEIP_MARK = constant_value("NFT_FAKEIP_MARK", "0x04000000");
|
||||||
const NFT_OUTBOUND_MARK = constant_value("NFT_OUTBOUND_MARK", "0x00200000");
|
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_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");
|
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 = constant_value("ZAPRET_DESYNC_MARK", "0x40000000");
|
||||||
const ZAPRET_DESYNC_MARK_POSTNAT = constant_value("ZAPRET_DESYNC_MARK_POSTNAT", "0x20000000");
|
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_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_QUEUE_BASE = constant_value("ZAPRET2_QUEUE_BASE", "4300");
|
||||||
const ZAPRET2_DESYNC_MARK = constant_value("ZAPRET2_DESYNC_MARK", "0x40000000");
|
const ZAPRET2_DESYNC_MARK = constant_value("ZAPRET2_DESYNC_MARK", "0x40000000");
|
||||||
const ZAPRET2_DESYNC_MARK_POSTNAT = constant_value("ZAPRET2_DESYNC_MARK_POSTNAT", "0x20000000");
|
const ZAPRET2_DESYNC_MARK_POSTNAT = constant_value("ZAPRET2_DESYNC_MARK_POSTNAT", "0x20000000");
|
||||||
|
|||||||
@@ -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 RUNTIME_STABLE_MIN_AGE = getenv("TACHYON_RUNTIME_STABLE_MIN_AGE") || "2";
|
||||||
const NFT_TABLE_NAME = getenv("NFT_TABLE_NAME") || "TachyonTable";
|
const NFT_TABLE_NAME = getenv("NFT_TABLE_NAME") || "TachyonTable";
|
||||||
const RT_TABLE_NAME = getenv("RT_TABLE_NAME") || "tachyon";
|
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 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 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";
|
const ZAPRET2_PROVIDER_NFQWS2_BIN = getenv("ZAPRET2_PROVIDER_NFQWS2_BIN") || "/opt/zapret2/nfq2/nfqws2";
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ const SERVICE_MIXED_INBOUND_ADDRESS = "127.0.0.1";
|
|||||||
const SERVICE_MIXED_INBOUND_PORT = 4534;
|
const SERVICE_MIXED_INBOUND_PORT = 4534;
|
||||||
const DIRECT_OUTBOUND_TAG = "direct-out";
|
const DIRECT_OUTBOUND_TAG = "direct-out";
|
||||||
const BYPASS_OUTBOUND_TAG = "bypass-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_INET4_RANGE = "198.18.0.0/15";
|
||||||
const FAKEIP_INET6_RANGE = "fc00::/18";
|
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_SING_BOX_FOLDER = "/tmp/sing-box";
|
||||||
const TMP_RULESET_FOLDER = TMP_SING_BOX_FOLDER + "/rulesets";
|
const TMP_RULESET_FOLDER = TMP_SING_BOX_FOLDER + "/rulesets";
|
||||||
const ZAPRET_ROUTE_MARK_BASE = 0x01000000;
|
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_LISTEN_ADDRESS = "127.0.0.1";
|
||||||
const BYEDPI_PORT_BASE = 1080;
|
const BYEDPI_PORT_BASE = 1080;
|
||||||
|
|
||||||
|
|||||||
@@ -498,7 +498,7 @@ function supported_subscription_outbound(outbound) {
|
|||||||
if (t == "direct" || t == "selector" || t == "urltest" || t == "dns" || t == "block")
|
if (t == "direct" || t == "selector" || t == "urltest" || t == "dns" || t == "block")
|
||||||
return false;
|
return false;
|
||||||
return t == "vless" || t == "vmess" || t == "trojan" || t == "shadowsocks" ||
|
return t == "vless" || t == "vmess" || t == "trojan" || t == "shadowsocks" ||
|
||||||
t == "socks" || t == "http" || t == "hysteria2";
|
t == "socks" || t == "hysteria2";
|
||||||
}
|
}
|
||||||
|
|
||||||
function outbound_uses_xhttp(outbound) {
|
function outbound_uses_xhttp(outbound) {
|
||||||
@@ -1747,32 +1747,6 @@ function apply_link_transport(outbound, query) {
|
|||||||
outbound.transport = result;
|
outbound.transport = result;
|
||||||
}
|
}
|
||||||
|
|
||||||
function manual_http_outbound(link, tag_name) {
|
|
||||||
let scheme = url_scheme(link);
|
|
||||||
let host = url_host(link);
|
|
||||||
let port = parse_port(url_port(link));
|
|
||||||
let path = url_path(link);
|
|
||||||
if (host == "" || port == null || (path != "" && path != "/") || index(link, "?") >= 0)
|
|
||||||
runtime_generate_unsupported("manual HTTP proxy link is invalid");
|
|
||||||
|
|
||||||
let outbound = {
|
|
||||||
type: "http",
|
|
||||||
tag: tag_name,
|
|
||||||
server: host,
|
|
||||||
server_port: port
|
|
||||||
};
|
|
||||||
let userinfo = url_userinfo(link);
|
|
||||||
if (userinfo != "") {
|
|
||||||
let colon = index(userinfo, ":");
|
|
||||||
outbound.username = colon >= 0 ? substr(userinfo, 0, colon) : userinfo;
|
|
||||||
if (colon >= 0)
|
|
||||||
outbound.password = substr(userinfo, colon + 1);
|
|
||||||
}
|
|
||||||
if (scheme == "https")
|
|
||||||
outbound.tls = { enabled: true };
|
|
||||||
return outbound;
|
|
||||||
}
|
|
||||||
|
|
||||||
function manual_socks_outbound(link, tag_name) {
|
function manual_socks_outbound(link, tag_name) {
|
||||||
let scheme = url_scheme(link);
|
let scheme = url_scheme(link);
|
||||||
let host = url_host(link);
|
let host = url_host(link);
|
||||||
@@ -2021,8 +1995,6 @@ function manual_link_outbound(link, tag_name) {
|
|||||||
|
|
||||||
link = url_strip_fragment_value(url_decode(link));
|
link = url_strip_fragment_value(url_decode(link));
|
||||||
scheme = url_scheme(link);
|
scheme = url_scheme(link);
|
||||||
if (scheme == "http" || scheme == "https")
|
|
||||||
return manual_http_outbound(link, tag_name);
|
|
||||||
if (scheme == "socks4" || scheme == "socks4a" || scheme == "socks5")
|
if (scheme == "socks4" || scheme == "socks4a" || scheme == "socks5")
|
||||||
return manual_socks_outbound(link, tag_name);
|
return manual_socks_outbound(link, tag_name);
|
||||||
if (scheme == "ss")
|
if (scheme == "ss")
|
||||||
|
|||||||
@@ -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_INTERFACE_SET_NAME = getenv("NFT_INTERFACE_SET_NAME") || "tachyon_interfaces";
|
||||||
const NFT_LOCALV4_SET_NAME = getenv("NFT_LOCALV4_SET_NAME") || "localv4";
|
const NFT_LOCALV4_SET_NAME = getenv("NFT_LOCALV4_SET_NAME") || "localv4";
|
||||||
const NFT_LOCALV6_SET_NAME = getenv("NFT_LOCALV6_SET_NAME") || "localv6";
|
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_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_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 SB_VARIANT_STATE_FILE = getenv("SB_VARIANT_STATE_FILE") || "/etc/tachyon/sing-box-variant";
|
||||||
|
|||||||
@@ -292,9 +292,23 @@ function read_json_file(path) {
|
|||||||
return data == null ? null : json_decode_text(data);
|
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) {
|
function validate_subscription(path) {
|
||||||
let value = read_json_file(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) {
|
function write_json_file(path, value) {
|
||||||
@@ -1474,17 +1488,6 @@ function parse_clash_record(record) {
|
|||||||
outbound.password = as_string(record.password);
|
outbound.password = as_string(record.password);
|
||||||
return outbound;
|
return outbound;
|
||||||
}
|
}
|
||||||
if (proxy_type == "http" || proxy_type == "https") {
|
|
||||||
let outbound = { type: "http", tag: name, server: server, server_port: port };
|
|
||||||
if (as_string(record.username) != "")
|
|
||||||
outbound.username = as_string(record.username);
|
|
||||||
if (as_string(record.password) != "")
|
|
||||||
outbound.password = as_string(record.password);
|
|
||||||
if (proxy_type == "https" || is_true(record.tls))
|
|
||||||
outbound.tls = { enabled: true };
|
|
||||||
return outbound;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -34,8 +34,12 @@ grep -Fq '{ id: "interface_sections", run: migrate_interface_sections }' "$MIGRA
|
|||||||
fail "interface section migration must have a stable named marker"
|
fail "interface section migration must have a stable named marker"
|
||||||
grep -Fq '{ id: "enable_component_checks", run: migrate_enable_component_checks }' "$MIGRATION" ||
|
grep -Fq '{ id: "enable_component_checks", run: migrate_enable_component_checks }' "$MIGRATION" ||
|
||||||
fail "component check migration must have a stable named marker"
|
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" ||
|
grep -Fq 'release_at_most(ctx, "1.0.1")' "$MIGRATION" ||
|
||||||
fail "release-specific migrations must use the source release only as a condition"
|
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)"
|
eval "$(ucode -L "$TACHYON_LIB" "$TACHYON_LIB/core/constants.uc" shell-env)"
|
||||||
export ZAPRET_LEGACY_DEFAULT_NFQWS_OPT ZAPRET_DEFAULT_NFQWS_OPT
|
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.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(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(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) {
|
function assert(condition, message) {
|
||||||
if (!condition) {
|
if (!condition) {
|
||||||
@@ -388,9 +392,9 @@ function assert(condition, message) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
assert(out.changed === true, '1.0.1 config should require migration');
|
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(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(!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');
|
assert(JSON.stringify(interfaces.map(item => item.name)) === JSON.stringify(['awg0', 'tun0']), 'interfaces should keep their order');
|
||||||
for (const item of interfaces) {
|
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(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
|
NODE
|
||||||
|
|
||||||
cat >"$WORK_DIR/runtime-migrate.state" <<'EOF_UCI'
|
cat >"$WORK_DIR/runtime-migrate.state" <<'EOF_UCI'
|
||||||
@@ -498,6 +600,7 @@ tachyon.main=section
|
|||||||
tachyon.main.enabled=1
|
tachyon.main.enabled=1
|
||||||
tachyon.main.action=connection
|
tachyon.main.action=connection
|
||||||
tachyon.main.interfaces=awg0
|
tachyon.main.interfaces=awg0
|
||||||
|
tachyon.main.selector_proxy_links=http://proxy.example:8080
|
||||||
EOF_UCI
|
EOF_UCI
|
||||||
: >"$WORK_DIR/runtime-version.log"
|
: >"$WORK_DIR/runtime-version.log"
|
||||||
TACHYON_UCI_STATE_FILE="$WORK_DIR/runtime-version.state" \
|
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" \
|
TACHYON_INTERNAL_CONFIG_TRIGGER_GUARD="$WORK_DIR/internal-config-change" \
|
||||||
ucode -L "$TACHYON_LIB" "$MIGRATION" migrate
|
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"
|
fail "runtime version migration must advance config_version"
|
||||||
grep -Fxq 'tachyon.settings.component_update_check_enabled=1' "$WORK_DIR/runtime-version.state" ||
|
grep -Fxq 'tachyon.settings.component_update_check_enabled=1' "$WORK_DIR/runtime-version.state" ||
|
||||||
fail "runtime version migration must enable component update checks"
|
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"
|
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" ||
|
grep -Eq '^tachyon\.cfg[0-9a-f]+=section_interface$' "$WORK_DIR/runtime-version.state" ||
|
||||||
fail "runtime version migration must create interface child settings"
|
fail "runtime version migration must create interface child settings"
|
||||||
if grep -Fq 'tachyon.main.interfaces=' "$WORK_DIR/runtime-version.state"; then
|
if grep -Fq 'tachyon.main.interfaces=' "$WORK_DIR/runtime-version.state"; then
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -117,7 +117,10 @@ mkdir -p "$fake_bin"
|
|||||||
cat >"$fake_bin/curl" <<'SH'
|
cat >"$fake_bin/curl" <<'SH'
|
||||||
#!/usr/bin/env sh
|
#!/usr/bin/env sh
|
||||||
printf '%s\n' "$*" >>"$FAKE_CURL_LOG"
|
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
|
SH
|
||||||
chmod +x "$fake_bin/curl"
|
chmod +x "$fake_bin/curl"
|
||||||
uci_state="$WORK_DIR/uci-state.txt"
|
uci_state="$WORK_DIR/uci-state.txt"
|
||||||
@@ -143,16 +146,31 @@ TACHYON_UCI_STATE_FILE="$uci_state" \
|
|||||||
TACHYON_LIB="$TACHYON_LIB" \
|
TACHYON_LIB="$TACHYON_LIB" \
|
||||||
TACHYON_UI_LATENCY_ACTION_DIR="$latency_action_dir" \
|
TACHYON_UI_LATENCY_ACTION_DIR="$latency_action_dir" \
|
||||||
PATH="$fake_bin:$PATH" \
|
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"
|
fail "clash-api get_proxy_latencies should update latency progress"
|
||||||
JOB_STATE="$latency_state" node - <<'NODE'
|
JOB_STATE="$latency_state" node - <<'NODE'
|
||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
const value = JSON.parse(fs.readFileSync(process.env.JOB_STATE, "utf8"));
|
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");
|
console.error("latency progress after proxy list mismatch");
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
NODE
|
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_rules="$(cat <<'EOF'
|
||||||
firewall.@rule[0]=rule
|
firewall.@rule[0]=rule
|
||||||
|
|||||||
@@ -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 ==="
|
||||||
@@ -90,6 +90,8 @@ fi
|
|||||||
|
|
||||||
pending_file="$WORK_DIR/reload.pending"
|
pending_file="$WORK_DIR/reload.pending"
|
||||||
if TACHYON_PENDING_RELOAD_FILE="$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 on_config_change 123 0 1 start >/dev/null 2>&1; then
|
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"
|
fail "queued config-change reload should not run immediately while service is starting"
|
||||||
fi
|
fi
|
||||||
@@ -100,6 +102,8 @@ grep -Fq 'reason=on_config_change' "$pending_file" ||
|
|||||||
|
|
||||||
rm -f "$pending_file"
|
rm -f "$pending_file"
|
||||||
if TACHYON_PENDING_RELOAD_FILE="$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
|
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"
|
fail "pending reload should not run while another service action is active"
|
||||||
fi
|
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"
|
fail "failed service start must schedule a retry even if WAN is already up"
|
||||||
grep -Fq 'start_retry_pending(START_RETRY_FILE)' "$INITD_UC" ||
|
grep -Fq 'start_retry_pending(START_RETRY_FILE)' "$INITD_UC" ||
|
||||||
fail "WAN retry must be gated by a failed-start marker"
|
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)"
|
service_enabled_line="$(awk '/function service_is_enabled\(\)/ { print NR; exit }' "$INITD_UC")"
|
||||||
retry_start_line="$(grep -nF 'function retry_start_on_wan_up(' "$INITD_UC" | head -n1 | cut -d: -f1)"
|
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" ] ||
|
[ -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"
|
fail "service_is_enabled must be declared before retry_start_on_wan_up for ucode runtime calls"
|
||||||
grep -Fq 'trigger-plan' "$INITD" ||
|
grep -Fq 'trigger-plan' "$INITD" ||
|
||||||
|
|||||||
@@ -374,6 +374,7 @@ cat >"$WORK_DIR/legacy-bin" <<'SH'
|
|||||||
printf '%s\n' "$*" >> "$TACHYON_INSTALLER_BIN_LOG"
|
printf '%s\n' "$*" >> "$TACHYON_INSTALLER_BIN_LOG"
|
||||||
case "$1" in
|
case "$1" in
|
||||||
get_status) printf '%s\n' '{"running":1}' ;;
|
get_status) printf '%s\n' '{"running":1}' ;;
|
||||||
|
restore_dnsmasq) exit "${TACHYON_INSTALLER_FAIL_BACKEND_RESTORE:-0}" ;;
|
||||||
esac
|
esac
|
||||||
exit 0
|
exit 0
|
||||||
SH
|
SH
|
||||||
@@ -405,6 +406,7 @@ TACHYON_INSTALLER_FAKE_LEGACY_BACKEND="$LEGACY_BACKEND" \
|
|||||||
TACHYON_INSTALLER_OPKG_LOG="$WORK_DIR/opkg.log" \
|
TACHYON_INSTALLER_OPKG_LOG="$WORK_DIR/opkg.log" \
|
||||||
TACHYON_INSTALLER_INIT_LOG="$WORK_DIR/init.log" \
|
TACHYON_INSTALLER_INIT_LOG="$WORK_DIR/init.log" \
|
||||||
TACHYON_INSTALLER_BIN_LOG="$WORK_DIR/bin.log" \
|
TACHYON_INSTALLER_BIN_LOG="$WORK_DIR/bin.log" \
|
||||||
|
TACHYON_INSTALLER_FAIL_BACKEND_RESTORE=1 \
|
||||||
TACHYON_UCI_STATE_FILE="$WORK_DIR/empty-uci.state" \
|
TACHYON_UCI_STATE_FILE="$WORK_DIR/empty-uci.state" \
|
||||||
ucode "$helper" installer-cleanup-legacy >"$state"
|
ucode "$helper" installer-cleanup-legacy >"$state"
|
||||||
|
|
||||||
|
|||||||
@@ -71,12 +71,12 @@ grep -Fq '"dashboard_include_groups"' <<<"$dashboard_options" ||
|
|||||||
fail "dashboard include filters must support URLTest and Priority groups"
|
fail "dashboard include filters must support URLTest and Priority groups"
|
||||||
grep -Fq '"dashboard_exclude_groups"' <<<"$dashboard_options" ||
|
grep -Fq '"dashboard_exclude_groups"' <<<"$dashboard_options" ||
|
||||||
fail "dashboard exclude filters must support URLTest and Priority groups"
|
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_group_line="$(awk '/"dashboard_include_groups"/ { print NR; exit }' <<<"$dashboard_options")"
|
||||||
include_proxy_line="$(grep -n 'includeProxyParameterOptions' <<<"$dashboard_options" | tail -n1 | cut -d: -f1)"
|
include_proxy_line="$(awk '/includeProxyParameterOptions/ { last = NR } END { print last }' <<<"$dashboard_options")"
|
||||||
[[ "$include_group_line" -lt "$include_proxy_line" ]] ||
|
[[ "$include_group_line" -lt "$include_proxy_line" ]] ||
|
||||||
fail "dashboard include group selector must precede the proxy-parameter toggle"
|
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_group_line="$(awk '/"dashboard_exclude_groups"/ { print NR; exit }' <<<"$dashboard_options")"
|
||||||
exclude_proxy_line="$(grep -n 'excludeProxyParameterOptions' <<<"$dashboard_options" | tail -n1 | cut -d: -f1)"
|
exclude_proxy_line="$(awk '/excludeProxyParameterOptions/ { last = NR } END { print last }' <<<"$dashboard_options")"
|
||||||
[[ "$exclude_group_line" -lt "$exclude_proxy_line" ]] ||
|
[[ "$exclude_group_line" -lt "$exclude_proxy_line" ]] ||
|
||||||
fail "dashboard exclude group selector must precede the proxy-parameter toggle"
|
fail "dashboard exclude group selector must precede the proxy-parameter toggle"
|
||||||
grep -Fq 'const liveValues = currentLiveDynamicListValues(section_id, typeName);' "$SECTION_JS" ||
|
grep -Fq 'const liveValues = currentLiveDynamicListValues(section_id, typeName);' "$SECTION_JS" ||
|
||||||
|
|||||||
@@ -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"
|
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"
|
SOURCE_PO="$ROOT_DIR/fe-app-tachyon/locales/tachyon.ru.po"
|
||||||
PACKAGE_PO="$ROOT_DIR/luci-app-tachyon/po/ru/tachyon.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() {
|
fail() {
|
||||||
printf 'FAIL: %s\n' "$1" >&2
|
printf 'FAIL: %s\n' "$1" >&2
|
||||||
@@ -17,6 +20,13 @@ fi
|
|||||||
grep -Fq '_("Close")' "$SECTION_JS" ||
|
grep -Fq '_("Close")' "$SECTION_JS" ||
|
||||||
fail "Tachyon section settings modal must expose a Close action"
|
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
|
for po in "$SOURCE_PO" "$PACKAGE_PO"; do
|
||||||
awk '
|
awk '
|
||||||
$0 == "msgid \"Close\"" {
|
$0 == "msgid \"Close\"" {
|
||||||
|
|||||||
+18
-4
@@ -44,10 +44,16 @@ assert_mark_range_no_overlap() {
|
|||||||
|
|
||||||
eval "$(ucode -L "$TACHYON_LIB" "$TACHYON_LIB/core/constants.uc" shell-env)"
|
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_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 "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_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 "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'
|
cat >"$WORK_DIR/fixture.json" <<'JSON'
|
||||||
{
|
{
|
||||||
@@ -57,6 +63,8 @@ JSON
|
|||||||
|
|
||||||
context_json() {
|
context_json() {
|
||||||
local zapret2_base="${1:-$ZAPRET2_ROUTE_MARK_BASE}"
|
local zapret2_base="${1:-$ZAPRET2_ROUTE_MARK_BASE}"
|
||||||
|
local fakeip_mark="${2:-$NFT_FAKEIP_MARK}"
|
||||||
|
local outbound_mark="${3:-$NFT_OUTBOUND_MARK}"
|
||||||
|
|
||||||
cat <<JSON
|
cat <<JSON
|
||||||
{
|
{
|
||||||
@@ -72,16 +80,22 @@ context_json() {
|
|||||||
"zapret_queue_range_size": "$ZAPRET_QUEUE_RANGE_SIZE",
|
"zapret_queue_range_size": "$ZAPRET_QUEUE_RANGE_SIZE",
|
||||||
"zapret2_route_mark_base": "$zapret2_base",
|
"zapret2_route_mark_base": "$zapret2_base",
|
||||||
"zapret2_queue_range_size": "$ZAPRET2_QUEUE_RANGE_SIZE",
|
"zapret2_queue_range_size": "$ZAPRET2_QUEUE_RANGE_SIZE",
|
||||||
"nft_fakeip_mark": "$NFT_FAKEIP_MARK",
|
"nft_fakeip_mark": "$fakeip_mark",
|
||||||
"nft_outbound_mark": "$NFT_OUTBOUND_MARK"
|
"nft_outbound_mark": "$outbound_mark"
|
||||||
}
|
}
|
||||||
JSON
|
JSON
|
||||||
}
|
}
|
||||||
|
|
||||||
TACHYON_LIB="$TACHYON_LIB" ucode -L "$TACHYON_LIB" "$VALIDATOR" validate-runtime-fixture "$WORK_DIR/fixture.json" "$(context_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
|
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 FakeIP mark"
|
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
|
fi
|
||||||
|
|
||||||
printf 'mark range checks passed\n'
|
printf 'mark range checks passed\n'
|
||||||
|
|||||||
+4
-3
@@ -52,8 +52,8 @@ assert_line_before() {
|
|||||||
local first_line
|
local first_line
|
||||||
local second_line
|
local second_line
|
||||||
|
|
||||||
first_line="$(grep -Fn "$first" "$file" | head -n 1 | cut -d: -f1 || true)"
|
first_line="$(awk -v pat="$first" 'index($0, pat) { print NR; exit }' "$file")"
|
||||||
second_line="$(grep -Fn "$second" "$file" | head -n 1 | cut -d: -f1 || true)"
|
second_line="$(awk -v pat="$second" 'index($0, pat) { print NR; exit }' "$file")"
|
||||||
[ -n "$first_line" ] || fail "$label: missing first line '$first'"
|
[ -n "$first_line" ] || fail "$label: missing first line '$first'"
|
||||||
[ -n "$second_line" ] || fail "$label: missing second line '$second'"
|
[ -n "$second_line" ] || fail "$label: missing second line '$second'"
|
||||||
[ "$first_line" -lt "$second_line" ] || fail "$label: expected first line before 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\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{ 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\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_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\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\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\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"
|
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"
|
||||||
|
|||||||
@@ -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"
|
fail "build.sh must use the exact three-part release version for APK metadata"
|
||||||
grep -Fq "option component_update_check_enabled '1'" "$TACHYON_CONFIG" ||
|
grep -Fq "option component_update_check_enabled '1'" "$TACHYON_CONFIG" ||
|
||||||
fail "new installations must enable component update checks by default"
|
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"
|
fail "new installations must start at the current configuration schema version"
|
||||||
grep -Fq "list applied_migrations 'interface_sections'" "$TACHYON_CONFIG" ||
|
grep -Fq "list applied_migrations 'interface_sections'" "$TACHYON_CONFIG" ||
|
||||||
fail "new installations must mark the interface section migration as applied"
|
fail "new installations must mark the interface section migration as applied"
|
||||||
grep -Fq "list applied_migrations 'enable_component_checks'" "$TACHYON_CONFIG" ||
|
grep -Fq "list applied_migrations 'enable_component_checks'" "$TACHYON_CONFIG" ||
|
||||||
fail "new installations must mark the component check migration as applied"
|
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" ||
|
grep -Fq '/usr/lib/tachyon/config/migration.uc migrate' "$TACHYON_MAKEFILE" ||
|
||||||
fail "OpenWrt package postinst must run configuration migrations"
|
fail "OpenWrt package postinst must run configuration migrations"
|
||||||
[ "$(grep -Fc '/usr/lib/tachyon/config/migration.uc migrate' "$BUILD_SCRIPT")" -ge 3 ] ||
|
[ "$(grep -Fc '/usr/lib/tachyon/config/migration.uc migrate' "$BUILD_SCRIPT")" -ge 3 ] ||
|
||||||
|
|||||||
@@ -131,8 +131,12 @@ TACHYON_SING_BOX_BIN="$WORK_DIR/missing-sing-box-bin" \
|
|||||||
TACHYON_SING_BOX_CRONET="$WORK_DIR/missing-cronet" \
|
TACHYON_SING_BOX_CRONET="$WORK_DIR/missing-cronet" \
|
||||||
TACHYON_RT_TABLES="$WORK_DIR/rt_tables_dont_touch" \
|
TACHYON_RT_TABLES="$WORK_DIR/rt_tables_dont_touch" \
|
||||||
ucode -L "$TACHYON_LIB" "$PACKAGE_UC" prerm
|
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"
|
fail "package prerm must skip dnsmasq restore when dont_touch_dhcp is enabled"
|
||||||
|
fi
|
||||||
|
|
||||||
cat >"$WORK_DIR/restore.state" <<'EOF_UCI'
|
cat >"$WORK_DIR/restore.state" <<'EOF_UCI'
|
||||||
tachyon.settings=settings
|
tachyon.settings=settings
|
||||||
|
|||||||
+1
-1
@@ -7,7 +7,7 @@ find tachyon/files/usr/lib -name "*.uc" -print0 | xargs -0 -n1 ucode -S -c -o /d
|
|||||||
|
|
||||||
echo "=== Running Tests ==="
|
echo "=== Running Tests ==="
|
||||||
for f in tests/*.sh; do
|
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"
|
echo "Running: $f"
|
||||||
bash "$f"
|
bash "$f"
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -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"
|
fail "service/lifecycle.uc must verify stable runtime after sing-box start/reload"
|
||||||
grep -Fq 'SING_BOX_START_STABLE_MIN_AGE' "$LIFECYCLE_UC" ||
|
grep -Fq 'SING_BOX_START_STABLE_MIN_AGE' "$LIFECYCLE_UC" ||
|
||||||
fail "service/lifecycle.uc must use a dedicated sing-box start stability window"
|
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")"
|
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")"
|
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" ] ||
|
[ -n "$sing_box_start_line" ] ||
|
||||||
fail "service/lifecycle.uc must start sing-box through init.d"
|
fail "service/lifecycle.uc must start sing-box through init.d"
|
||||||
[ -n "$early_stable_line" ] ||
|
[ -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"
|
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" ] ||
|
[ -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"
|
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")"
|
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" ] ||
|
[ -n "$sing_box_reload_line" ] ||
|
||||||
fail "service/lifecycle.uc must reload sing-box through service/state.uc"
|
fail "service/lifecycle.uc must reload sing-box through service/state.uc"
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ if sed -n '/^function process_age_seconds(pid) {$/,/^}$/p' "$STATE_UC" | grep -F
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
mkdir -p "$WORK_DIR/stable-start-bin"
|
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'
|
cat >"$WORK_DIR/stable-start-bin/ubus" <<'SH'
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -eo pipefail
|
set -eo pipefail
|
||||||
@@ -169,7 +169,7 @@ exec "${REAL_UCODE:?}" "$@"
|
|||||||
SH
|
SH
|
||||||
chmod 0755 "$WORK_DIR/stable-start-bin/ubus" "$WORK_DIR/stable-start-bin/nft" "$WORK_DIR/stable-start-bin/ucode"
|
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=$!
|
sing_box_pid=$!
|
||||||
printf '%s\n' "$sing_box_pid" >"$WORK_DIR/sing-box.pid"
|
printf '%s\n' "$sing_box_pid" >"$WORK_DIR/sing-box.pid"
|
||||||
if ! PATH="$WORK_DIR/stable-start-bin:$PATH" \
|
if ! PATH="$WORK_DIR/stable-start-bin:$PATH" \
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ lifecycle_ucode() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
json_flatten() {
|
json_flatten() {
|
||||||
tr -d '[:space:]'
|
tr -d ' \t\r\n'
|
||||||
}
|
}
|
||||||
|
|
||||||
cat >"$WORK_DIR/before.json" <<'JSON'
|
cat >"$WORK_DIR/before.json" <<'JSON'
|
||||||
|
|||||||
@@ -76,8 +76,8 @@ require_pattern 'function abort_reload(status, runtime_changed)' \
|
|||||||
"reload failures must share one cleanup decision owner"
|
"reload failures must share one cleanup decision owner"
|
||||||
require_pattern 'return abort_reload(status, true);' \
|
require_pattern 'return abort_reload(status, true);' \
|
||||||
"reload failures after runtime mutation must clean the partial runtime"
|
"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)"
|
cleanup_function_line="$(awk '/function cleanup_failed_runtime\(\)/ { print NR; exit }' "$LIFECYCLE_UC")"
|
||||||
abort_function_line="$(grep -nF 'function abort_reload(status, runtime_changed)' "$LIFECYCLE_UC" | head -n1 | cut -d: -f1)"
|
abort_function_line="$(awk '/function abort_reload\(status, runtime_changed\)/ { print NR; exit }' "$LIFECYCLE_UC")"
|
||||||
[ -n "$cleanup_function_line" ] && [ -n "$abort_function_line" ] &&
|
[ -n "$cleanup_function_line" ] && [ -n "$abort_function_line" ] &&
|
||||||
[ "$cleanup_function_line" -lt "$abort_function_line" ] ||
|
[ "$cleanup_function_line" -lt "$abort_function_line" ] ||
|
||||||
fail "cleanup_failed_runtime must be declared before abort_reload for ucode runtime calls"
|
fail "cleanup_failed_runtime must be declared before abort_reload for ucode runtime calls"
|
||||||
|
|||||||
@@ -45,7 +45,8 @@ expected_shell_scripts="$(
|
|||||||
printf '%s\n' \
|
printf '%s\n' \
|
||||||
'luci-app-tachyon/root/etc/uci-defaults/50_luci-tachyon' \
|
'luci-app-tachyon/root/etc/uci-defaults/50_luci-tachyon' \
|
||||||
'tachyon/files/etc/hotplug.d/iface/99-tachyon-wan-monitor' \
|
'tachyon/files/etc/hotplug.d/iface/99-tachyon-wan-monitor' \
|
||||||
'tachyon/files/etc/init.d/tachyon' |
|
'tachyon/files/etc/init.d/tachyon' \
|
||||||
|
'tachyon/files/usr/lib/tachyon/firewall.include' |
|
||||||
LC_ALL=C sort
|
LC_ALL=C sort
|
||||||
)"
|
)"
|
||||||
|
|
||||||
|
|||||||
@@ -421,9 +421,9 @@ cat >"$WORK_DIR/urltest-filter-fixture.json" <<'JSON'
|
|||||||
".type": "section",
|
".type": "section",
|
||||||
"enabled": "1",
|
"enabled": "1",
|
||||||
"action": "proxy",
|
"action": "proxy",
|
||||||
"selector_proxy_links": [
|
"outbound_jsons": [
|
||||||
"http://127.0.0.1:18081#Keep",
|
"{\"type\":\"http\",\"tag\":\"Keep\",\"server\":\"127.0.0.1\",\"server_port\":18081}",
|
||||||
"http://127.0.0.1:18082#Drop"
|
"{\"type\":\"http\",\"tag\":\"Drop\",\"server\":\"127.0.0.1\",\"server_port\":18082}"
|
||||||
],
|
],
|
||||||
"urltest_enabled": "1",
|
"urltest_enabled": "1",
|
||||||
"urltest_filter_mode": "include",
|
"urltest_filter_mode": "include",
|
||||||
@@ -434,6 +434,25 @@ cat >"$WORK_DIR/urltest-filter-fixture.json" <<'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'
|
cat >"$WORK_DIR/provider-actions-fixture.json" <<'JSON'
|
||||||
{
|
{
|
||||||
"settings": {
|
"settings": {
|
||||||
@@ -908,6 +927,12 @@ cat >"$WORK_DIR/subscriptions/grouped-subscription-1.json" <<'JSON'
|
|||||||
{
|
{
|
||||||
"type": "direct",
|
"type": "direct",
|
||||||
"tag": "provider-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/runtime-matchers-fixture.json" "$WORK_DIR/matchers.json"
|
||||||
generate_config "$WORK_DIR/dns-action-fixture.json" "$WORK_DIR/dns-action.json"
|
generate_config "$WORK_DIR/dns-action-fixture.json" "$WORK_DIR/dns-action.json"
|
||||||
generate_config "$WORK_DIR/urltest-filter-fixture.json" "$WORK_DIR/urltest.json"
|
generate_config "$WORK_DIR/urltest-filter-fixture.json" "$WORK_DIR/urltest.json"
|
||||||
|
if generate_config "$WORK_DIR/manual-http-fixture.json" "$WORK_DIR/manual-http.json" \
|
||||||
|
>"$WORK_DIR/manual-http.stdout" 2>"$WORK_DIR/manual-http.stderr"; then
|
||||||
|
fail "generator should reject native HTTP connection URLs"
|
||||||
|
fi
|
||||||
|
grep -Fxq 'manual proxy link scheme is not supported by sing-box config generation yet' "$WORK_DIR/manual-http.stderr" ||
|
||||||
|
fail "native HTTP connection URL failure should identify the unsupported scheme"
|
||||||
generate_config "$WORK_DIR/provider-actions-fixture.json" "$WORK_DIR/providers.json"
|
generate_config "$WORK_DIR/provider-actions-fixture.json" "$WORK_DIR/providers.json"
|
||||||
generate_config "$WORK_DIR/manual-transport-fixture.json" "$WORK_DIR/manual.json"
|
generate_config "$WORK_DIR/manual-transport-fixture.json" "$WORK_DIR/manual.json"
|
||||||
generate_config "$WORK_DIR/vpn-interface-fixture.json" "$WORK_DIR/vpn.json"
|
generate_config "$WORK_DIR/vpn-interface-fixture.json" "$WORK_DIR/vpn.json"
|
||||||
@@ -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 = cfg("urltest");
|
||||||
let urltest_out = outbound(urltest, "proxy-urltest-out");
|
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");
|
assert(outbound(urltest, "proxy-out").default == "proxy-urltest-out", "selector defaults to URLTest");
|
||||||
|
|
||||||
let providers = cfg("providers");
|
let providers = cfg("providers");
|
||||||
assert(outbound(providers, "zap-out").routing_mark == 0x01000001, "Zapret mark");
|
assert(outbound(providers, "zap-out").routing_mark == 0x01000001, "Zapret mark");
|
||||||
assert(outbound(providers, "zap2-out").routing_mark == 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");
|
assert(outbound(providers, "bye-out").type == "socks" && outbound(providers, "bye-out").server_port == 1080, "ByeDPI outbound");
|
||||||
|
|
||||||
let manual = cfg("manual");
|
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(grouped_leaf && grouped_leaf.detour == null, "hidden subscription leaf does not receive connection URL detour");
|
||||||
assert(second_leaf && second_leaf.detour == null, "second hidden subscription leaf does not receive connection URL detour");
|
assert(second_leaf && second_leaf.detour == null, "second hidden subscription leaf does not receive connection URL detour");
|
||||||
assert(outbound(subscription_group, "provider-direct") == null, "provider direct outbound skipped");
|
assert(outbound(subscription_group, "provider-direct") == null, "provider direct outbound skipped");
|
||||||
|
assert(outbound(subscription_group, "provider-http") == null, "provider HTTP outbound skipped");
|
||||||
let grouped_selector = outbound(subscription_group, "grouped-out");
|
let grouped_selector = outbound(subscription_group, "grouped-out");
|
||||||
assert(grouped_selector && length(grouped_selector.outbounds) == 1 && grouped_selector.outbounds[0] == "Provider Group", "selector exposes provider group only");
|
assert(grouped_selector && length(grouped_selector.outbounds) == 1 && grouped_selector.outbounds[0] == "Provider Group", "selector exposes provider group only");
|
||||||
let grouped_state = cfg("subscription-group.json.section-cache/grouped");
|
let grouped_state = cfg("subscription-group.json.section-cache/grouped");
|
||||||
|
|||||||
@@ -67,6 +67,18 @@ assert_alpn() {
|
|||||||
UUID='00000000-0000-4000-8000-000000000001'
|
UUID='00000000-0000-4000-8000-000000000001'
|
||||||
BASE_QUERY='encryption=none&security=tls&fp=chrome&alpn=h2%2Chttp%2F1.1&sni=example.com'
|
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")"
|
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" '"transport": { "type": "ws"' "vless-ws"
|
||||||
assert_contains "$ws_output" '"alpn": [ "http/1.1" ]' "vless-ws"
|
assert_contains "$ws_output" '"alpn": [ "http/1.1" ]' "vless-ws"
|
||||||
@@ -117,10 +129,16 @@ proxies:
|
|||||||
alpn: [h2, http/1.1]
|
alpn: [h2, http/1.1]
|
||||||
ws-opts:
|
ws-opts:
|
||||||
path: /ws
|
path: /ws
|
||||||
|
- name: clash-http
|
||||||
|
type: http
|
||||||
|
server: proxy.example
|
||||||
|
port: 8080
|
||||||
YAML
|
YAML
|
||||||
ucode "$PARSER" normalize-clash-yaml "$clash_input" "$clash_output"
|
ucode "$PARSER" normalize-clash-yaml "$clash_input" "$clash_output"
|
||||||
assert_contains "$clash_output" '"alpn": [ "http/1.1" ]' "clash-vless-ws"
|
assert_contains "$clash_output" '"alpn": [ "http/1.1" ]' "clash-vless-ws"
|
||||||
assert_contains "$clash_output" '"encryption": "mlkem768x25519plus.native.test"' "clash-vless-ws"
|
assert_contains "$clash_output" '"encryption": "mlkem768x25519plus.native.test"' "clash-vless-ws"
|
||||||
|
assert_not_contains "$clash_output" '"tag": "clash-http"' "clash-http"
|
||||||
|
assert_contains "$clash_output" '"skipped": 1' "clash-http"
|
||||||
|
|
||||||
xray_input="$WORK_DIR/xray.json"
|
xray_input="$WORK_DIR/xray.json"
|
||||||
xray_output="$WORK_DIR/xray-normalized.json"
|
xray_output="$WORK_DIR/xray-normalized.json"
|
||||||
|
|||||||
@@ -215,7 +215,8 @@ ui_ucode cleanup-action-dir-fixture "$cleanup_dir"
|
|||||||
|
|
||||||
old_state="$cleanup_dir/old.json"
|
old_state="$cleanup_dir/old.json"
|
||||||
printf '%s\n' '{"running":false}' >"$old_state"
|
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"
|
TACHYON_UI_ACTION_FINISHED_TTL_MINUTES=60 ui_ucode cleanup-action-dir-fixture "$cleanup_dir"
|
||||||
[ ! -e "$old_state" ] ||
|
[ ! -e "$old_state" ] ||
|
||||||
fail "old finished action state should be cleaned without find"
|
fail "old finished action state should be cleaned without find"
|
||||||
|
|||||||
@@ -86,6 +86,12 @@ assert_eq "0 0 * * *" \
|
|||||||
assert_eq "*/30 * * * * /usr/bin/tachyon list_update_if_due # list" \
|
assert_eq "*/30 * * * * /usr/bin/tachyon list_update_if_due # list" \
|
||||||
"$(updates_ucode list-update-cron-job 30m /usr/bin/tachyon '# list')" \
|
"$(updates_ucode list-update-cron-job 30m /usr/bin/tachyon '# list')" \
|
||||||
"list update cron job"
|
"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" \
|
assert_eq "0 */2 * * * /usr/bin/tachyon subscription_update_if_due # subscription" \
|
||||||
"$(updates_ucode subscription-update-cron-job 7200 /usr/bin/tachyon '# subscription')" \
|
"$(updates_ucode subscription-update-cron-job 7200 /usr/bin/tachyon '# subscription')" \
|
||||||
"subscription update cron job"
|
"subscription update cron job"
|
||||||
|
|||||||
Reference in New Issue
Block a user