feat: per-section MAC/DNS exclusions, boot latency sweep, WDTT/OlcRTC providers, DNS hijack socket collision fix

Features:
- Per-Section MAC Exclusions: nft, generator_routes, section.js, i18n
- Per-Section Routed DNS: connections, generator_routes, nft, validator,
  types.ts, section.js, state.uc, reload.uc
- Boot Latency Sweep: ui.uc latency boot sweep with fixture dispatchers
- WDTT provider: full backend (common, validator, runtime), sing-box
  outbound, diagnostics, component install, updater, i18n, section.js UI
- OlcRTC provider: full backend (common, validator, runtime), sing-box
  outbound, diagnostics, component install, updater, i18n, section.js UI
- WireGuard-over-TURN tunnel (WDTT) and TCP-over-WebRTC tunnel (OlcRTC)
  as sidecar processes with local SOCKS5, routed through sing-box outbound

Bug fixes:
- route.uc: restrict hijack-dns to local DNS inbounds only (dns-in,
  source-dns-in, dns-health-*) — fixes socket collision between sing-box
  and dnsmasq on port 53 via SO_REUSEADDR transparent write-back sockets
- runtime.uc: add Port 53 socket collision diagnostic with auto-fix
  (sing-box restart to clear parasitic UNCONN sockets with Recv-Q)
- runtime.uc: add dnsmasq dns_redirect check with auto-fix — disables
  firewall4 DNAT that causes TProxy to rewrite destination to router :53
- failover.uc: add wdtt/olcrtc to failover candidate actions
- telegram.uc: show WDTT/OlcRTC installed status in /info

Tests: 135/155 shell + 591/591 frontend
This commit is contained in:
Dushnilin
2026-09-11 18:04:03 +03:00
parent 3e716b1d1b
commit 26431f50d3
48 changed files with 3392 additions and 39 deletions
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -13214,8 +13214,8 @@
] ]
}, },
{ {
"call": "Traffic from these IP addresses will never be routed through this section, even if other conditions match.", "call": "Traffic from these IP addresses, CIDRs, or MAC addresses will never be routed through this section, even if other conditions match.",
"key": "Traffic from these IP addresses will never be routed through this section, even if other conditions match.", "key": "Traffic from these IP addresses, CIDRs, or MAC addresses will never be routed through this section, even if other conditions match.",
"places": [ "places": [
"../luci-app-tachyon/htdocs/luci-static/resources/view/tachyon/section.js:10821" "../luci-app-tachyon/htdocs/luci-static/resources/view/tachyon/section.js:10821"
] ]
+76 -1
View File
@@ -1329,6 +1329,81 @@ msgstr ""
msgid "ByeDPI strategy cannot be empty" msgid "ByeDPI strategy cannot be empty"
msgstr "" msgstr ""
msgid "WDTT"
msgstr ""
msgid "WDTT URI"
msgstr ""
msgid "WDTT connection URI (wdtt://...)"
msgstr ""
msgid "WDTT Mode"
msgstr ""
msgid "WDTT Device ID"
msgstr ""
msgid "WDTT Workers"
msgstr ""
msgid "WDTT Max Hashes"
msgstr ""
msgid "WDTT MTU"
msgstr ""
msgid "WDTT Refresh"
msgstr ""
msgid "WDTT Auto Update"
msgstr ""
msgid "WDTT Block DoH"
msgstr ""
msgid "WDTT Block IPv6"
msgstr ""
msgid "WDTT QWDTT Mode"
msgstr ""
msgid "WDTT Subscription Links"
msgstr ""
msgid "WDTT Community Lists"
msgstr ""
msgid "WDTT Remote Domain List"
msgstr ""
msgid "OlcRTC"
msgstr ""
msgid "OlcRTC Provider"
msgstr ""
msgid "OlcRTC Transport"
msgstr ""
msgid "OlcRTC Room ID"
msgstr ""
msgid "OlcRTC Crypto Key"
msgstr ""
msgid "OlcRTC SOCKS Host"
msgstr ""
msgid "OlcRTC SOCKS Port"
msgstr ""
msgid "OlcRTC DNS Server"
msgstr ""
msgid "OlcRTC Subscription Links"
msgstr ""
#: ../luci-app-tachyon/htdocs/luci-static/resources/view/tachyon/main.js:14975 #: ../luci-app-tachyon/htdocs/luci-static/resources/view/tachyon/main.js:14975
#: src/tachyon/tabs/diagnostic/partials/renderLeakCheckModal.ts:170 #: src/tachyon/tabs/diagnostic/partials/renderLeakCheckModal.ts:170
msgid "Bypasses proxy (SO_MARK 0x08000000)" msgid "Bypasses proxy (SO_MARK 0x08000000)"
@@ -8155,7 +8230,7 @@ msgid "Traffic for this level goes directly."
msgstr "" msgstr ""
#: ../luci-app-tachyon/htdocs/luci-static/resources/view/tachyon/section.js:10821 #: ../luci-app-tachyon/htdocs/luci-static/resources/view/tachyon/section.js:10821
msgid "Traffic from these IP addresses will never be routed through this section, even if other conditions match." msgid "Traffic from these IP addresses, CIDRs, or MAC addresses will never be routed through this section, even if other conditions match."
msgstr "" msgstr ""
#: ../luci-app-tachyon/htdocs/luci-static/resources/view/tachyon/section.js:10849 #: ../luci-app-tachyon/htdocs/luci-static/resources/view/tachyon/section.js:10849
+95 -2
View File
@@ -839,6 +839,81 @@ msgstr "Стратегия ByeDPI"
msgid "ByeDPI strategy cannot be empty" msgid "ByeDPI strategy cannot be empty"
msgstr "Стратегия ByeDPI не может быть пустой" msgstr "Стратегия ByeDPI не может быть пустой"
msgid "WDTT"
msgstr "WDTT"
msgid "WDTT URI"
msgstr "URI WDTT"
msgid "WDTT connection URI (wdtt://...)"
msgstr "URI подключения WDTT (wdtt://...)"
msgid "WDTT Mode"
msgstr "Режим WDTT"
msgid "WDTT Device ID"
msgstr "ID устройства WDTT"
msgid "WDTT Workers"
msgstr "Рабочие процессы WDTT"
msgid "WDTT Max Hashes"
msgstr "Максимум хешей WDTT"
msgid "WDTT MTU"
msgstr "MTU WDTT"
msgid "WDTT Refresh"
msgstr "Обновление WDTT"
msgid "WDTT Auto Update"
msgstr "Автообновление WDTT"
msgid "WDTT Block DoH"
msgstr "Блокировка DoH в WDTT"
msgid "WDTT Block IPv6"
msgstr "Блокировка IPv6 в WDTT"
msgid "WDTT QWDTT Mode"
msgstr "Режим QWDTT"
msgid "WDTT Subscription Links"
msgstr "Ссылки подписок WDTT"
msgid "WDTT Community Lists"
msgstr "Списки сообществ WDTT"
msgid "WDTT Remote Domain List"
msgstr "Удалённый список доменов WDTT"
msgid "OlcRTC"
msgstr "OlcRTC"
msgid "OlcRTC Provider"
msgstr "Провайдер OlcRTC"
msgid "OlcRTC Transport"
msgstr "Транспорт OlcRTC"
msgid "OlcRTC Room ID"
msgstr "ID комнаты OlcRTC"
msgid "OlcRTC Crypto Key"
msgstr "Ключ шифрования OlcRTC"
msgid "OlcRTC SOCKS Host"
msgstr "Хост SOCKS OlcRTC"
msgid "OlcRTC SOCKS Port"
msgstr "Порт SOCKS OlcRTC"
msgid "OlcRTC DNS Server"
msgstr "DNS-сервер OlcRTC"
msgid "OlcRTC Subscription Links"
msgstr "Ссылки подписок OlcRTC"
msgid "Bypasses proxy (SO_MARK 0x08000000)" msgid "Bypasses proxy (SO_MARK 0x08000000)"
msgstr "В обход прокси (SO_MARK 0x08000000)" msgstr "В обход прокси (SO_MARK 0x08000000)"
@@ -1445,6 +1520,24 @@ msgstr "DNS через прокси"
msgid "DNS through section" msgid "DNS through section"
msgstr "DNS через секцию" msgstr "DNS через секцию"
msgid "Routed DNS"
msgstr "Маршрутизированный DNS"
msgid "Resolve matched domains through an encrypted DNS server of this section instead of FakeIP. DNS queries traverse the same proxy as the section's outbound."
msgstr "Резолвить совпавшие домены через зашифрованный DNS-сервер этой секции вместо FakeIP. DNS-запросы проходят через тот же прокси, что и исходящий трафик секции."
msgid "Routed DNS Protocol"
msgstr "Протокол маршрутизированного DNS"
msgid "Protocol for the routed DNS resolver."
msgstr "Протокол для DNS-резолвера маршрутизации."
msgid "Routed DNS Servers"
msgstr "Серверы маршрутизированного DNS"
msgid "DNS server addresses for domain resolution in this section (e.g. 1.1.1.1, https://cloudflare-dns.com/dns-query)."
msgstr "Адреса DNS-серверов для резолвинга доменов в этой секции (напр. 1.1.1.1, https://cloudflare-dns.com/dns-query)."
msgid "DNS Turbo Cache" msgid "DNS Turbo Cache"
msgstr "Быстрый DNS-кэш" msgstr "Быстрый DNS-кэш"
@@ -5096,8 +5189,8 @@ msgstr "Трафик"
msgid "Traffic for this level goes directly." msgid "Traffic for this level goes directly."
msgstr "Трафик этого уровня идёт напрямую." msgstr "Трафик этого уровня идёт напрямую."
msgid "Traffic from these IP addresses will never be routed through this section, even if other conditions match." msgid "Traffic from these IP addresses, CIDRs, or MAC addresses will never be routed through this section, even if other conditions match."
msgstr "Трафик от этих IP-адресов никогда не будет маршрутизирован через эту секцию, даже если другие условия совпадают." msgstr "Трафик от этих IP-адресов, CIDR или MAC-адресов никогда не будет маршрутизирован через эту секцию, даже если другие условия совпадают."
msgid "Traffic of specified protocols (e.g. BitTorrent, QUIC, STUN) will bypass this section." msgid "Traffic of specified protocols (e.g. BitTorrent, QUIC, STUN) will bypass this section."
msgstr "Трафик указанных протоколов (например BitTorrent, QUIC, STUN) будет обходить эту секцию." msgstr "Трафик указанных протоколов (например BitTorrent, QUIC, STUN) будет обходить эту секцию."
@@ -33,6 +33,16 @@ const componentActionKeyMap: Record<string, UpdatesActionKey> = {
'byedpi:install_version': 'byedpiInstall', 'byedpi:install_version': 'byedpiInstall',
'byedpi:remove': 'byedpiRemove', 'byedpi:remove': 'byedpiRemove',
'byedpi:rollback': 'byedpiRollback', 'byedpi:rollback': 'byedpiRollback',
'wdtt:check_update': 'wdttCheck',
'wdtt:install': 'wdttInstall',
'wdtt:install_version': 'wdttInstall',
'wdtt:remove': 'wdttRemove',
'wdtt:rollback': 'wdttRollback',
'olcrtc:check_update': 'olcrtcCheck',
'olcrtc:install': 'olcrtcInstall',
'olcrtc:install_version': 'olcrtcInstall',
'olcrtc:remove': 'olcrtcRemove',
'olcrtc:rollback': 'olcrtcRollback',
'tailscale:check_update': 'tailscaleCheck', 'tailscale:check_update': 'tailscaleCheck',
'tailscale:install': 'tailscaleInstall', 'tailscale:install': 'tailscaleInstall',
'tailscale:install_version': 'tailscaleInstall', 'tailscale:install_version': 'tailscaleInstall',
@@ -415,7 +415,10 @@ function isConnectionAction(action?: string) {
} }
function isServiceAction(action?: string) { function isServiceAction(action?: string) {
return Boolean(action && ['zapret', 'zapret2', 'byedpi'].includes(action)); return Boolean(
action &&
['zapret', 'zapret2', 'byedpi', 'wdtt', 'olcrtc'].includes(action),
);
} }
function hasSubscriptionSources(section: Tachyon.ConfigSection) { function hasSubscriptionSources(section: Tachyon.ConfigSection) {
@@ -1517,7 +1520,9 @@ export async function getDashboardSections(
Promise<Tachyon.ServiceStatus | undefined> Promise<Tachyon.ServiceStatus | undefined>
>(); >();
const getServiceStatus = (serviceType: 'zapret' | 'zapret2' | 'byedpi') => { const getServiceStatus = (
serviceType: 'zapret' | 'zapret2' | 'byedpi' | 'wdtt' | 'olcrtc',
) => {
if (!serviceStatusCache.has(serviceType)) { if (!serviceStatusCache.has(serviceType)) {
serviceStatusCache.set( serviceStatusCache.set(
serviceType, serviceType,
@@ -1571,6 +1576,38 @@ export async function getDashboardSections(
statusMessage: s.status_message, statusMessage: s.status_message,
}; };
} }
} else if (serviceType === 'wdtt') {
const result = await TachyonShellMethods.getWdttStatus();
if (result.success && result.data) {
const s = result.data;
return {
serviceType: 'wdtt',
configured: Boolean(s.configured),
ready: Boolean(s.ready),
conflict: Boolean(s.conflict),
runningProcesses: s.running_process_count,
expectedProcesses: s.expected_process_count,
restartCount: s.restart_count,
unstable: Boolean(s.runtime_unstable),
statusMessage: s.status_message,
};
}
} else if (serviceType === 'olcrtc') {
const result = await TachyonShellMethods.getOlcrtcStatus();
if (result.success && result.data) {
const s = result.data;
return {
serviceType: 'olcrtc',
configured: Boolean(s.configured),
ready: Boolean(s.ready),
conflict: Boolean(s.conflict),
runningProcesses: s.running_process_count,
expectedProcesses: s.expected_process_count,
restartCount: s.restart_count,
unstable: Boolean(s.runtime_unstable),
statusMessage: s.status_message,
};
}
} }
} catch (_error) { } catch (_error) {
// Status fetch failed — show section without status // Status fetch failed — show section without status
@@ -316,6 +316,20 @@ export const TachyonShellMethods = {
'/usr/bin/tachyon', '/usr/bin/tachyon',
{ allowNonZeroWithStdout: true }, { allowNonZeroWithStdout: true },
), ),
getWdttStatus: async () =>
callBaseMethod<Tachyon.GetWdttStatus>(
Tachyon.AvailableMethods.GET_WDTT_STATUS,
[],
'/usr/bin/tachyon',
{ allowNonZeroWithStdout: true },
),
getOlcrtcStatus: async () =>
callBaseMethod<Tachyon.GetOlcrtcStatus>(
Tachyon.AvailableMethods.GET_OLCRTC_STATUS,
[],
'/usr/bin/tachyon',
{ allowNonZeroWithStdout: true },
),
getClashApiProxies: async () => getClashApiProxies: async () =>
callBaseMethod<ClashAPI.Proxies>(Tachyon.AvailableMethods.CLASH_API, [ callBaseMethod<ClashAPI.Proxies>(Tachyon.AvailableMethods.CLASH_API, [
Tachyon.AvailableClashAPIMethods.GET_PROXIES, Tachyon.AvailableClashAPIMethods.GET_PROXIES,
@@ -28,6 +28,8 @@ function componentDisplayName(component: string) {
zapret: 'Zapret', zapret: 'Zapret',
zapret2: 'Zapret2', zapret2: 'Zapret2',
byedpi: 'ByeDPI', byedpi: 'ByeDPI',
wdtt: 'WDTT',
olcrtc: 'OlcRTC',
}; };
return names[component] || component; return names[component] || component;
@@ -86,7 +86,7 @@ export function getTachyonLogNotification(
} }
const update = line.match( const update = line.match(
/\[component-update\]\s+(tachyon|sing_box|zapret|zapret2|byedpi)\s+(\S+)/i, /\[component-update\]\s+(tachyon|sing_box|zapret|zapret2|byedpi|wdtt|olcrtc)\s+(\S+)/i,
); );
if (!update) { if (!update) {
@@ -233,6 +233,14 @@ export interface StoreType {
byedpi_installed: number; byedpi_installed: number;
byedpi_backup_version?: string; byedpi_backup_version?: string;
byedpi_backup_time?: number; byedpi_backup_time?: number;
wdtt_version: string;
wdtt_installed: number;
wdtt_backup_version?: string;
wdtt_backup_time?: number;
olcrtc_version: string;
olcrtc_installed: number;
olcrtc_backup_version?: string;
olcrtc_backup_time?: number;
tailscale_version: string; tailscale_version: string;
tailscale_installed: number; tailscale_installed: number;
tailscale_backup_version?: string; tailscale_backup_version?: string;
@@ -273,6 +281,14 @@ export interface StoreType {
byedpiInstall: { loading: boolean }; byedpiInstall: { loading: boolean };
byedpiRemove: { loading: boolean }; byedpiRemove: { loading: boolean };
byedpiRollback: { loading: boolean }; byedpiRollback: { loading: boolean };
wdttCheck: { loading: boolean };
wdttInstall: { loading: boolean };
wdttRemove: { loading: boolean };
wdttRollback: { loading: boolean };
olcrtcCheck: { loading: boolean };
olcrtcInstall: { loading: boolean };
olcrtcRemove: { loading: boolean };
olcrtcRollback: { loading: boolean };
tailscaleCheck: { loading: boolean }; tailscaleCheck: { loading: boolean };
tailscaleInstall: { loading: boolean }; tailscaleInstall: { loading: boolean };
tailscaleRemove: { loading: boolean }; tailscaleRemove: { loading: boolean };
@@ -24,6 +24,10 @@ const UNKNOWN_SYSTEM_INFO: StoreType['diagnosticsSystemInfo'] = {
zapret2_installed: 0, zapret2_installed: 0,
byedpi_version: _('unknown'), byedpi_version: _('unknown'),
byedpi_installed: 0, byedpi_installed: 0,
wdtt_version: _('unknown'),
wdtt_installed: 0,
olcrtc_version: _('unknown'),
olcrtc_installed: 0,
tailscale_version: _('unknown'), tailscale_version: _('unknown'),
tailscale_installed: 0, tailscale_installed: 0,
server_inbounds_enabled_count: -1, server_inbounds_enabled_count: -1,
@@ -108,6 +112,8 @@ export async function ensureSystemInfo({
zapret_installed: latestSystemInfo.zapret_installed, zapret_installed: latestSystemInfo.zapret_installed,
zapret2_installed: latestSystemInfo.zapret2_installed, zapret2_installed: latestSystemInfo.zapret2_installed,
byedpi_installed: latestSystemInfo.byedpi_installed, byedpi_installed: latestSystemInfo.byedpi_installed,
wdtt_installed: latestSystemInfo.wdtt_installed,
olcrtc_installed: latestSystemInfo.olcrtc_installed,
server_inbounds_enabled_count: server_inbounds_enabled_count:
latestSystemInfo.server_inbounds_enabled_count, latestSystemInfo.server_inbounds_enabled_count,
}; };
@@ -43,6 +43,8 @@ function createUiState(
zapret_installed: 1, zapret_installed: 1,
zapret2_installed: 1, zapret2_installed: 1,
byedpi_installed: 0, byedpi_installed: 0,
wdtt_installed: 0,
olcrtc_installed: 0,
server_inbounds_enabled_count: 0, server_inbounds_enabled_count: 0,
}, },
actions: { actions: {
@@ -37,6 +37,8 @@ function createUiState(
zapret_installed: 1, zapret_installed: 1,
zapret2_installed: 0, zapret2_installed: 0,
byedpi_installed: 1, byedpi_installed: 1,
wdtt_installed: 0,
olcrtc_installed: 0,
server_inbounds_enabled_count: 0, server_inbounds_enabled_count: 0,
...capabilities, ...capabilities,
}, },
@@ -37,6 +37,14 @@ function getEmptyUpdatesActions(): StoreType['updatesActions'] {
byedpiInstall: { loading: false }, byedpiInstall: { loading: false },
byedpiRemove: { loading: false }, byedpiRemove: { loading: false },
byedpiRollback: { loading: false }, byedpiRollback: { loading: false },
wdttCheck: { loading: false },
wdttInstall: { loading: false },
wdttRemove: { loading: false },
wdttRollback: { loading: false },
olcrtcCheck: { loading: false },
olcrtcInstall: { loading: false },
olcrtcRemove: { loading: false },
olcrtcRollback: { loading: false },
tailscaleCheck: { loading: false }, tailscaleCheck: { loading: false },
tailscaleInstall: { loading: false }, tailscaleInstall: { loading: false },
tailscaleRemove: { loading: false }, tailscaleRemove: { loading: false },
@@ -88,6 +96,8 @@ function applyServiceState(uiState: Tachyon.UiState) {
zapret_installed: uiState.capabilities.zapret_installed, zapret_installed: uiState.capabilities.zapret_installed,
zapret2_installed: uiState.capabilities.zapret2_installed, zapret2_installed: uiState.capabilities.zapret2_installed,
byedpi_installed: uiState.capabilities.byedpi_installed, byedpi_installed: uiState.capabilities.byedpi_installed,
wdtt_installed: uiState.capabilities.wdtt_installed,
olcrtc_installed: uiState.capabilities.olcrtc_installed,
server_inbounds_enabled_count: server_inbounds_enabled_count:
uiState.capabilities.server_inbounds_enabled_count, uiState.capabilities.server_inbounds_enabled_count,
}; };
@@ -102,6 +102,14 @@ export const initialDiagnosticStore: Pick<
byedpi_installed: 0, byedpi_installed: 0,
byedpi_backup_version: '', byedpi_backup_version: '',
byedpi_backup_time: 0, byedpi_backup_time: 0,
wdtt_version: 'loading',
wdtt_installed: 0,
wdtt_backup_version: '',
wdtt_backup_time: 0,
olcrtc_version: 'loading',
olcrtc_installed: 0,
olcrtc_backup_version: '',
olcrtc_backup_time: 0,
tailscale_version: 'loading', tailscale_version: 'loading',
tailscale_installed: 0, tailscale_installed: 0,
tailscale_backup_version: '', tailscale_backup_version: '',
@@ -182,6 +190,14 @@ export const initialDiagnosticStore: Pick<
byedpiInstall: { loading: false }, byedpiInstall: { loading: false },
byedpiRemove: { loading: false }, byedpiRemove: { loading: false },
byedpiRollback: { loading: false }, byedpiRollback: { loading: false },
wdttCheck: { loading: false },
wdttInstall: { loading: false },
wdttRemove: { loading: false },
wdttRollback: { loading: false },
olcrtcCheck: { loading: false },
olcrtcInstall: { loading: false },
olcrtcRemove: { loading: false },
olcrtcRollback: { loading: false },
tailscaleCheck: { loading: false }, tailscaleCheck: { loading: false },
tailscaleInstall: { loading: false }, tailscaleInstall: { loading: false },
tailscaleRemove: { loading: false }, tailscaleRemove: { loading: false },
+87 -1
View File
@@ -69,6 +69,8 @@ export namespace Tachyon {
GET_TAILSCALE_PEERS = 'get_tailscale_peers', GET_TAILSCALE_PEERS = 'get_tailscale_peers',
GET_ZAPRET2_STATUS = 'get_zapret2_status', GET_ZAPRET2_STATUS = 'get_zapret2_status',
GET_BYEDPI_STATUS = 'get_byedpi_status', GET_BYEDPI_STATUS = 'get_byedpi_status',
GET_WDTT_STATUS = 'get_wdtt_status',
GET_OLCRTC_STATUS = 'get_olcrtc_status',
CLASH_API = 'clash_api', CLASH_API = 'clash_api',
ENABLE = 'enable', ENABLE = 'enable',
DISABLE = 'disable', DISABLE = 'disable',
@@ -200,7 +202,7 @@ export namespace Tachyon {
} }
export interface ServiceStatus { export interface ServiceStatus {
serviceType: 'zapret' | 'zapret2' | 'byedpi'; serviceType: 'zapret' | 'zapret2' | 'byedpi' | 'wdtt' | 'olcrtc';
configured: boolean; configured: boolean;
ready: boolean; ready: boolean;
conflict: boolean; conflict: boolean;
@@ -275,6 +277,8 @@ export namespace Tachyon {
| 'zapret' | 'zapret'
| 'zapret2' | 'zapret2'
| 'byedpi' | 'byedpi'
| 'wdtt'
| 'olcrtc'
| 'awg' | 'awg'
| 'warp' | 'warp'
| 'anytls' | 'anytls'
@@ -321,6 +325,9 @@ export namespace Tachyon {
ports?: string[]; ports?: string[];
protocol?: string[]; protocol?: string[];
excluded_protocol?: string[]; excluded_protocol?: string[];
routed_dns_enabled?: '0' | '1';
routed_dns_type?: 'udp' | 'dot' | 'doh' | 'doq';
routed_dns_server?: string | string[];
update_interval?: string; update_interval?: string;
proxy_string?: string; proxy_string?: string;
@@ -328,6 +335,31 @@ export namespace Tachyon {
nfqws2_opt?: string; nfqws2_opt?: string;
byedpi_cmd_opts?: string; byedpi_cmd_opts?: string;
cmd_opts?: string; cmd_opts?: string;
warp_flow_uri?: string;
warp_flow_mode?: string;
warp_flow_device_id?: string;
warp_flow_workers?: string;
warp_flow_max_hashes?: string;
warp_flow_mtu?: string;
warp_flow_refresh?: string;
warp_flow_auto_update?: boolean;
warp_flow_block_doh?: boolean;
warp_flow_block_ipv6?: boolean;
warp_flow_subscription_links?: string[];
warp_flow_community_lists?: string[];
warp_flow_remote_domain_list?: string[];
warp_flow_qwdtt_mode?: string;
olcrtc_provider?: string;
olcrtc_transport?: string;
olcrtc_room_id?: string;
olcrtc_crypto_key?: string;
olcrtc_socks_host?: string;
olcrtc_socks_port?: string;
olcrtc_dns_server?: string;
olcrtc_subscription_links?: string[];
selector_proxy_links?: string[]; selector_proxy_links?: string[];
subscription_urls?: string[]; subscription_urls?: string[];
interfaces?: string[]; interfaces?: string[];
@@ -610,6 +642,10 @@ export namespace Tachyon {
zapret2_installed: 0 | 1; zapret2_installed: 0 | 1;
byedpi_version: string; byedpi_version: string;
byedpi_installed: 0 | 1; byedpi_installed: 0 | 1;
wdtt_version: string;
wdtt_installed: 0 | 1;
olcrtc_version: string;
olcrtc_installed: 0 | 1;
tailscale_version: string; tailscale_version: string;
tailscale_installed: 0 | 1; tailscale_installed: 0 | 1;
direct_bypass_enabled?: 0 | 1; direct_bypass_enabled?: 0 | 1;
@@ -639,6 +675,8 @@ export namespace Tachyon {
zapret_installed: 0 | 1; zapret_installed: 0 | 1;
zapret2_installed: 0 | 1; zapret2_installed: 0 | 1;
byedpi_installed: 0 | 1; byedpi_installed: 0 | 1;
wdtt_installed: 0 | 1;
olcrtc_installed: 0 | 1;
server_inbounds_enabled_count: number; server_inbounds_enabled_count: number;
} }
@@ -887,6 +925,54 @@ export namespace Tachyon {
status_message: string; status_message: string;
} }
export interface GetWdttStatus {
installed: 0 | 1;
package_installed: 0 | 1;
provider_available: 0 | 1;
provider_path: string;
version: string;
configured: 0 | 1;
enabled_rule_count: number;
expected_process_count: number;
running_process_count: number;
supervisor_process_count: number;
restart_count: number;
runtime_unstable: 0 | 1;
standalone_service_enabled: 0 | 1;
standalone_service_running: 0 | 1;
listen_address: string;
port_base: number;
outbounds_configured: 0 | 1;
routes_configured: 0 | 1;
ready: 0 | 1;
conflict: 0 | 1;
status_message: string;
}
export interface GetOlcrtcStatus {
installed: 0 | 1;
package_installed: 0 | 1;
provider_available: 0 | 1;
provider_path: string;
version: string;
configured: 0 | 1;
enabled_rule_count: number;
expected_process_count: number;
running_process_count: number;
supervisor_process_count: number;
restart_count: number;
runtime_unstable: 0 | 1;
standalone_service_enabled: 0 | 1;
standalone_service_running: 0 | 1;
listen_address: string;
port_base: number;
outbounds_configured: 0 | 1;
routes_configured: 0 | 1;
ready: 0 | 1;
conflict: 0 | 1;
status_message: string;
}
export interface ByedpiCheckResult { export interface ByedpiCheckResult {
byedpi_installed: 0 | 1; byedpi_installed: 0 | 1;
byedpi_package_installed: 0 | 1; byedpi_package_installed: 0 | 1;
@@ -3407,6 +3407,8 @@ var Tachyon;
AvailableMethods2["GET_TAILSCALE_PEERS"] = "get_tailscale_peers"; AvailableMethods2["GET_TAILSCALE_PEERS"] = "get_tailscale_peers";
AvailableMethods2["GET_ZAPRET2_STATUS"] = "get_zapret2_status"; AvailableMethods2["GET_ZAPRET2_STATUS"] = "get_zapret2_status";
AvailableMethods2["GET_BYEDPI_STATUS"] = "get_byedpi_status"; AvailableMethods2["GET_BYEDPI_STATUS"] = "get_byedpi_status";
AvailableMethods2["GET_WDTT_STATUS"] = "get_wdtt_status";
AvailableMethods2["GET_OLCRTC_STATUS"] = "get_olcrtc_status";
AvailableMethods2["CLASH_API"] = "clash_api"; AvailableMethods2["CLASH_API"] = "clash_api";
AvailableMethods2["ENABLE"] = "enable"; AvailableMethods2["ENABLE"] = "enable";
AvailableMethods2["DISABLE"] = "disable"; AvailableMethods2["DISABLE"] = "disable";
@@ -3734,6 +3736,18 @@ var TachyonShellMethods = {
"/usr/bin/tachyon", "/usr/bin/tachyon",
{ allowNonZeroWithStdout: true } { allowNonZeroWithStdout: true }
), ),
getWdttStatus: async () => callBaseMethod(
Tachyon.AvailableMethods.GET_WDTT_STATUS,
[],
"/usr/bin/tachyon",
{ allowNonZeroWithStdout: true }
),
getOlcrtcStatus: async () => callBaseMethod(
Tachyon.AvailableMethods.GET_OLCRTC_STATUS,
[],
"/usr/bin/tachyon",
{ allowNonZeroWithStdout: true }
),
getClashApiProxies: async () => callBaseMethod(Tachyon.AvailableMethods.CLASH_API, [ getClashApiProxies: async () => callBaseMethod(Tachyon.AvailableMethods.CLASH_API, [
Tachyon.AvailableClashAPIMethods.GET_PROXIES Tachyon.AvailableClashAPIMethods.GET_PROXIES
]), ]),
@@ -5045,7 +5059,9 @@ function isConnectionAction(action) {
); );
} }
function isServiceAction(action) { function isServiceAction(action) {
return Boolean(action && ["zapret", "zapret2", "byedpi"].includes(action)); return Boolean(
action && ["zapret", "zapret2", "byedpi", "wdtt", "olcrtc"].includes(action)
);
} }
function hasSubscriptionSources(section) { function hasSubscriptionSources(section) {
return getSubscriptionSourceCount(section) > 0; return getSubscriptionSourceCount(section) > 0;
@@ -5826,6 +5842,38 @@ async function getDashboardSections(options = {}) {
statusMessage: s.status_message statusMessage: s.status_message
}; };
} }
} else if (serviceType === "wdtt") {
const result = await TachyonShellMethods.getWdttStatus();
if (result.success && result.data) {
const s = result.data;
return {
serviceType: "wdtt",
configured: Boolean(s.configured),
ready: Boolean(s.ready),
conflict: Boolean(s.conflict),
runningProcesses: s.running_process_count,
expectedProcesses: s.expected_process_count,
restartCount: s.restart_count,
unstable: Boolean(s.runtime_unstable),
statusMessage: s.status_message
};
}
} else if (serviceType === "olcrtc") {
const result = await TachyonShellMethods.getOlcrtcStatus();
if (result.success && result.data) {
const s = result.data;
return {
serviceType: "olcrtc",
configured: Boolean(s.configured),
ready: Boolean(s.ready),
conflict: Boolean(s.conflict),
runningProcesses: s.running_process_count,
expectedProcesses: s.expected_process_count,
restartCount: s.restart_count,
unstable: Boolean(s.runtime_unstable),
statusMessage: s.status_message
};
}
} }
} catch (_error) { } catch (_error) {
} }
@@ -6227,6 +6275,14 @@ var initialDiagnosticStore = {
byedpi_installed: 0, byedpi_installed: 0,
byedpi_backup_version: "", byedpi_backup_version: "",
byedpi_backup_time: 0, byedpi_backup_time: 0,
wdtt_version: "loading",
wdtt_installed: 0,
wdtt_backup_version: "",
wdtt_backup_time: 0,
olcrtc_version: "loading",
olcrtc_installed: 0,
olcrtc_backup_version: "",
olcrtc_backup_time: 0,
tailscale_version: "loading", tailscale_version: "loading",
tailscale_installed: 0, tailscale_installed: 0,
tailscale_backup_version: "", tailscale_backup_version: "",
@@ -6307,6 +6363,14 @@ var initialDiagnosticStore = {
byedpiInstall: { loading: false }, byedpiInstall: { loading: false },
byedpiRemove: { loading: false }, byedpiRemove: { loading: false },
byedpiRollback: { loading: false }, byedpiRollback: { loading: false },
wdttCheck: { loading: false },
wdttInstall: { loading: false },
wdttRemove: { loading: false },
wdttRollback: { loading: false },
olcrtcCheck: { loading: false },
olcrtcInstall: { loading: false },
olcrtcRemove: { loading: false },
olcrtcRollback: { loading: false },
tailscaleCheck: { loading: false }, tailscaleCheck: { loading: false },
tailscaleInstall: { loading: false }, tailscaleInstall: { loading: false },
tailscaleRemove: { loading: false }, tailscaleRemove: { loading: false },
@@ -6632,7 +6696,7 @@ function getTachyonLogNotification(line) {
return { kind: "error", line }; return { kind: "error", line };
} }
const update = line.match( const update = line.match(
/\[component-update\]\s+(tachyon|sing_box|zapret|zapret2|byedpi)\s+(\S+)/i /\[component-update\]\s+(tachyon|sing_box|zapret|zapret2|byedpi|wdtt|olcrtc)\s+(\S+)/i
); );
if (!update) { if (!update) {
return null; return null;
@@ -6697,6 +6761,16 @@ var componentActionKeyMap = {
"byedpi:install_version": "byedpiInstall", "byedpi:install_version": "byedpiInstall",
"byedpi:remove": "byedpiRemove", "byedpi:remove": "byedpiRemove",
"byedpi:rollback": "byedpiRollback", "byedpi:rollback": "byedpiRollback",
"wdtt:check_update": "wdttCheck",
"wdtt:install": "wdttInstall",
"wdtt:install_version": "wdttInstall",
"wdtt:remove": "wdttRemove",
"wdtt:rollback": "wdttRollback",
"olcrtc:check_update": "olcrtcCheck",
"olcrtc:install": "olcrtcInstall",
"olcrtc:install_version": "olcrtcInstall",
"olcrtc:remove": "olcrtcRemove",
"olcrtc:rollback": "olcrtcRollback",
"tailscale:check_update": "tailscaleCheck", "tailscale:check_update": "tailscaleCheck",
"tailscale:install": "tailscaleInstall", "tailscale:install": "tailscaleInstall",
"tailscale:install_version": "tailscaleInstall", "tailscale:install_version": "tailscaleInstall",
@@ -6872,6 +6946,14 @@ function getEmptyUpdatesActions() {
byedpiInstall: { loading: false }, byedpiInstall: { loading: false },
byedpiRemove: { loading: false }, byedpiRemove: { loading: false },
byedpiRollback: { loading: false }, byedpiRollback: { loading: false },
wdttCheck: { loading: false },
wdttInstall: { loading: false },
wdttRemove: { loading: false },
wdttRollback: { loading: false },
olcrtcCheck: { loading: false },
olcrtcInstall: { loading: false },
olcrtcRemove: { loading: false },
olcrtcRollback: { loading: false },
tailscaleCheck: { loading: false }, tailscaleCheck: { loading: false },
tailscaleInstall: { loading: false }, tailscaleInstall: { loading: false },
tailscaleRemove: { loading: false }, tailscaleRemove: { loading: false },
@@ -6913,6 +6995,8 @@ function applyServiceState(uiState) {
zapret_installed: uiState.capabilities.zapret_installed, zapret_installed: uiState.capabilities.zapret_installed,
zapret2_installed: uiState.capabilities.zapret2_installed, zapret2_installed: uiState.capabilities.zapret2_installed,
byedpi_installed: uiState.capabilities.byedpi_installed, byedpi_installed: uiState.capabilities.byedpi_installed,
wdtt_installed: uiState.capabilities.wdtt_installed,
olcrtc_installed: uiState.capabilities.olcrtc_installed,
server_inbounds_enabled_count: uiState.capabilities.server_inbounds_enabled_count server_inbounds_enabled_count: uiState.capabilities.server_inbounds_enabled_count
}; };
nextSystemInfo.sing_box_extended = uiState.capabilities.sing_box_extended; nextSystemInfo.sing_box_extended = uiState.capabilities.sing_box_extended;
@@ -7146,7 +7230,9 @@ function componentDisplayName(component) {
sing_box: "sing-box", sing_box: "sing-box",
zapret: "Zapret", zapret: "Zapret",
zapret2: "Zapret2", zapret2: "Zapret2",
byedpi: "ByeDPI" byedpi: "ByeDPI",
wdtt: "WDTT",
olcrtc: "OlcRTC"
}; };
return names[component] || component; return names[component] || component;
} }
@@ -10767,6 +10853,7 @@ async function runZapret2Check() {
const tachyonRuntimeReady = !hasZapret2Rules || runningProcesses === expectedProcesses && supervisorProcesses === expectedProcesses; const tachyonRuntimeReady = !hasZapret2Rules || runningProcesses === expectedProcesses && supervisorProcesses === expectedProcesses;
const unexpectedRuntime = !hasZapret2Rules && (runningProcesses > 0 || supervisorProcesses > 0); const unexpectedRuntime = !hasZapret2Rules && (runningProcesses > 0 || supervisorProcesses > 0);
const outboundsConfigured = Boolean(data.outbounds_configured); const outboundsConfigured = Boolean(data.outbounds_configured);
const routesConfigured = Boolean(data.routes_configured);
const standaloneServiceEnabled = Boolean(data.standalone_service_enabled); const standaloneServiceEnabled = Boolean(data.standalone_service_enabled);
const standaloneServiceRunning = Boolean(data.standalone_service_running); const standaloneServiceRunning = Boolean(data.standalone_service_running);
const standaloneConflict = hasZapret2Rules && standaloneServiceRunning; const standaloneConflict = hasZapret2Rules && standaloneServiceRunning;
@@ -10802,6 +10889,11 @@ async function runZapret2Check() {
key: outboundsConfigured ? _("Zapret2 sing-box outbound is configured") : _("Zapret2 sing-box outbound is not configured"), key: outboundsConfigured ? _("Zapret2 sing-box outbound is configured") : _("Zapret2 sing-box outbound is not configured"),
value: "" value: ""
}, },
{
state: !hasZapret2Rules || routesConfigured ? "success" : "error",
key: routesConfigured ? _("Zapret2 sing-box route rules are configured") : _("Zapret2 sing-box route rules are not configured"),
value: ""
},
{ {
state: standaloneConflict ? "error" : standaloneAutostartRisk ? "warning" : "success", state: standaloneConflict ? "error" : standaloneAutostartRisk ? "warning" : "success",
key: standaloneServiceRunning ? hasZapret2Rules ? _( key: standaloneServiceRunning ? hasZapret2Rules ? _(
@@ -10925,6 +11017,10 @@ var UNKNOWN_SYSTEM_INFO = {
zapret2_installed: 0, zapret2_installed: 0,
byedpi_version: _("unknown"), byedpi_version: _("unknown"),
byedpi_installed: 0, byedpi_installed: 0,
wdtt_version: _("unknown"),
wdtt_installed: 0,
olcrtc_version: _("unknown"),
olcrtc_installed: 0,
tailscale_version: _("unknown"), tailscale_version: _("unknown"),
tailscale_installed: 0, tailscale_installed: 0,
server_inbounds_enabled_count: -1, server_inbounds_enabled_count: -1,
@@ -10990,6 +11086,8 @@ async function ensureSystemInfo({
zapret_installed: latestSystemInfo.zapret_installed, zapret_installed: latestSystemInfo.zapret_installed,
zapret2_installed: latestSystemInfo.zapret2_installed, zapret2_installed: latestSystemInfo.zapret2_installed,
byedpi_installed: latestSystemInfo.byedpi_installed, byedpi_installed: latestSystemInfo.byedpi_installed,
wdtt_installed: latestSystemInfo.wdtt_installed,
olcrtc_installed: latestSystemInfo.olcrtc_installed,
server_inbounds_enabled_count: latestSystemInfo.server_inbounds_enabled_count server_inbounds_enabled_count: latestSystemInfo.server_inbounds_enabled_count
}; };
store.set({ store.set({
@@ -23,6 +23,8 @@ const ROUTING_ACTIONS = [
"zapret", "zapret",
"zapret2", "zapret2",
"byedpi", "byedpi",
"wdtt",
"olcrtc",
"awg", "awg",
"warp", "warp",
"anytls", "anytls",
@@ -487,6 +489,8 @@ const actionProvidersAvailabilityState = {
zapretInstalled: false, zapretInstalled: false,
zapret2Installed: false, zapret2Installed: false,
byedpiInstalled: false, byedpiInstalled: false,
wdttInstalled: false,
olcrtcInstalled: false,
torInstalled: false, torInstalled: false,
singBoxExtended: false, singBoxExtended: false,
}; };
@@ -669,6 +673,18 @@ function updateActionProvidersAvailabilityState(nextState) {
); );
} }
if (typeof nextState.wdttInstalled !== "undefined") {
actionProvidersAvailabilityState.wdttInstalled = Boolean(
nextState.wdttInstalled,
);
}
if (typeof nextState.olcrtcInstalled !== "undefined") {
actionProvidersAvailabilityState.olcrtcInstalled = Boolean(
nextState.olcrtcInstalled,
);
}
if (typeof nextState.singBoxExtended !== "undefined") { if (typeof nextState.singBoxExtended !== "undefined") {
actionProvidersAvailabilityState.singBoxExtended = Boolean( actionProvidersAvailabilityState.singBoxExtended = Boolean(
nextState.singBoxExtended, nextState.singBoxExtended,
@@ -693,6 +709,8 @@ function updateActionProvidersAvailabilityFromSystemInfo(systemInfo) {
zapretInstalled: Boolean(systemInfo.zapret_installed), zapretInstalled: Boolean(systemInfo.zapret_installed),
zapret2Installed: Boolean(systemInfo.zapret2_installed), zapret2Installed: Boolean(systemInfo.zapret2_installed),
byedpiInstalled: Boolean(systemInfo.byedpi_installed), byedpiInstalled: Boolean(systemInfo.byedpi_installed),
wdttInstalled: Boolean(systemInfo.wdtt_installed),
olcrtcInstalled: Boolean(systemInfo.olcrtc_installed),
singBoxExtended: Boolean(systemInfo.sing_box_extended), singBoxExtended: Boolean(systemInfo.sing_box_extended),
torInstalled: Boolean(systemInfo.tor_installed), torInstalled: Boolean(systemInfo.tor_installed),
}); });
@@ -2102,6 +2120,14 @@ function isDownloadThroughTargetSection(section, currentSectionId) {
return isByedpiInstalledForUi(); return isByedpiInstalledForUi();
} }
if (action === "wdtt") {
return isWdttInstalledForUi();
}
if (action === "olcrtc") {
return isOlcrtcInstalledForUi();
}
return false; return false;
} }
@@ -4534,6 +4560,14 @@ function isByedpiInstalledForUi() {
return actionProvidersAvailabilityState.byedpiInstalled; return actionProvidersAvailabilityState.byedpiInstalled;
} }
function isWdttInstalledForUi() {
return actionProvidersAvailabilityState.wdttInstalled;
}
function isOlcrtcInstalledForUi() {
return actionProvidersAvailabilityState.olcrtcInstalled;
}
function isSingBoxExtendedForUi() { function isSingBoxExtendedForUi() {
return actionProvidersAvailabilityState.singBoxExtended; return actionProvidersAvailabilityState.singBoxExtended;
} }
@@ -4565,6 +4599,10 @@ function getActionOptionLabel(action) {
return "Zapret2"; return "Zapret2";
case "byedpi": case "byedpi":
return "ByeDPI"; return "ByeDPI";
case "wdtt":
return "WDTT";
case "olcrtc":
return "OlcRTC";
case "awg": case "awg":
return "AmneziaWG"; return "AmneziaWG";
@@ -4607,6 +4645,14 @@ function getRuleActionDisplayValue(section_id) {
return "ByeDPI"; return "ByeDPI";
} }
if (action === "wdtt") {
return "WDTT";
}
if (action === "olcrtc") {
return "OlcRTC";
}
if (action === "awg") { if (action === "awg") {
return "AmneziaWG"; return "AmneziaWG";
} }
@@ -4673,6 +4719,12 @@ function populateActionOptionValues(option) {
if (isByedpiInstalledForUi()) { if (isByedpiInstalledForUi()) {
option.value("byedpi", getActionOptionLabel("byedpi")); option.value("byedpi", getActionOptionLabel("byedpi"));
} }
if (isWdttInstalledForUi()) {
option.value("wdtt", getActionOptionLabel("wdtt"));
}
if (isOlcrtcInstalledForUi()) {
option.value("olcrtc", getActionOptionLabel("olcrtc"));
}
option.value("hosts", getActionOptionLabel("hosts")); option.value("hosts", getActionOptionLabel("hosts"));
} }
@@ -7819,6 +7871,227 @@ function createSectionContent(section) {
}; };
configureTextareaOption(o, analyzeByedpiStrategy); configureTextareaOption(o, analyzeByedpiStrategy);
// ─── WDTT Fields ──────────────────────────────────────────────────────
o = section.taboption(
"settings",
form.Value,
"warp_flow_uri",
_("WDTT URI"),
_("WDTT connection URI (wdtt://...)"),
);
o.depends("action", "wdtt");
o.modalonly = true;
o = section.taboption(
"settings",
form.ListValue,
"warp_flow_mode",
_("WDTT Mode"),
);
o.depends("action", "wdtt");
o.value("", _("Default"));
o.value("freedom", _("Freedom"));
o.value("captive", _("Captive"));
o.value("camou", _("Camou"));
o.modalonly = true;
o = section.taboption(
"settings",
form.Value,
"warp_flow_device_id",
_("WDTT Device ID"),
);
o.depends("action", "wdtt");
o.modalonly = true;
o = section.taboption(
"settings",
form.Value,
"warp_flow_workers",
_("WDTT Workers"),
);
o.depends("action", "wdtt");
o.modalonly = true;
o = section.taboption(
"settings",
form.Value,
"warp_flow_max_hashes",
_("WDTT Max Hashes"),
);
o.depends("action", "wdtt");
o.modalonly = true;
o = section.taboption(
"settings",
form.Value,
"warp_flow_mtu",
_("WDTT MTU"),
);
o.depends("action", "wdtt");
o.modalonly = true;
o = section.taboption(
"settings",
form.Value,
"warp_flow_refresh",
_("WDTT Refresh"),
);
o.depends("action", "wdtt");
o.modalonly = true;
o = section.taboption(
"settings",
form.Flag,
"warp_flow_auto_update",
_("WDTT Auto Update"),
);
o.default = "1";
o.depends("action", "wdtt");
o.modalonly = true;
o = section.taboption(
"settings",
form.Flag,
"warp_flow_block_doh",
_("WDTT Block DoH"),
);
o.default = "0";
o.depends("action", "wdtt");
o.modalonly = true;
o = section.taboption(
"settings",
form.Flag,
"warp_flow_block_ipv6",
_("WDTT Block IPv6"),
);
o.default = "0";
o.depends("action", "wdtt");
o.modalonly = true;
o = section.taboption(
"settings",
form.ListValue,
"warp_flow_qwdtt_mode",
_("WDTT QWDTT Mode"),
);
o.depends("action", "wdtt");
o.value("", _("Default"));
o.value("client", _("Client"));
o.value("server", _("Server"));
o.modalonly = true;
o = section.taboption(
"settings",
form.DynamicList,
"warp_flow_subscription_links",
_("WDTT Subscription Links"),
);
o.depends("action", "wdtt");
o.modalonly = true;
o = section.taboption(
"settings",
form.DynamicList,
"warp_flow_community_lists",
_("WDTT Community Lists"),
);
o.depends("action", "wdtt");
o.modalonly = true;
o = section.taboption(
"settings",
form.DynamicList,
"warp_flow_remote_domain_list",
_("WDTT Remote Domain List"),
);
o.depends("action", "wdtt");
o.modalonly = true;
// ─── OlcRTC Fields ────────────────────────────────────────────────────
o = section.taboption(
"settings",
form.ListValue,
"olcrtc_provider",
_("OlcRTC Provider"),
);
o.depends("action", "olcrtc");
o.value("jitsi", _("Jitsi"));
o.value("wrt", _("WRT"));
o.modalonly = true;
o = section.taboption(
"settings",
form.ListValue,
"olcrtc_transport",
_("OlcRTC Transport"),
);
o.depends("action", "olcrtc");
o.value("whep", _("WHEP"));
o.value("whip", _("WHIP"));
o.modalonly = true;
o = section.taboption(
"settings",
form.Value,
"olcrtc_room_id",
_("OlcRTC Room ID"),
);
o.depends("action", "olcrtc");
o.modalonly = true;
o = section.taboption(
"settings",
form.Value,
"olcrtc_crypto_key",
_("OlcRTC Crypto Key"),
);
o.depends("action", "olcrtc");
o.password = true;
o.modalonly = true;
o = section.taboption(
"settings",
form.Value,
"olcrtc_socks_host",
_("OlcRTC SOCKS Host"),
);
o.depends("action", "olcrtc");
o.default = "127.0.0.1";
o.modalonly = true;
o = section.taboption(
"settings",
form.Value,
"olcrtc_socks_port",
_("OlcRTC SOCKS Port"),
);
o.depends("action", "olcrtc");
o.default = "1082";
o.modalonly = true;
o = section.taboption(
"settings",
form.Value,
"olcrtc_dns_server",
_("OlcRTC DNS Server"),
);
o.depends("action", "olcrtc");
o.default = "8.8.8.8:53";
o.modalonly = true;
o = section.taboption(
"settings",
form.DynamicList,
"olcrtc_subscription_links",
_("OlcRTC Subscription Links"),
);
o.depends("action", "olcrtc");
o.modalonly = true;
// ─── Hosts Import Helpers ────────────────────────────────────────────── // ─── Hosts Import Helpers ──────────────────────────────────────────────
function parseHostsFile(text, options) { function parseHostsFile(text, options) {
@@ -10333,6 +10606,8 @@ function createSectionContent(section) {
o.depends("action", "connection"); o.depends("action", "connection");
o.depends("action", "awg"); o.depends("action", "awg");
o.depends("action", "byedpi"); o.depends("action", "byedpi");
o.depends("action", "wdtt");
o.depends("action", "olcrtc");
o.depends("action", "zapret"); o.depends("action", "zapret");
o.depends("action", "zapret2"); o.depends("action", "zapret2");
o.modalonly = true; o.modalonly = true;
@@ -10814,11 +11089,47 @@ function createSectionContent(section) {
key: "excluded_ips", key: "excluded_ips",
label: _("Excluded devices"), label: _("Excluded devices"),
description: _( description: _(
"Traffic from these IP addresses will never be routed through this section, even if other conditions match.", "Traffic from these IP addresses, CIDRs, or MAC addresses will never be routed through this section, even if other conditions match.",
), ),
}); });
dependsOnRoutingAction(excludedIpsOption); dependsOnRoutingAction(excludedIpsOption);
const routedDnsEnabledOption = section.taboption(
"conditions",
form.Flag,
"routed_dns_enabled",
_("Routed DNS"),
_("Resolve matched domains through an encrypted DNS server of this section instead of FakeIP. DNS queries traverse the same proxy as the section's outbound."),
);
dependsOnRoutingAction(routedDnsEnabledOption);
routedDnsEnabledOption.default = "0";
routedDnsEnabledOption.rmempty = false;
routedDnsEnabledOption.modalonly = true;
const routedDnsTypeOption = section.taboption(
"conditions",
form.ListValue,
"routed_dns_type",
_("Routed DNS Protocol"),
_("Protocol for the routed DNS resolver."),
);
routedDnsTypeOption.depends({ routed_dns_enabled: "1" });
routedDnsTypeOption.rmempty = false;
routedDnsTypeOption.modalonly = true;
routedDnsTypeOption.value("udp", "UDP");
routedDnsTypeOption.value("dot", "DNS over TLS");
routedDnsTypeOption.value("doh", "DNS over HTTPS");
routedDnsTypeOption.value("doq", "DNS over QUIC");
const routedDnsServerOption = addDynamicConditionField(section, {
key: "routed_dns_server",
label: _("Routed DNS Servers"),
description: _(
"DNS server addresses for domain resolution in this section (e.g. 1.1.1.1, https://cloudflare-dns.com/dns-query).",
),
});
routedDnsServerOption.depends({ routed_dns_enabled: "1" });
const portsOption = addDynamicConditionField(section, { const portsOption = addDynamicConditionField(section, {
key: "ports", key: "ports",
label: _("Ports"), label: _("Ports"),
@@ -10914,6 +11225,8 @@ const ACTION_COLORS = {
zapret: "#8e44ad", zapret: "#8e44ad",
zapret2: "#9b59b6", zapret2: "#9b59b6",
byedpi: "#d35400", byedpi: "#d35400",
wdtt: "#27ae60",
olcrtc: "#2980b9",
awg: "#008080", awg: "#008080",
warp: "#e67e22", warp: "#e67e22",
anytls: "#16a085", anytls: "#16a085",
+95 -2
View File
@@ -839,6 +839,81 @@ msgstr "Стратегия ByeDPI"
msgid "ByeDPI strategy cannot be empty" msgid "ByeDPI strategy cannot be empty"
msgstr "Стратегия ByeDPI не может быть пустой" msgstr "Стратегия ByeDPI не может быть пустой"
msgid "WDTT"
msgstr "WDTT"
msgid "WDTT URI"
msgstr "URI WDTT"
msgid "WDTT connection URI (wdtt://...)"
msgstr "URI подключения WDTT (wdtt://...)"
msgid "WDTT Mode"
msgstr "Режим WDTT"
msgid "WDTT Device ID"
msgstr "ID устройства WDTT"
msgid "WDTT Workers"
msgstr "Рабочие процессы WDTT"
msgid "WDTT Max Hashes"
msgstr "Максимум хешей WDTT"
msgid "WDTT MTU"
msgstr "MTU WDTT"
msgid "WDTT Refresh"
msgstr "Обновление WDTT"
msgid "WDTT Auto Update"
msgstr "Автообновление WDTT"
msgid "WDTT Block DoH"
msgstr "Блокировка DoH в WDTT"
msgid "WDTT Block IPv6"
msgstr "Блокировка IPv6 в WDTT"
msgid "WDTT QWDTT Mode"
msgstr "Режим QWDTT"
msgid "WDTT Subscription Links"
msgstr "Ссылки подписок WDTT"
msgid "WDTT Community Lists"
msgstr "Списки сообществ WDTT"
msgid "WDTT Remote Domain List"
msgstr "Удалённый список доменов WDTT"
msgid "OlcRTC"
msgstr "OlcRTC"
msgid "OlcRTC Provider"
msgstr "Провайдер OlcRTC"
msgid "OlcRTC Transport"
msgstr "Транспорт OlcRTC"
msgid "OlcRTC Room ID"
msgstr "ID комнаты OlcRTC"
msgid "OlcRTC Crypto Key"
msgstr "Ключ шифрования OlcRTC"
msgid "OlcRTC SOCKS Host"
msgstr "Хост SOCKS OlcRTC"
msgid "OlcRTC SOCKS Port"
msgstr "Порт SOCKS OlcRTC"
msgid "OlcRTC DNS Server"
msgstr "DNS-сервер OlcRTC"
msgid "OlcRTC Subscription Links"
msgstr "Ссылки подписок OlcRTC"
msgid "Bypasses proxy (SO_MARK 0x08000000)" msgid "Bypasses proxy (SO_MARK 0x08000000)"
msgstr "В обход прокси (SO_MARK 0x08000000)" msgstr "В обход прокси (SO_MARK 0x08000000)"
@@ -1445,6 +1520,24 @@ msgstr "DNS через прокси"
msgid "DNS through section" msgid "DNS through section"
msgstr "DNS через секцию" msgstr "DNS через секцию"
msgid "Routed DNS"
msgstr "Маршрутизированный DNS"
msgid "Resolve matched domains through an encrypted DNS server of this section instead of FakeIP. DNS queries traverse the same proxy as the section's outbound."
msgstr "Резолвить совпавшие домены через зашифрованный DNS-сервер этой секции вместо FakeIP. DNS-запросы проходят через тот же прокси, что и исходящий трафик секции."
msgid "Routed DNS Protocol"
msgstr "Протокол маршрутизированного DNS"
msgid "Protocol for the routed DNS resolver."
msgstr "Протокол для DNS-резолвера маршрутизации."
msgid "Routed DNS Servers"
msgstr "Серверы маршрутизированного DNS"
msgid "DNS server addresses for domain resolution in this section (e.g. 1.1.1.1, https://cloudflare-dns.com/dns-query)."
msgstr "Адреса DNS-серверов для резолвинга доменов в этой секции (напр. 1.1.1.1, https://cloudflare-dns.com/dns-query)."
msgid "DNS Turbo Cache" msgid "DNS Turbo Cache"
msgstr "Быстрый DNS-кэш" msgstr "Быстрый DNS-кэш"
@@ -5096,8 +5189,8 @@ msgstr "Трафик"
msgid "Traffic for this level goes directly." msgid "Traffic for this level goes directly."
msgstr "Трафик этого уровня идёт напрямую." msgstr "Трафик этого уровня идёт напрямую."
msgid "Traffic from these IP addresses will never be routed through this section, even if other conditions match." msgid "Traffic from these IP addresses, CIDRs, or MAC addresses will never be routed through this section, even if other conditions match."
msgstr "Трафик от этих IP-адресов никогда не будет маршрутизирован через эту секцию, даже если другие условия совпадают." msgstr "Трафик от этих IP-адресов, CIDR или MAC-адресов никогда не будет маршрутизирован через эту секцию, даже если другие условия совпадают."
msgid "Traffic of specified protocols (e.g. BitTorrent, QUIC, STUN) will bypass this section." msgid "Traffic of specified protocols (e.g. BitTorrent, QUIC, STUN) will bypass this section."
msgstr "Трафик указанных протоколов (например BitTorrent, QUIC, STUN) будет обходить эту секцию." msgstr "Трафик указанных протоколов (например BitTorrent, QUIC, STUN) будет обходить эту секцию."
+94 -1
View File
@@ -1329,6 +1329,81 @@ msgstr ""
msgid "ByeDPI strategy cannot be empty" msgid "ByeDPI strategy cannot be empty"
msgstr "" msgstr ""
msgid "WDTT"
msgstr ""
msgid "WDTT URI"
msgstr ""
msgid "WDTT connection URI (wdtt://...)"
msgstr ""
msgid "WDTT Mode"
msgstr ""
msgid "WDTT Device ID"
msgstr ""
msgid "WDTT Workers"
msgstr ""
msgid "WDTT Max Hashes"
msgstr ""
msgid "WDTT MTU"
msgstr ""
msgid "WDTT Refresh"
msgstr ""
msgid "WDTT Auto Update"
msgstr ""
msgid "WDTT Block DoH"
msgstr ""
msgid "WDTT Block IPv6"
msgstr ""
msgid "WDTT QWDTT Mode"
msgstr ""
msgid "WDTT Subscription Links"
msgstr ""
msgid "WDTT Community Lists"
msgstr ""
msgid "WDTT Remote Domain List"
msgstr ""
msgid "OlcRTC"
msgstr ""
msgid "OlcRTC Provider"
msgstr ""
msgid "OlcRTC Transport"
msgstr ""
msgid "OlcRTC Room ID"
msgstr ""
msgid "OlcRTC Crypto Key"
msgstr ""
msgid "OlcRTC SOCKS Host"
msgstr ""
msgid "OlcRTC SOCKS Port"
msgstr ""
msgid "OlcRTC DNS Server"
msgstr ""
msgid "OlcRTC Subscription Links"
msgstr ""
#: ../luci-app-tachyon/htdocs/luci-static/resources/view/tachyon/main.js:14975 #: ../luci-app-tachyon/htdocs/luci-static/resources/view/tachyon/main.js:14975
#: src/tachyon/tabs/diagnostic/partials/renderLeakCheckModal.ts:170 #: src/tachyon/tabs/diagnostic/partials/renderLeakCheckModal.ts:170
msgid "Bypasses proxy (SO_MARK 0x08000000)" msgid "Bypasses proxy (SO_MARK 0x08000000)"
@@ -2355,6 +2430,24 @@ msgstr ""
msgid "DNS through section" msgid "DNS through section"
msgstr "" msgstr ""
msgid "Routed DNS"
msgstr ""
msgid "Resolve matched domains through an encrypted DNS server of this section instead of FakeIP. DNS queries traverse the same proxy as the section's outbound."
msgstr ""
msgid "Routed DNS Protocol"
msgstr ""
msgid "Protocol for the routed DNS resolver."
msgstr ""
msgid "Routed DNS Servers"
msgstr ""
msgid "DNS server addresses for domain resolution in this section (e.g. 1.1.1.1, https://cloudflare-dns.com/dns-query)."
msgstr ""
#: ../luci-app-tachyon/htdocs/luci-static/resources/view/tachyon/settings.js:1615 #: ../luci-app-tachyon/htdocs/luci-static/resources/view/tachyon/settings.js:1615
msgid "DNS Turbo Cache" msgid "DNS Turbo Cache"
msgstr "" msgstr ""
@@ -8155,7 +8248,7 @@ msgid "Traffic for this level goes directly."
msgstr "" msgstr ""
#: ../luci-app-tachyon/htdocs/luci-static/resources/view/tachyon/section.js:10821 #: ../luci-app-tachyon/htdocs/luci-static/resources/view/tachyon/section.js:10821
msgid "Traffic from these IP addresses will never be routed through this section, even if other conditions match." msgid "Traffic from these IP addresses, CIDRs, or MAC addresses will never be routed through this section, even if other conditions match."
msgstr "" msgstr ""
#: ../luci-app-tachyon/htdocs/luci-static/resources/view/tachyon/section.js:10849 #: ../luci-app-tachyon/htdocs/luci-static/resources/view/tachyon/section.js:10849
+4
View File
@@ -112,6 +112,8 @@ function show_help() {
print(" get_zapret_status Get zapret provider and Tachyon-managed nfqws status\n"); print(" get_zapret_status Get zapret provider and Tachyon-managed nfqws status\n");
print(" get_zapret2_status Get zapret2 provider and Tachyon-managed nfqws2 status\n"); print(" get_zapret2_status Get zapret2 provider and Tachyon-managed nfqws2 status\n");
print(" get_byedpi_status Get ByeDPI provider and Tachyon-managed ciadpi status\n"); print(" get_byedpi_status Get ByeDPI provider and Tachyon-managed ciadpi status\n");
print(" get_wdtt_status Get WDTT provider and Tachyon-managed wdtt status\n");
print(" get_olcrtc_status Get OlcRTC provider and Tachyon-managed olcrtc status\n");
print(" get_system_info Get system information in JSON format\n"); print(" get_system_info Get system information in JSON format\n");
print(" get_server_capabilities Get server UI capabilities in JSON format\n"); print(" get_server_capabilities Get server UI capabilities in JSON format\n");
print(" get_ui_capabilities Get lightweight UI capabilities in JSON format\n"); print(" get_ui_capabilities Get lightweight UI capabilities in JSON format\n");
@@ -226,6 +228,8 @@ function command_spec(command) {
get_zapret_status: [ "diagnostics/runtime.uc", "get-zapret-status", 0 ], get_zapret_status: [ "diagnostics/runtime.uc", "get-zapret-status", 0 ],
get_zapret2_status: [ "diagnostics/runtime.uc", "get-zapret2-status", 0 ], get_zapret2_status: [ "diagnostics/runtime.uc", "get-zapret2-status", 0 ],
get_byedpi_status: [ "diagnostics/runtime.uc", "get-byedpi-status", 0 ], get_byedpi_status: [ "diagnostics/runtime.uc", "get-byedpi-status", 0 ],
get_wdtt_status: [ "diagnostics/runtime.uc", "get-wdtt-status", 0 ],
get_olcrtc_status: [ "diagnostics/runtime.uc", "get-olcrtc-status", 0 ],
get_tailscale_status: [ "diagnostics/runtime.uc", "get-tailscale-status", 0 ], get_tailscale_status: [ "diagnostics/runtime.uc", "get-tailscale-status", 0 ],
get_tailscale_peers: [ "diagnostics/runtime.uc", "get-tailscale-peers", 0 ], get_tailscale_peers: [ "diagnostics/runtime.uc", "get-tailscale-peers", 0 ],
tailscale_restart: [ "providers/tailscale/runtime.uc", "start-runtime", 0 ], tailscale_restart: [ "providers/tailscale/runtime.uc", "start-runtime", 0 ],
+176
View File
@@ -1349,6 +1349,74 @@ function resolve_byedpi_release(arch, tag) {
return null; return null;
} }
function resolve_wdtt_release(arch, tag) {
let asset_ext = is_apk() ? "apk" : "ipk";
let release_series = trim(helper_output("openwrt-release-series", [ "/etc/openwrt_release" ]));
let releases_json = (tag != null && tag != "") ?
fetch_github_release_by_tag_json("ret_dev", "wdtt", tag) :
fetch_github_releases_json("ret_dev", "wdtt", "30");
if (releases_json != "") {
let resolved = trim(helper_output_input(releases_json, "wdtt-select-asset", [ release_series, asset_ext, arch.candidates ]));
let fields = split(resolved, "\t");
if (length(fields) >= 4) {
return {
arch: fields[0],
package_name: fields[1],
package_url: fields[2],
release_url: fields[3],
version: extract_arch_package_version(fields[1], fields[0])
};
}
}
if (tag != null && tag != "") {
let distrib_arch = read_openwrt_release_value("DISTRIB_ARCH");
let tag_clean = replace(tag, /^v/, "");
let pkg_name = "wdtt_" + tag_clean + "_openwrt_" + distrib_arch + "." + asset_ext;
return {
arch: distrib_arch,
package_name: pkg_name,
package_url: "https://github.com/ret_dev/wdtt/releases/download/" + tag + "/" + pkg_name,
release_url: "https://github.com/ret_dev/wdtt/releases/tag/" + tag,
version: tag
};
}
return null;
}
function resolve_olcrtc_release(arch, tag) {
let asset_ext = is_apk() ? "apk" : "ipk";
let release_series = trim(helper_output("openwrt-release-series", [ "/etc/openwrt_release" ]));
let releases_json = (tag != null && tag != "") ?
fetch_github_release_by_tag_json("ret_dev", "olcrtc", tag) :
fetch_github_releases_json("ret_dev", "olcrtc", "30");
if (releases_json != "") {
let resolved = trim(helper_output_input(releases_json, "olcrtc-select-asset", [ release_series, asset_ext, arch.candidates ]));
let fields = split(resolved, "\t");
if (length(fields) >= 4) {
return {
arch: fields[0],
package_name: fields[1],
package_url: fields[2],
release_url: fields[3],
version: extract_arch_package_version(fields[1], fields[0])
};
}
}
if (tag != null && tag != "") {
let distrib_arch = read_openwrt_release_value("DISTRIB_ARCH");
let tag_clean = replace(tag, /^v/, "");
let pkg_name = "olcrtc_" + tag_clean + "_openwrt_" + distrib_arch + "." + asset_ext;
return {
arch: distrib_arch,
package_name: pkg_name,
package_url: "https://github.com/ret_dev/olcrtc/releases/download/" + tag + "/" + pkg_name,
release_url: "https://github.com/ret_dev/olcrtc/releases/tag/" + tag,
version: tag
};
}
return null;
}
function download_direct_package(release) { function download_direct_package(release) {
let package_file = tmp_dir + "/" + release.package_name; let package_file = tmp_dir + "/" + release.package_name;
if (!download_with_retry(release.package_url, package_file, release.package_name) || !file_nonempty(package_file)) if (!download_with_retry(release.package_url, package_file, release.package_name) || !file_nonempty(package_file))
@@ -1540,6 +1608,86 @@ function install_byedpi(action, target_tag) {
action_success("byedpi", action, "ByeDPI package has been installed", current_version, pkg.version, 1, "latest", release.release_url || ""); action_success("byedpi", action, "ByeDPI package has been installed", current_version, pkg.version, 1, "latest", release.release_url || "");
} }
function install_wdtt(action, target_tag) {
init_tmp_dir() || action_fail("wdtt", action, "Failed to create temporary directory");
let arch = resolve_arch_candidates();
if (arch == null)
action_fail("wdtt", action, "Failed to detect package architecture");
let release = null;
retry_resolve("Resolving WDTT package", function() {
release = resolve_wdtt_release(arch, target_tag);
return release != null;
});
if (release == null)
action_fail("wdtt", action, "Failed to resolve WDTT package for this router architecture");
let runtime_module = LIB_DIR + "/providers/wdtt/runtime.uc";
let installed = provider_installed(runtime_module);
let current_version = provider_package_version(runtime_module);
if (action == "check_update") {
if (!installed)
action_success("wdtt", action, "WDTT is not installed", current_version, release.version, 0, "", release.release_url || "");
check_success("wdtt", current_version, release.version, release.release_url || "");
}
let pkg = download_direct_package(release);
if (pkg == null)
action_fail("wdtt", action, "Failed to download WDTT package");
run_logged("Updating package lists before WDTT package installation", pkg_list_update_command());
if (!run_logged("Installing WDTT package " + pkg.name, pkg_install_files_command([ pkg.file ])))
action_fail("wdtt", action, "Failed to install WDTT package", current_version, pkg.version);
disable_standalone_service("wdtt");
restart_tachyon_after_successful_change();
clear_version_caches();
current_version = provider_package_version(runtime_module);
if (current_version == "")
current_version = pkg.version || "unknown";
action_success("wdtt", action, "WDTT package has been installed", current_version, pkg.version, 1, "latest", release.release_url || "");
}
function install_olcrtc(action, target_tag) {
init_tmp_dir() || action_fail("olcrtc", action, "Failed to create temporary directory");
let arch = resolve_arch_candidates();
if (arch == null)
action_fail("olcrtc", action, "Failed to detect package architecture");
let release = null;
retry_resolve("Resolving OlcRTC package", function() {
release = resolve_olcrtc_release(arch, target_tag);
return release != null;
});
if (release == null)
action_fail("olcrtc", action, "Failed to resolve OlcRTC package for this router architecture");
let runtime_module = LIB_DIR + "/providers/olcrtc/runtime.uc";
let installed = provider_installed(runtime_module);
let current_version = provider_package_version(runtime_module);
if (action == "check_update") {
if (!installed)
action_success("olcrtc", action, "OlcRTC is not installed", current_version, release.version, 0, "", release.release_url || "");
check_success("olcrtc", current_version, release.version, release.release_url || "");
}
let pkg = download_direct_package(release);
if (pkg == null)
action_fail("olcrtc", action, "Failed to download OlcRTC package");
run_logged("Updating package lists before OlcRTC package installation", pkg_list_update_command());
if (!run_logged("Installing OlcRTC package " + pkg.name, pkg_install_files_command([ pkg.file ])))
action_fail("olcrtc", action, "Failed to install OlcRTC package", current_version, pkg.version);
disable_standalone_service("olcrtc");
restart_tachyon_after_successful_change();
clear_version_caches();
current_version = provider_package_version(runtime_module);
if (current_version == "")
current_version = pkg.version || "unknown";
action_success("olcrtc", action, "OlcRTC package has been installed", current_version, pkg.version, 1, "latest", release.release_url || "");
}
function install_tailscale(action) { function install_tailscale(action) {
let component = "tailscale"; let component = "tailscale";
let label = "Tailscale"; let label = "Tailscale";
@@ -3115,6 +3263,26 @@ function rollback_component(component) {
restart_tachyon_after_successful_change(); restart_tachyon_after_successful_change();
action_success("byedpi", "rollback", "ByeDPI rolled back to " + backup_version, backup_version, "", 1); action_success("byedpi", "rollback", "ByeDPI rolled back to " + backup_version, backup_version, "", 1);
} }
else if (component == "wdtt") {
let backup_bin = bdir + "/wdtt";
if (!file_exists(backup_bin)) action_fail("wdtt", "rollback", "WDTT backup binary is missing");
remove_file("/usr/bin/wdtt");
command_success_from_args([ "cp", "-p", backup_bin, "/usr/bin/wdtt" ]);
command_success_from_args([ "chmod", "0755", "/usr/bin/wdtt" ]);
clear_version_caches();
restart_tachyon_after_successful_change();
action_success("wdtt", "rollback", "WDTT rolled back to " + backup_version, backup_version, "", 1);
}
else if (component == "olcrtc") {
let backup_bin = bdir + "/olcrtc";
if (!file_exists(backup_bin)) action_fail("olcrtc", "rollback", "OlcRTC backup binary is missing");
remove_file("/usr/bin/olcrtc");
command_success_from_args([ "cp", "-p", backup_bin, "/usr/bin/olcrtc" ]);
command_success_from_args([ "chmod", "0755", "/usr/bin/olcrtc" ]);
clear_version_caches();
restart_tachyon_after_successful_change();
action_success("olcrtc", "rollback", "OlcRTC rolled back to " + backup_version, backup_version, "", 1);
}
else if (component == "zapret") { else if (component == "zapret") {
let backup_bin = bdir + "/nfqws"; let backup_bin = bdir + "/nfqws";
if (!file_exists(backup_bin)) action_fail("zapret", "rollback", "Zapret backup binary is missing"); if (!file_exists(backup_bin)) action_fail("zapret", "rollback", "Zapret backup binary is missing");
@@ -3380,6 +3548,14 @@ function component_action(component, action, extra) {
install_byedpi(action); install_byedpi(action);
else if (component == "byedpi" && action == "remove") else if (component == "byedpi" && action == "remove")
remove_optional_component("byedpi", "byedpi", "ByeDPI", LIB_DIR + "/providers/byedpi/runtime.uc"); remove_optional_component("byedpi", "byedpi", "ByeDPI", LIB_DIR + "/providers/byedpi/runtime.uc");
else if (component == "wdtt" && (action == "check_update" || action == "install"))
install_wdtt(action);
else if (component == "wdtt" && action == "remove")
remove_optional_component("wdtt", "wdtt", "WDTT", LIB_DIR + "/providers/wdtt/runtime.uc");
else if (component == "olcrtc" && (action == "check_update" || action == "install"))
install_olcrtc(action);
else if (component == "olcrtc" && action == "remove")
remove_optional_component("olcrtc", "olcrtc", "OlcRTC", LIB_DIR + "/providers/olcrtc/runtime.uc");
else if (component == "tailscale" && (action == "check_update" || action == "install")) else if (component == "tailscale" && (action == "check_update" || action == "install"))
install_tailscale(action); install_tailscale(action);
else if (component == "tailscale" && action == "remove") else if (component == "tailscale" && action == "remove")
+77 -1
View File
@@ -414,7 +414,7 @@ function openwrt_release_series(path) {
function updates_arch_package_version(package_name, package_arch) { function updates_arch_package_version(package_name, package_arch) {
let version = str_remove_suffix(str_remove_suffix(package_name, ".ipk"), ".apk"); let version = str_remove_suffix(str_remove_suffix(package_name, ".ipk"), ".apk");
let prefixes = ["zapret2_", "zapret2-", "zapret_", "zapret-", "byedpi_", "byedpi-"]; let prefixes = ["zapret2_", "zapret2-", "zapret_", "zapret-", "byedpi_", "byedpi-", "wdtt_", "wdtt-", "olcrtc_", "olcrtc-"];
for (let prefix in prefixes) { for (let prefix in prefixes) {
if (str_startswith(version, prefix)) { if (str_startswith(version, prefix)) {
@@ -1037,6 +1037,78 @@ function byedpi_select_asset(series, asset_ext, arch_candidates) {
} }
} }
function wdtt_select_asset(series, asset_ext, arch_candidates) {
let releases = releases_array_or_wrapped(read_stdin_json());
for (let pass = 0; pass < 2; pass++) {
if (pass == 0 && as_string(series) == "")
continue;
for (let release in releases) {
if (type(release) != "object")
continue;
if (release.draft === true || (length(releases) > 1 && release.prerelease === true))
continue;
if (pass == 0 && length(releases) > 1) {
let tag = as_string(release.tag_name || "");
let name = as_string(release.name || "");
if (!str_contains(tag, series) && !str_contains(name, series))
continue;
}
for (let arch in split(as_string(arch_candidates), " ")) {
if (arch == "")
continue;
for (let asset in array_or_empty(release.assets)) {
if (type(asset) != "object")
continue;
let name = as_string(asset.name || "");
let url = as_string(asset.browser_download_url || "");
if (url != "" && release_asset_matches_arch(name, "wdtt", arch, asset_ext)) {
print(arch, "\t", name, "\t", url, "\t", as_string(release.html_url || ""), "\n");
return;
}
}
}
}
}
}
function olcrtc_select_asset(series, asset_ext, arch_candidates) {
let releases = releases_array_or_wrapped(read_stdin_json());
for (let pass = 0; pass < 2; pass++) {
if (pass == 0 && as_string(series) == "")
continue;
for (let release in releases) {
if (type(release) != "object")
continue;
if (release.draft === true || (length(releases) > 1 && release.prerelease === true))
continue;
if (pass == 0 && length(releases) > 1) {
let tag = as_string(release.tag_name || "");
let name = as_string(release.name || "");
if (!str_contains(tag, series) && !str_contains(name, series))
continue;
}
for (let arch in split(as_string(arch_candidates), " ")) {
if (arch == "")
continue;
for (let asset in array_or_empty(release.assets)) {
if (type(asset) != "object")
continue;
let name = as_string(asset.name || "");
let url = as_string(asset.browser_download_url || "");
if (url != "" && release_asset_matches_arch(name, "olcrtc", arch, asset_ext)) {
print(arch, "\t", name, "\t", url, "\t", as_string(release.html_url || ""), "\n");
return;
}
}
}
}
}
}
function sing_box_extended_release_tag() { function sing_box_extended_release_tag() {
for (let release in releases_array_or_wrapped(read_stdin_json())) { for (let release in releases_array_or_wrapped(read_stdin_json())) {
if (type(release) != "object") if (type(release) != "object")
@@ -1444,6 +1516,10 @@ else if (mode == "release-select-arch-suffix-asset")
release_select_arch_suffix_asset(ARGV[1], ARGV[2]); release_select_arch_suffix_asset(ARGV[1], ARGV[2]);
else if (mode == "byedpi-select-asset") else if (mode == "byedpi-select-asset")
byedpi_select_asset(ARGV[1], ARGV[2], ARGV[3]); byedpi_select_asset(ARGV[1], ARGV[2], ARGV[3]);
else if (mode == "wdtt-select-asset")
wdtt_select_asset(ARGV[1], ARGV[2], ARGV[3]);
else if (mode == "olcrtc-select-asset")
olcrtc_select_asset(ARGV[1], ARGV[2], ARGV[3]);
else if (mode == "sing-box-extended-release-tag") else if (mode == "sing-box-extended-release-tag")
sing_box_extended_release_tag(); sing_box_extended_release_tag();
else if (mode == "sing-box-lx-release-tag") else if (mode == "sing-box-lx-release-tag")
+6 -2
View File
@@ -631,7 +631,7 @@ function normalize_component_name(component) {
function valid_component_name(component) { function valid_component_name(component) {
component = normalize_component_name(component); component = normalize_component_name(component);
return component == "tachyon" || component == "sing_box" || component == "zapret" || return component == "tachyon" || component == "sing_box" || component == "zapret" ||
component == "zapret2" || component == "byedpi" || component == "tailscale"; component == "zapret2" || component == "byedpi" || component == "wdtt" || component == "olcrtc" || component == "tailscale";
} }
function settings_component_auto_update_enabled(settings) { function settings_component_auto_update_enabled(settings) {
@@ -1604,7 +1604,7 @@ function component_update_check_cache() {
let results = []; let results = [];
if (enabled) { if (enabled) {
for (let component in [ "tachyon", "sing_box", "zapret", "zapret2", "byedpi" ]) { for (let component in [ "tachyon", "sing_box", "zapret", "zapret2", "byedpi", "wdtt", "olcrtc" ]) {
let value = read_json_file(component_update_check_cache_path(component)); let value = read_json_file(component_update_check_cache_path(component));
if (component_update_check_result_cacheable(value)) if (component_update_check_result_cacheable(value))
push(results, value); push(results, value);
@@ -2030,6 +2030,10 @@ function automatic_component_check_names() {
push(result, "zapret2"); push(result, "zapret2");
if (module_success([ LIB_DIR + "/providers/byedpi/runtime.uc", "installed" ])) if (module_success([ LIB_DIR + "/providers/byedpi/runtime.uc", "installed" ]))
push(result, "byedpi"); push(result, "byedpi");
if (module_success([ LIB_DIR + "/providers/wdtt/runtime.uc", "installed" ]))
push(result, "wdtt");
if (module_success([ LIB_DIR + "/providers/olcrtc/runtime.uc", "installed" ]))
push(result, "olcrtc");
return result; return result;
} }
+56 -1
View File
@@ -306,6 +306,34 @@ function is_connections_action(action) {
return action == "connection" || is_legacy_connection_action(action); return action == "connection" || is_legacy_connection_action(action);
} }
function is_wdtt_action(action) {
return as_string(action) == "wdtt";
}
function is_olcrtc_action(action) {
return as_string(action) == "olcrtc";
}
function wdtt_sections() {
let result = [];
let sections = uci_core.section_objects(CONFIG_NAME, "section");
for (let section in sections) {
if (bool_option(section, "enabled", true) && option(section, "action", "") == "wdtt")
push(result, section);
}
return result;
}
function olcrtc_sections() {
let result = [];
let sections = uci_core.section_objects(CONFIG_NAME, "section");
for (let section in sections) {
if (bool_option(section, "enabled", true) && option(section, "action", "") == "olcrtc")
push(result, section);
}
return result;
}
function normalize_action(action) { function normalize_action(action) {
action = as_string(action); action = as_string(action);
return is_connections_action(action) ? "connection" : action; return is_connections_action(action) ? "connection" : action;
@@ -617,6 +645,26 @@ function dscp_value(section) {
return list_option_value_from_array(dscp_list(section)); return list_option_value_from_array(dscp_list(section));
} }
function routed_dns_enabled(section) {
return bool_option(section, "routed_dns_enabled", false);
}
function routed_dns_type(section) {
let t = option(section, "routed_dns_type", "udp");
if (!contains(["udp", "dot", "doh", "doq"], t))
t = "udp";
return t;
}
function routed_dns_servers(section) {
let servers = list_option(section, "routed_dns_server");
if (length(servers) == 0) {
let single = option(section, "routed_dns_server", "");
servers = [single];
}
return servers;
}
function contains(arr, val) { function contains(arr, val) {
if (type(arr) != "array") return false; if (type(arr) != "array") return false;
for (let item in arr) { for (let item in arr) {
@@ -1552,6 +1600,13 @@ return {
subscription_download_targets, subscription_download_targets,
subscription_download_target_port, subscription_download_target_port,
has_dns_matchers, has_dns_matchers,
routed_dns_enabled,
routed_dns_type,
routed_dns_servers,
item_index_from_cursor, item_index_from_cursor,
cli_delete_section cli_delete_section,
wdtt_sections,
olcrtc_sections,
is_wdtt_action,
is_olcrtc_action
}; };
+99 -2
View File
@@ -8,6 +8,8 @@ let subscription_parser_module = null;
let zapret_validator_module = null; let zapret_validator_module = null;
let zapret2_validator_module = null; let zapret2_validator_module = null;
let byedpi_validator_module = null; let byedpi_validator_module = null;
let wdtt_validator_module = null;
let olcrtc_validator_module = null;
let constants_module = null; let constants_module = null;
let core_url = require("core.url"); let core_url = require("core.url");
let core_ip = require("core.ip"); let core_ip = require("core.ip");
@@ -695,6 +697,18 @@ function byedpi_validator() {
return byedpi_validator_module; return byedpi_validator_module;
} }
function wdtt_validator() {
if (wdtt_validator_module == null)
wdtt_validator_module = require("providers.wdtt.validator");
return wdtt_validator_module;
}
function olcrtc_validator() {
if (olcrtc_validator_module == null)
olcrtc_validator_module = require("providers.olcrtc.validator");
return olcrtc_validator_module;
}
function runtime_constants() { function runtime_constants() {
if (constants_module == null) if (constants_module == null)
constants_module = require("core.constants"); constants_module = require("core.constants");
@@ -769,13 +783,14 @@ function rule_action(section) {
function rule_action_supported(action) { function rule_action_supported(action) {
return contains([ "connection", "proxy", "outbound", "vpn", "awg", "warp", return contains([ "connection", "proxy", "outbound", "vpn", "awg", "warp",
"anytls", "snell", "mieru", "sudoku", "masque", "openvpn", "anytls", "snell", "mieru", "sudoku", "masque", "openvpn",
"bypass", "block", "dns", "zapret", "zapret2", "byedpi", "hosts" ], as_string(action)); "bypass", "block", "dns", "zapret", "zapret2", "byedpi", "hosts",
"wdtt", "olcrtc" ], as_string(action));
} }
function server_routing_section_action_supported(action) { function server_routing_section_action_supported(action) {
return contains([ "connection", "proxy", "outbound", "vpn", "awg", "warp", return contains([ "connection", "proxy", "outbound", "vpn", "awg", "warp",
"anytls", "snell", "mieru", "sudoku", "masque", "openvpn", "anytls", "snell", "mieru", "sudoku", "masque", "openvpn",
"zapret", "zapret2", "byedpi" ], as_string(action)); "zapret", "zapret2", "byedpi", "wdtt", "olcrtc" ], as_string(action));
} }
function duration_to_seconds_value(value) { function duration_to_seconds_value(value) {
@@ -1367,6 +1382,53 @@ function validate_provider_strategy(kind, section, context) {
fail_validation("Invalid ByeDPI strategy for rule '" + name + "': " + result.message); fail_validation("Invalid ByeDPI strategy for rule '" + name + "': " + result.message);
} }
function validate_wdtt_section(section, context) {
let name = section_name(section);
let uri = option(section, "warp_flow_uri", "");
if (uri == "") {
fail_validation("WDTT rule '" + name + "' has empty URI. Aborted.");
return;
}
let result = wdtt_validator().parse_wdtt_uri(uri);
if (!result.valid)
fail_validation("Invalid WDTT URI for rule '" + name + "': " + result.message + ". Aborted.");
let mode = option(section, "warp_flow_mode", "");
if (mode != "" && !contains(["freedom", "captive", "camou"], mode))
fail_validation("Invalid WDTT mode '" + mode + "' in rule '" + name + "'. Aborted.");
let device_id = option(section, "warp_flow_device_id", "");
if (device_id != "" && !wdtt_validator().validate_device_id(device_id))
fail_validation("Invalid WDTT device ID '" + device_id + "' in rule '" + name + "'. Aborted.");
}
function validate_olcrtc_section(section, context) {
let name = section_name(section);
let provider = option(section, "olcrtc_provider", "");
if (provider == "") {
fail_validation("OlcRTC rule '" + name + "' has empty provider. Aborted.");
return;
}
if (!contains(["livekit", "wrt"], provider))
fail_validation("Invalid OlcRTC provider '" + provider + "' in rule '" + name + "'. Aborted.");
let transport = option(section, "olcrtc_transport", "whep");
if (!contains(["whep", "whip"], transport))
fail_validation("Invalid OlcRTC transport '" + transport + "' in rule '" + name + "'. Aborted.");
let room_id = option(section, "olcrtc_room_id", "");
if (room_id == "")
fail_validation("OlcRTC rule '" + name + "' has empty room ID. Aborted.");
let crypto_key = option(section, "olcrtc_crypto_key", "");
if (crypto_key == "")
fail_validation("OlcRTC rule '" + name + "' has empty crypto key. Aborted.");
}
function dns_action_has_domain_matchers(section) { function dns_action_has_domain_matchers(section) {
for (let key in [ "domain", "domain_suffix", "domain_keyword", "domain_regex" ]) for (let key in [ "domain", "domain_suffix", "domain_keyword", "domain_regex" ])
if (option(section, key, "") != "" || option(section, key + "_text", "") != "" || length(list_option(section, key)) > 0) if (option(section, key, "") != "" || option(section, key + "_text", "") != "" || length(list_option(section, key)) > 0)
@@ -1467,6 +1529,22 @@ function validate_rule(section, sections, context) {
validate_provider_strategy("byedpi", section, context); validate_provider_strategy("byedpi", section, context);
} }
if (action == "wdtt") {
if (!context.wdtt_installed) {
validate_common_rule_references(section, context);
return;
}
validate_wdtt_section(section, context);
}
if (action == "olcrtc") {
if (!context.olcrtc_installed) {
validate_common_rule_references(section, context);
return;
}
validate_olcrtc_section(section, context);
}
if (connections.is_connections_action(action)) { if (connections.is_connections_action(action)) {
validate_dashboard_filter(section); validate_dashboard_filter(section);
@@ -1536,6 +1614,23 @@ function validate_rule(section, sections, context) {
for (let value in list_option(section, "domain_suffix")) for (let value in list_option(section, "domain_suffix"))
validate_combined_domain_value(value, name); validate_combined_domain_value(value, name);
validate_combined_domain_text_value(option(section, "domain_suffix_text", ""), name); validate_combined_domain_text_value(option(section, "domain_suffix_text", ""), name);
if (connections.routed_dns_enabled(section)) {
if (action == "dns" || action == "hosts" || action == "bypass" || action == "block")
fail_validation("Rule '" + name + "' has routed DNS enabled but action '" + action + "' does not support routed DNS. Use a proxy action (connection, zapret, etc.). Aborted.");
let routed_type = connections.routed_dns_type(section);
if (!contains(["udp", "dot", "doh", "doq"], routed_type))
fail_validation("Rule '" + name + "' has invalid routed DNS type '" + routed_type + "'. Use udp, dot, doh, or doq. Aborted.");
let routed_servers = connections.routed_dns_servers(section);
if (length(routed_servers) == 0)
fail_validation("Rule '" + name + "' has routed DNS enabled but no DNS server specified. Aborted.");
for (let srv in routed_servers)
if (!dns_server_value_valid(srv))
fail_validation("Rule '" + name + "' has an invalid routed DNS server '" + srv + "'. Aborted.");
if (!connections.has_dns_matchers(section) && length(list_option(section, "fully_routed_ips")) == 0)
fail_validation("Rule '" + name + "' has routed DNS enabled but no domain conditions or forced devices. Routed DNS requires at least one domain matcher. Aborted.");
}
validate_common_rule_references(section, context); validate_common_rule_references(section, context);
} }
@@ -1992,6 +2087,8 @@ function context_from_runtime() {
byedpi_installed: file_executable(constant_value(constants, "BYEDPI_BIN")), byedpi_installed: file_executable(constant_value(constants, "BYEDPI_BIN")),
zapret_installed: file_executable(constant_value(constants, "ZAPRET_PROVIDER_NFQWS_BIN")), zapret_installed: file_executable(constant_value(constants, "ZAPRET_PROVIDER_NFQWS_BIN")),
zapret2_installed: file_executable(constant_value(constants, "ZAPRET2_PROVIDER_NFQWS2_BIN")), zapret2_installed: file_executable(constant_value(constants, "ZAPRET2_PROVIDER_NFQWS2_BIN")),
wdtt_installed: file_executable(constant_value(constants, "WDTT_BIN")),
olcrtc_installed: file_executable(constant_value(constants, "OLCRTC_BIN")),
zapret_provider_nfqws_bin: constant_value(constants, "ZAPRET_PROVIDER_NFQWS_BIN"), zapret_provider_nfqws_bin: constant_value(constants, "ZAPRET_PROVIDER_NFQWS_BIN"),
zapret2_provider_nfqws2_bin: constant_value(constants, "ZAPRET2_PROVIDER_NFQWS2_BIN"), zapret2_provider_nfqws2_bin: constant_value(constants, "ZAPRET2_PROVIDER_NFQWS2_BIN"),
zapret_route_mark_base: constant_value(constants, "ZAPRET_ROUTE_MARK_BASE"), zapret_route_mark_base: constant_value(constants, "ZAPRET_ROUTE_MARK_BASE"),
+19
View File
@@ -225,6 +225,25 @@ function constants_map() {
c.BYEDPI_OPEN_FILES_LIMIT = env("BYEDPI_OPEN_FILES_LIMIT", "4096"); c.BYEDPI_OPEN_FILES_LIMIT = env("BYEDPI_OPEN_FILES_LIMIT", "4096");
c.BYEDPI_DEFAULT_CMD_OPTS = env("BYEDPI_DEFAULT_CMD_OPTS", "-o 2 --auto=t,r,a,s -d 2"); c.BYEDPI_DEFAULT_CMD_OPTS = env("BYEDPI_DEFAULT_CMD_OPTS", "-o 2 --auto=t,r,a,s -d 2");
c.WDTT_BIN = env("WDTT_BIN", "/usr/bin/wdtt");
c.WDTT_SERVICE_INIT = env("WDTT_SERVICE_INIT", "/etc/init.d/wdtt");
c.WDTT_STATE_DIR = env("WDTT_STATE_DIR", "/var/run/tachyon/wdtt");
c.WDTT_PID_DIR = env("WDTT_PID_DIR", c.WDTT_STATE_DIR + "/pid");
c.WDTT_LOG_DIR = env("WDTT_LOG_DIR", c.WDTT_STATE_DIR + "/log");
c.WDTT_LISTEN_ADDRESS = env("WDTT_LISTEN_ADDRESS", "127.0.0.1");
c.WDTT_PORT_BASE = env("WDTT_PORT_BASE", "1081");
c.WDTT_RESPAWN_DELAY = env("WDTT_RESPAWN_DELAY", "5");
c.WDTT_DEFAULT_MODE = env("WDTT_DEFAULT_MODE", "freedom");
c.OLCRTC_BIN = env("OLCRTC_BIN", "/usr/bin/olcrtc");
c.OLCRTC_SERVICE_INIT = env("OLCRTC_SERVICE_INIT", "/etc/init.d/olcrtc");
c.OLCRTC_STATE_DIR = env("OLCRTC_STATE_DIR", "/var/run/tachyon/olcrtc");
c.OLCRTC_PID_DIR = env("OLCRTC_PID_DIR", c.OLCRTC_STATE_DIR + "/pid");
c.OLCRTC_LOG_DIR = env("OLCRTC_LOG_DIR", c.OLCRTC_STATE_DIR + "/log");
c.OLCRTC_LISTEN_ADDRESS = env("OLCRTC_LISTEN_ADDRESS", "127.0.0.1");
c.OLCRTC_PORT_BASE = env("OLCRTC_PORT_BASE", "1082");
c.OLCRTC_RESPAWN_DELAY = env("OLCRTC_RESPAWN_DELAY", "5");
return c; return c;
} }
@@ -57,6 +57,8 @@ const SINGBOX_RUNTIME_UC = LIB_DIR + "/singbox/runtime.uc";
const ZAPRET_RUNTIME_UC = LIB_DIR + "/providers/zapret/runtime.uc"; const ZAPRET_RUNTIME_UC = LIB_DIR + "/providers/zapret/runtime.uc";
const ZAPRET2_RUNTIME_UC = LIB_DIR + "/providers/zapret2/runtime.uc"; const ZAPRET2_RUNTIME_UC = LIB_DIR + "/providers/zapret2/runtime.uc";
const BYEDPI_RUNTIME_UC = LIB_DIR + "/providers/byedpi/runtime.uc"; const BYEDPI_RUNTIME_UC = LIB_DIR + "/providers/byedpi/runtime.uc";
const WDTT_RUNTIME_UC = LIB_DIR + "/providers/wdtt/runtime.uc";
const OLCRTC_RUNTIME_UC = LIB_DIR + "/providers/olcrtc/runtime.uc";
const TAILSCALE_RUNTIME_UC = LIB_DIR + "/providers/tailscale/runtime.uc"; const TAILSCALE_RUNTIME_UC = LIB_DIR + "/providers/tailscale/runtime.uc";
const ZAPRET_VALIDATOR_UC = LIB_DIR + "/providers/zapret/validator.uc"; const ZAPRET_VALIDATOR_UC = LIB_DIR + "/providers/zapret/validator.uc";
const ZAPRET2_VALIDATOR_UC = LIB_DIR + "/providers/zapret2/validator.uc"; const ZAPRET2_VALIDATOR_UC = LIB_DIR + "/providers/zapret2/validator.uc";
@@ -2658,6 +2660,37 @@ function run_doctor_checks_impl(repair) {
} }
} }
// 4b2. Dnsmasq dns_redirect — when dhcp.@dnsmasq[0].dns_redirect='1',
// firewall4 creates DNAT rules that redirect external DNS queries to the
// router's :53. If Tachyon TProxy has already marked those packets, the
// DNAT rewrites destination to 192.168.1.1:53 and sing-box hijack-dns
// creates transparent sockets that collide with dnsmasq.
if (!agh_primary) {
let dns_redirect = uci_core.get("dhcp.@dnsmasq[0].dns_redirect");
if (dns_redirect != "1") {
doc_check("✅", "dnsmasq dns_redirect", "disabled (OK)", "");
} else {
issues++;
if (!DOCTOR_REPAIR_MODE) {
doc_plan("uci set dhcp.@dnsmasq[0].dns_redirect='0' + dnsmasq restart");
doc_check("⚠️", "dnsmasq dns_redirect", "enabled (causes DNAT → port 53 collision with sing-box)",
"→ WILL FIX (doctor --fix): отключение dns_redirect");
} else {
doc_set("dhcp.@dnsmasq[0].dns_redirect", "0");
doc_commit("dhcp");
command_status("/etc/init.d/dnsmasq restart >/dev/null 2>&1");
command_status("sleep 1");
let dr2 = uci_core.get("dhcp.@dnsmasq[0].dns_redirect");
if (dr2 != "1") {
doc_check("❌", "dnsmasq dns_redirect", "enabled", "→ FIXED: dns_redirect=0");
fixed++;
} else {
doc_check("❌", "dnsmasq dns_redirect", "enabled", "→ не удалось отключить dns_redirect");
}
}
}
}
// 4c. Resolv.conf symlink // 4c. Resolv.conf symlink
let resolv_link = ""; let resolv_link = "";
// Throws when /etc/resolv.conf is a regular file rather than a symlink, // Throws when /etc/resolv.conf is a regular file rather than a symlink,
@@ -3354,6 +3387,61 @@ function run_doctor_checks_impl(repair) {
} }
} }
// 20b. Port 53 Socket Collision — sing-box hijack-dns on tproxy-in creates
// transparent write-only sockets that collide with dnsmasq on :53 via
// SO_REUSEADDR. Diagnostic: find sing-box UNCONN sockets on :53 with
// non-zero Recv-Q (parasitic write-back sockets that steal UDP packets
// from dnsmasq).
{
let ss_output = command_capture("ss -u -a -e -n 'sport = :53' 2>/dev/null").output;
let parasitic_count = 0;
let collision_detail = "";
for (let line in split(ss_output, "\n")) {
if (index(line, "UNCONN") < 0)
continue;
if (index(line, "sing-box") < 0 && index(line, "sing_box") < 0)
continue;
let fields = split(trim(line), /[ \t]+/);
if (length(fields) < 5)
continue;
let recv_q = int(fields[1]);
if (recv_q > 0) {
parasitic_count++;
if (collision_detail == "")
collision_detail = fields[3] + " Recv-Q=" + as_string(recv_q);
}
}
if (parasitic_count == 0) {
doc_check("✅", "Port 53 socket collision", "none", "");
} else {
issues++;
if (!DOCTOR_REPAIR_MODE) {
doc_plan("restart sing-box to clear parasitic :53 sockets");
doc_check("⚠️", "Port 53 socket collision", as_string(parasitic_count) + " sing-box sockets on :53 (" + collision_detail + ")",
"→ sing-box hijack-dns создаёт прозрачные write-only сокеты, перехватывающие UDP у dnsmasq; WILL FIX: restart sing-box");
} else {
command_status("/etc/init.d/sing-box restart >/dev/null 2>&1");
command_status("sleep 2");
let ss_check = command_capture("ss -u -a -e -n 'sport = :53' 2>/dev/null").output;
let still_parasitic = 0;
for (let line2 in split(ss_check, "\n")) {
if (index(line2, "UNCONN") < 0) continue;
if (index(line2, "sing-box") < 0 && index(line2, "sing_box") < 0) continue;
let f2 = split(trim(line2), /[ \t]+/);
if (length(f2) >= 5 && int(f2[1]) > 0) still_parasitic++;
}
if (still_parasitic == 0) {
doc_check("❌", "Port 53 socket collision", as_string(parasitic_count) + " sing-box sockets on :53",
"→ FIXED: sing-box перезапущен, паразитные сокеты очищены");
fixed++;
} else {
doc_check("❌", "Port 53 socket collision", "persists after restart",
"→ не удалось устранить — проверьте конфигурацию hijack-dns inbound фильтра");
}
}
}
}
// 21. Subscription Cache Age — sing-box can route through days-old node // 21. Subscription Cache Age — sing-box can route through days-old node
// lists while the subscription URL itself is reachable. // lists while the subscription URL itself is reachable.
if (has_sections && cfg.subscription_url) { if (has_sections && cfg.subscription_url) {
@@ -5020,6 +5108,10 @@ else if (mode == "get-zapret2-status")
exit(module_passthrough(ZAPRET2_RUNTIME_UC, [ "status" ])); exit(module_passthrough(ZAPRET2_RUNTIME_UC, [ "status" ]));
else if (mode == "get-byedpi-status") else if (mode == "get-byedpi-status")
exit(module_passthrough(BYEDPI_RUNTIME_UC, [ "status" ])); exit(module_passthrough(BYEDPI_RUNTIME_UC, [ "status" ]));
else if (mode == "get-wdtt-status")
exit(module_passthrough(WDTT_RUNTIME_UC, [ "status" ]));
else if (mode == "get-olcrtc-status")
exit(module_passthrough(OLCRTC_RUNTIME_UC, [ "status" ]));
else if (mode == "get-tailscale-status") else if (mode == "get-tailscale-status")
exit(module_passthrough(TAILSCALE_RUNTIME_UC, [ "status" ])); exit(module_passthrough(TAILSCALE_RUNTIME_UC, [ "status" ]));
else if (mode == "get-tailscale-peers") else if (mode == "get-tailscale-peers")
+26 -4
View File
@@ -584,7 +584,8 @@ function section_action(section) {
function action_captures_traffic(action) { function action_captures_traffic(action) {
return action == "connection" || action == "proxy" || action == "outbound" || action == "vpn" || return action == "connection" || action == "proxy" || action == "outbound" || action == "vpn" ||
action == "awg" || action == "warp" || action == "block" || action == "zapret" || action == "zapret2" || action == "byedpi"; action == "awg" || action == "warp" || action == "block" || action == "zapret" || action == "zapret2" ||
action == "byedpi" || action == "wdtt" || action == "olcrtc";
} }
function section_priority_action(section) { function section_priority_action(section) {
@@ -698,9 +699,18 @@ function nft_excluded_source_match_args(section, family) {
return []; return [];
let ip_key = family == 6 ? "ip6" : "ip"; let ip_key = family == 6 ? "ip6" : "ip";
let addrs = []; let addrs = [];
for (let ip in excluded) for (let item in excluded) {
if (core_ip.ip_family(as_string(ip)) == family) let val = trim(as_string(item));
push(addrs, as_string(ip)); if (val == "") continue;
let is_mac = match(val, /^([0-9a-fA-F]{2}[:-]){5}[0-9a-fA-F]{2}$/) != null;
if (is_mac) {
for (let res_ip in core_ip.resolve_mac_to_ips(val))
if (core_ip.ip_family(res_ip) == family)
push(addrs, res_ip);
} else if (core_ip.ip_family(val) == family) {
push(addrs, val);
}
}
if (length(addrs) == 0) if (length(addrs) == 0)
return []; return [];
return [ ip_key, "saddr", "!=", "{ " + join(", ", addrs) + " }" ]; return [ ip_key, "saddr", "!=", "{ " + join(", ", addrs) + " }" ];
@@ -2369,6 +2379,9 @@ function nft_rule_signature_body(body, section) {
body = signature_add_value(body, "rule." + section_name + ".fully_routed_ips", option(section, "fully_routed_ips", "")); body = signature_add_value(body, "rule." + section_name + ".fully_routed_ips", option(section, "fully_routed_ips", ""));
body = signature_add_value(body, "rule." + section_name + ".excluded_ips", option(section, "excluded_ips", "")); body = signature_add_value(body, "rule." + section_name + ".excluded_ips", option(section, "excluded_ips", ""));
body = signature_add_value(body, "rule." + section_name + ".excluded_protocol", option(section, "excluded_protocol", "")); body = signature_add_value(body, "rule." + section_name + ".excluded_protocol", option(section, "excluded_protocol", ""));
body = signature_add_value(body, "rule." + section_name + ".routed_dns_enabled", bool_option(section, "routed_dns_enabled", false) ? "1" : "0");
body = signature_add_value(body, "rule." + section_name + ".routed_dns_type", option(section, "routed_dns_type", ""));
body = signature_add_value(body, "rule." + section_name + ".routed_dns_server", option(section, "routed_dns_server", ""));
body = signature_add_value(body, "rule." + section_name + ".protocol", option(section, "protocol", "")); body = signature_add_value(body, "rule." + section_name + ".protocol", option(section, "protocol", ""));
let comm_subnets = bool_option(section, "community_subnets", true) ? filter_community_subnet_lists_value(connections.community_lists_value(section)) : ""; let comm_subnets = bool_option(section, "community_subnets", true) ? filter_community_subnet_lists_value(connections.community_lists_value(section)) : "";
body = signature_add_value(body, "rule." + section_name + ".community_subnet_lists", comm_subnets); body = signature_add_value(body, "rule." + section_name + ".community_subnet_lists", comm_subnets);
@@ -2771,6 +2784,15 @@ function source_aware_dns_values(sections, deferred_sections) {
} }
} }
} }
if (connections.routed_dns_enabled(section)) {
for (let value in nft_csv_values(section_source_ip_values(section))) {
if (!seen[value]) {
seen[value] = true;
push(values, value);
}
}
}
} }
let settings = uci_settings(); let settings = uci_settings();
@@ -0,0 +1,32 @@
let constants = require("core.constants");
function config(ctx) {
let env_get = function(key, fallback) {
let val = getenv(key);
return val != null ? val : fallback;
};
return {
kind: "olcrtc",
action: "olcrtc",
config_path: env_get("TACHYON_OLCRTC_CONFIG", "/etc/olcrtc/client.yaml"),
service_init: env_get("TACHYON_OLCRTC_SERVICE_INIT", "/etc/init.d/olcrtc"),
binary: env_get("TACHYON_OLCRTC_BIN", "/usr/bin/olcrtc"),
default_socks_host: "127.0.0.1",
default_socks_port: "1080",
default_dns: "8.8.8.8:53",
default_provider: "jitsi",
default_transport: "datachannel",
package_name: "olcrtc",
runtime_path: ctx.lib_dir + "/providers/olcrtc/runtime.uc",
config_name: "olcrtc",
status_label: "olcrtc",
check_prefix: "olcrtc"
};
}
function validator() {
return require("providers.olcrtc.validator");
}
return { config, validator };
@@ -0,0 +1,425 @@
#!/usr/bin/env ucode
let fs = require("fs");
let common = require("core.common");
let uci_core = require("core.uci");
let connections = require("config.connections");
let as_string = common.as_string;
let bool_value = common.bool_value;
let object_or_empty = common.object_or_empty;
const MODE = as_string(getenv("TACHYON_CONFIG_NAME")) || "tachyon";
const LIB_DIR = as_string(getenv("TACHYON_LIB_DIR")) || "/usr/lib/tachyon";
const CONFIG_NAME = MODE;
let cfg = require("providers.olcrtc.common").config({ lib_dir: LIB_DIR });
let olcrtc_validator = require("providers.olcrtc.validator");
function log_message(msg, level) {
level = level || "info";
printf("%s\n", msg);
system("logger -t " + MODE + " -p " + (level == "fatal" ? "user.err" : level == "warn" ? "user.warning" : "user.info") + " '" + msg + "'");
}
function command_from_args(args) {
let parts = [];
for (let arg in args)
push(parts, common.shell_quote(as_string(arg)));
return join(" ", parts);
}
function command_output(cmd) {
let output = trim(popen(cmd, "r") || "");
return output;
}
function command_output_from_args(args) {
return command_output(command_from_args(args));
}
function command_success_from_args(args) {
return system(command_from_args(args)) == 0;
}
function option(section, key, fallback) {
let val = section[key];
if (val == null)
return fallback || "";
if (type(val) == "array")
return join(" ", val);
return as_string(val);
}
function bool_option(section, key, fallback) {
let val = section[key];
if (val == null)
return fallback || false;
return bool_value(val);
}
function list_option(section, key) {
let val = section[key];
if (val == null)
return [];
if (type(val) == "array")
return val;
return split(trim(as_string(val)), " \t\n\r");
}
function section_name(section) {
return as_string(section[".name"] || "");
}
function read_json_file(path) {
let data = fs.readfile(path);
if (data == null || data == "")
return null;
return json(data);
}
function yaml_quote(value) {
value = as_string(value);
if (value == "")
return "''";
if (match(value, /^[a-zA-Z0-9_./:-]+$/) != null)
return value;
return "'" + replace(value, "'", "''") + "'";
}
function enabled_sections() {
return connections.olcrtc_sections();
}
function enabled_rule_count() {
let sections = enabled_sections();
return length(sections);
}
function provider_available() {
return fs.stat(cfg.binary) != null;
}
function service_init_exists() {
return fs.stat(cfg.service_init) != null;
}
function service_running() {
return command_success_from_args([cfg.service_init, "status"]);
}
function service_enabled() {
return command_success_from_args([cfg.service_init, "enabled"]);
}
function package_version() {
let ver = trim(fs.readfile("/etc/olcrtc/.version") || "");
if (ver != "")
return ver;
return "";
}
function fetch_url(url) {
let tmp = "/tmp/tachyon-olcrtc-sub";
if (command_success_from_args(["curl", "-fsSL", "-o", tmp, "--connect-timeout", "20", "--max-time", "20", url]))
return trim(fs.readfile(tmp) || "");
if (command_success_from_args(["wget", "-q", "-O", tmp, "--timeout=20", url]))
return trim(fs.readfile(tmp) || "");
if (command_success_from_args(["uclient-fetch", "-q", "-O", tmp, "--timeout=20", url]))
return trim(fs.readfile(tmp) || "");
return "";
}
function first_uri_from_subscription(data) {
let lines = split(data, "\n\r");
for (let line in lines) {
line = trim(line);
if (substr(line, 0, 9) == "olcrtc://")
return line;
}
return "";
}
function resolve_connection(section) {
let links = list_option(section, "subscription_links");
let resolved = null;
let used_link = "";
let subscription_name = "";
let subscription_refresh = "";
for (let link in links) {
link = trim(as_string(link));
if (link == "")
continue;
if (substr(link, 0, 9) == "olcrtc://") {
let parsed = olcrtc_validator.parse_uri(link);
if (parsed.valid) {
resolved = parsed;
used_link = link;
break;
}
log_message("Invalid olcrtc:// URI: " + parsed.reason, "warn");
continue;
}
if (substr(link, 0, 8) == "https://" || substr(link, 0, 7) == "http://") {
let data = fetch_url(link);
if (data == "") {
log_message("Failed to fetch subscription: " + link, "warn");
continue;
}
let uri = first_uri_from_subscription(data);
if (uri == "") {
log_message("No olcrtc:// URI found in subscription: " + link, "warn");
continue;
}
let parsed = olcrtc_validator.parse_uri(uri);
if (parsed.valid) {
resolved = parsed;
used_link = link;
let lines = split(data, "\n\r");
for (let line in lines) {
line = trim(line);
if (substr(line, 0, 6) == "#name:")
subscription_name = trim(substr(line, 6));
else if (substr(line, 0, 8) == "#refresh:")
subscription_refresh = trim(substr(line, 8));
}
break;
}
log_message("Invalid olcrtc URI in subscription: " + parsed.reason, "warn");
}
}
if (resolved == null)
return { valid: false, reason: "No valid connection found in subscription links" };
let provider = option(section, "provider", "");
if (provider == "")
provider = resolved.provider;
let transport = option(section, "transport", "");
if (transport == "")
transport = resolved.transport;
let room_id = option(section, "room_id", "");
if (room_id == "")
room_id = resolved.room_id;
let crypto_key = option(section, "crypto_key", "");
if (crypto_key == "")
crypto_key = resolved.crypto_key;
return {
valid: true,
provider: provider,
transport: transport,
room_id: room_id,
crypto_key: crypto_key,
mimo: resolved.mimo,
payload: resolved.payload,
used_link: used_link,
subscription_name: subscription_name,
subscription_refresh: subscription_refresh
};
}
function payload_option_mapping() {
return {
"vp8-fps": "vp8_fps",
"vp8-batch": "vp8_batch",
"fps": "sei_fps",
"batch": "sei_batch",
"video-w": "video_w",
"video-h": "video_h",
"video-fps": "video_fps",
"video-codec": "video_codec",
"video-qr-size": "video_qr_size",
"video-qr-recovery": "video_qr_recovery",
"video-tile-module": "video_tile_module",
"video-tile-rs": "video_tile_rs"
};
}
function payload_uci_commands(payload) {
let cmds = [];
if (payload == "" || payload == null)
return cmds;
let mapping = payload_option_mapping();
let pairs = split(payload, "&");
for (let pair in pairs) {
let kv = split(pair, "=", 2);
if (length(kv) == 2) {
let key = kv[0];
let val = kv[1];
if (mapping[key] != null)
push(cmds, "uci set olcrtc.config." + mapping[key] + "=" + yaml_quote(val));
}
}
return cmds;
}
function write_olcrtc_config(section, connection) {
let uci_cmds = [
"uci set olcrtc.config.carrier=" + yaml_quote(connection.provider),
"uci set olcrtc.config.transport=" + yaml_quote(connection.transport),
"uci set olcrtc.config.room_id=" + yaml_quote(connection.room_id),
"uci set olcrtc.config.key=" + yaml_quote(connection.crypto_key),
"uci set olcrtc.config.socks_host=" + yaml_quote(option(section, "socks_host", cfg.default_socks_host)),
"uci set olcrtc.config.socks_port=" + yaml_quote(option(section, "socks_port", cfg.default_socks_port)),
"uci set olcrtc.config.dns=" + yaml_quote(option(section, "dns_server", cfg.default_dns))
];
let socks_user = option(section, "socks_user", "");
if (socks_user != "")
push(uci_cmds, "uci set olcrtc.config.socks_user=" + yaml_quote(socks_user));
let socks_pass = option(section, "socks_pass", "");
if (socks_pass != "")
push(uci_cmds, "uci set olcrtc.config.socks_pass=" + yaml_quote(socks_pass));
let payload_cmds = payload_uci_commands(connection.payload);
for (let cmd in payload_cmds)
push(uci_cmds, cmd);
for (let cmd in uci_cmds)
system(cmd);
system("uci commit olcrtc");
return true;
}
function write_olcrtc_yaml_config(section, connection) {
let yaml = "mode: cnc\n";
yaml += "auth:\n";
yaml += " provider: " + yaml_quote(connection.provider) + "\n";
yaml += "room:\n";
yaml += " id: " + yaml_quote(connection.room_id) + "\n";
yaml += "crypto:\n";
yaml += " key: " + yaml_quote(connection.crypto_key) + "\n";
yaml += "net:\n";
yaml += " transport: " + yaml_quote(connection.transport) + "\n";
yaml += " dns: " + yaml_quote(option(section, "dns_server", cfg.default_dns)) + "\n";
yaml += "socks:\n";
yaml += " host: " + yaml_quote(option(section, "socks_host", cfg.default_socks_host)) + "\n";
yaml += " port: " + yaml_quote(option(section, "socks_port", cfg.default_socks_port)) + "\n";
let socks_user = option(section, "socks_user", "");
if (socks_user != "") {
yaml += " username: " + yaml_quote(socks_user) + "\n";
yaml += " password: " + yaml_quote(option(section, "socks_pass", "")) + "\n";
}
let tmp = cfg.config_path + ".tmp";
let f = fs.open(tmp, "w");
if (!f)
return false;
f.write(yaml);
f.close();
return rename(tmp, cfg.config_path) == 0;
}
function start_runtime() {
let sections = enabled_sections();
if (length(sections) == 0) {
log_message("No enabled OlcRTC sections found, stopping service");
command_success_from_args([cfg.service_init, "stop"]);
return true;
}
if (!provider_available()) {
log_message("OlcRTC provider not available (binary not found: " + cfg.binary + ")", "fatal");
return false;
}
if (!service_init_exists()) {
log_message("OlcRTC service init script not found: " + cfg.service_init, "fatal");
return false;
}
if (length(sections) > 1)
log_message("Multiple OlcRTC sections found, using first: " + section_name(sections[0]), "warn");
let section = sections[0];
let connection = resolve_connection(section);
if (!connection.valid) {
log_message("OlcRTC rule '" + section_name(section) + "': " + connection.reason, "fatal");
return false;
}
let config_type = fs.stat("/etc/olcrtc/client.yaml") != null ? "yaml" : "uci";
if (config_type == "yaml") {
if (!write_olcrtc_yaml_config(section, connection)) {
log_message("Failed to write OlcRTC YAML config", "fatal");
return false;
}
}
else {
if (!write_olcrtc_config(section, connection)) {
log_message("Failed to write OlcRTC UCI config", "fatal");
return false;
}
}
if (!command_success_from_args([cfg.service_init, "restart"])) {
log_message("Failed to restart OlcRTC service", "fatal");
return false;
}
log_message("OlcRTC provider started successfully (provider=" + connection.provider + ", transport=" + connection.transport + ")");
return true;
}
function stop_runtime() {
command_success_from_args([cfg.service_init, "stop"]);
log_message("OlcRTC provider stopped");
return true;
}
function status_json() {
let installed = provider_available();
let running = installed ? service_running() : false;
let enabled = installed ? service_enabled() : false;
let rule_count = enabled_rule_count();
let version = package_version();
write_json({
installed: installed,
configured: rule_count > 0,
enabled_rule_count: rule_count,
service_enabled: enabled,
service_running: running,
config_path: cfg.config_path,
binary: cfg.binary,
service_init: cfg.service_init,
ready: installed && running && rule_count > 0,
status_message: running ? "olcrtc provider status is normal" : (installed ? "olcrtc service is not running" : "olcrtc is not installed")
});
return true;
}
function check_json() {
write_json({
olcrtc_installed: provider_available(),
olcrtc_config_path: cfg.config_path
});
return true;
}
let mode = as_string(ARGV[0]);
if (mode == "start-runtime")
start_runtime();
else if (mode == "stop-runtime")
stop_runtime();
else if (mode == "status")
status_json();
else if (mode == "check")
check_json();
else if (mode == "installed" || mode == "provider-available")
exit(provider_available() ? 0 : 1);
else if (mode == "package-version")
printf("%s\n", package_version());
else if (mode == "enabled-rule-count")
printf("%d\n", enabled_rule_count());
else {
warn("Usage: olcrtc/runtime.uc <operation>\n");
exit(1);
}
@@ -0,0 +1,203 @@
let common = require("core.common");
let fs = require("fs");
let as_string = common.as_string;
const OLCRTC_PROVIDERS = [ "jitsi", "telemost", "wbstream" ];
const OLCRTC_TRANSPORTS = [ "datachannel", "vp8channel", "seichannel", "videochannel" ];
function yaml_quote(value) {
value = as_string(value);
if (value == "")
return "''";
if (match(value, /^[a-zA-Z0-9_./:-]+$/) != null)
return value;
return "'" + replace(value, "'", "''") + "'";
}
function parse_uri(value) {
value = trim(as_string(value));
if (substr(value, 0, 9) != "olcrtc://")
return { valid: false, reason: "URI must start with olcrtc://" };
let rest = substr(value, 9);
let dollar_idx = rindex(rest, "$");
let mimo = "";
if (dollar_idx >= 0) {
mimo = substr(rest, dollar_idx + 1);
rest = substr(rest, 0, dollar_idx);
}
let hash_idx = rindex(rest, "#");
let crypto_key = "";
if (hash_idx >= 0) {
crypto_key = substr(rest, hash_idx + 1);
rest = substr(rest, 0, hash_idx);
}
let at_idx = index(rest, "@");
let room_id = "";
let provider_transport = rest;
if (at_idx >= 0) {
provider_transport = substr(rest, 0, at_idx);
room_id = substr(rest, at_idx + 1);
}
let q_idx = index(provider_transport, "?");
let provider = "";
let transport_with_payload = provider_transport;
if (q_idx >= 0) {
provider = substr(provider_transport, 0, q_idx);
transport_with_payload = substr(provider_transport, q_idx + 1);
}
let payload = "";
let transport = transport_with_payload;
let lt_idx = index(transport_with_payload, "<");
let gt_idx = index(transport_with_payload, ">");
if (lt_idx >= 0 && gt_idx > lt_idx) {
transport = substr(transport_with_payload, 0, lt_idx);
payload = substr(transport_with_payload, lt_idx + 1, gt_idx - lt_idx - 1);
}
if (!array_contains(OLCRTC_PROVIDERS, provider))
return { valid: false, reason: "provider must be one of: " + join(", ", OLCRTC_PROVIDERS) };
if (!array_contains(OLCRTC_TRANSPORTS, transport))
return { valid: false, reason: "transport must be one of: " + join(", ", OLCRTC_TRANSPORTS) };
if (room_id == "")
return { valid: false, reason: "room_id is required" };
if (crypto_key != "" && length(crypto_key) != 64)
return { valid: false, reason: "crypto_key must be 64 hex characters" };
return {
valid: true,
provider: provider,
transport: transport,
room_id: room_id,
crypto_key: crypto_key,
mimo: mimo,
payload: payload
};
}
function valid_hex_key(value) {
value = trim(as_string(value));
if (value == "" || value == null)
return true;
return length(value) == 64 && match(value, /^[0-9a-fA-F]+$/) != null;
}
function valid_number(value, min, max) {
value = as_string(value);
if (value == "" || value == null)
return true;
let num = int(value);
if (num == null || isNaN(num))
return false;
return num >= min && num <= max;
}
function valid_url(value) {
value = trim(as_string(value));
if (value == "" || value == null)
return true;
return match(value, /^https?:\/\/[^ \t\r\n]+$/) != null;
}
function valid_host_port(value) {
value = trim(as_string(value));
if (value == "" || value == null)
return true;
return match(value, /^[^ \t\r\n:]+:[0-9]{1,5}$/) != null;
}
function list_values(value) {
if (type(value) == "array")
return value;
value = trim(as_string(value));
if (value == "" || value == null)
return [];
return split(value, " \t\n\r");
}
function validation_result(valid, message, options) {
let result = { valid: valid, message: message || "" };
if (options != null)
for (let key in options)
result[key] = options[key];
return result;
}
function validate_section(section) {
let errors = [];
let provider = as_string(section.provider || "");
if (provider != "" && !array_contains(OLCRTC_PROVIDERS, provider))
errors.push("provider must be one of: " + join(", ", OLCRTC_PROVIDERS));
let transport = as_string(section.transport || "");
if (transport != "" && !array_contains(OLCRTC_TRANSPORTS, transport))
errors.push("transport must be one of: " + join(", ", OLCRTC_TRANSPORTS));
let crypto_key = as_string(section.crypto_key || "");
if (crypto_key != "" && !valid_hex_key(crypto_key))
errors.push("crypto_key must be 64 hex characters");
let socks_port = as_string(section.socks_port || "");
if (socks_port != "" && !valid_number(socks_port, 1, 65535))
errors.push("socks_port must be 1-65535");
let dns_server = as_string(section.dns_server || "");
if (dns_server != "" && !valid_host_port(dns_server))
errors.push("dns_server must be in HOST:PORT format");
let subscription_links = list_values(section.subscription_links);
for (let link in subscription_links) {
link = trim(as_string(link));
if (link == "")
continue;
if (substr(link, 0, 9) == "olcrtc://") {
let parsed = parse_uri(link);
if (!parsed.valid)
errors.push("invalid olcrtc:// URI: " + parsed.reason);
}
else if (!valid_url(link))
errors.push("subscription link must be olcrtc:// URI or HTTP URL: " + link);
}
return errors;
}
function validate_section_json() {
let input = read_stdin();
let data = object_or_empty(json(input));
let errors = validate_section(data);
if (length(errors) > 0)
write_json({ valid: false, message: join("; ", errors) });
else
write_json({ valid: true, message: "" });
return length(errors) == 0;
}
function read_stdin() {
let input = fs.open("/dev/stdin", "r");
if (!input)
return "";
let data = input.read("all");
input.close();
return data == null ? "" : data;
}
function write_json(value) {
printf("%s\n", sprintf("%J", value));
}
return {
validate_section,
validate_section_json,
parse_uri,
valid_hex_key,
providers: OLCRTC_PROVIDERS,
transports: OLCRTC_TRANSPORTS
};
@@ -0,0 +1,41 @@
let constants = require("core.constants");
function config(ctx) {
let env_get = function(key, fallback) {
let val = getenv(key);
return val != null ? val : fallback;
};
return {
kind: "wdtt",
action: "wdtt",
config_path: env_get("TACHYON_WDTT_CONFIG", "/etc/config/wdtt"),
qwdtt_config_path: env_get("TACHYON_QWDTT_CONFIG", "/etc/qwdtt/config.json"),
service_init: env_get("TACHYON_WDTT_SERVICE_INIT", "/etc/init.d/wdtt-client"),
qwdtt_service_init: env_get("TACHYON_QWDTT_SERVICE_INIT", "/etc/init.d/qwdtt"),
binary: env_get("TACHYON_QWDTT_BIN", "/usr/bin/qwdtt-client"),
genlists_bin: env_get("TACHYON_WDTT_GENLISTS_BIN", "/usr/sbin/wdtt-genlists"),
resolve_bin: env_get("TACHYON_WDTT_RESOLVE_BIN", "/usr/sbin/wdtt-resolve"),
hashes_dir: env_get("TACHYON_WDTT_HASHES_DIR", "/etc/wdtt"),
captcha_token_default: "/var/run/qwdtt/captcha.token",
default_peer: "YOUR_SERVER:56000",
default_workers_qwdtt: "9",
default_workers_wdtt: "36",
default_max_hashes: "4",
default_mode: "selective",
default_mtu: "1280",
default_refresh: "15m",
default_socks_port_base: 1080,
package_name: "wdtt",
runtime_path: ctx.lib_dir + "/providers/wdtt/runtime.uc",
config_name: "tachyon",
status_label: "wdtt",
check_prefix: "wdtt"
};
}
function validator() {
return require("providers.wdtt.validator");
}
return { config, validator };
@@ -0,0 +1,607 @@
#!/usr/bin/env ucode
let fs = require("fs");
let common = require("core.common");
let uci_core = require("core.uci");
let connections = require("config.connections");
let as_string = common.as_string;
let bool_value = common.bool_value;
let object_or_empty = common.object_or_empty;
const MODE = as_string(getenv("TACHYON_CONFIG_NAME")) || "tachyon";
const LIB_DIR = as_string(getenv("TACHYON_LIB_DIR")) || "/usr/lib/tachyon";
const CONFIG_NAME = MODE;
let cfg = require("providers.wdtt.common").config({ lib_dir: LIB_DIR });
let wdtt_validator = require("providers.wdtt.validator");
function log_message(msg, level) {
level = level || "info";
printf("%s\n", msg);
system("logger -t " + MODE + " -p " + (level == "fatal" ? "user.err" : level == "warn" ? "user.warning" : "user.info") + " '" + msg + "'");
}
function command_from_args(args) {
let parts = [];
for (let arg in args)
push(parts, common.shell_quote(as_string(arg)));
return join(" ", parts);
}
function command_output(cmd) {
let output = trim(popen(cmd, "r") || "");
return output;
}
function command_output_from_args(args) {
return command_output(command_from_args(args));
}
function command_success_from_args(args) {
return system(command_from_args(args)) == 0;
}
function option(section, key, fallback) {
let val = section[key];
if (val == null)
return fallback || "";
if (type(val) == "array")
return join(" ", val);
return as_string(val);
}
function bool_option(section, key, fallback) {
let val = section[key];
if (val == null)
return fallback || false;
return bool_value(val);
}
function list_option(section, key) {
let val = section[key];
if (val == null)
return [];
if (type(val) == "array")
return val;
return split(trim(as_string(val)), " \t\n\r");
}
function section_name(section) {
return as_string(section[".name"] || "");
}
function read_json_file(path) {
let data = fs.readfile(path);
if (data == null || data == "")
return null;
return json(data);
}
function write_json_file(path, data) {
let tmp = path + ".tmp";
let f = fs.open(tmp, "w");
if (!f)
return false;
f.write(sprintf("%J", data));
f.close();
return rename(tmp, path) == 0;
}
function lan_mac_address() {
for (let iface in ["br-lan", "lan", "eth0", "eth1"]) {
let mac_path = "/sys/class/net/" + iface + "/address";
let mac = trim(fs.readfile(mac_path) || "");
if (mac != "" && mac != "00:00:00:00:00:00")
return mac;
}
return "";
}
function enabled_sections() {
return connections.wdtt_sections();
}
function enabled_rule_count() {
let sections = enabled_sections();
return length(sections);
}
function provider_available() {
return fs.stat(cfg.binary) != null || fs.stat(cfg.config_path) != null;
}
function qwdtt_installed() {
return fs.stat(cfg.binary) != null;
}
function active_service_init() {
if (qwdtt_installed())
return cfg.qwdtt_service_init;
return cfg.service_init;
}
function service_init_exists() {
return fs.stat(active_service_init()) != null;
}
function service_running() {
return command_success_from_args([active_service_init(), "status"]);
}
function service_enabled() {
return command_success_from_args([active_service_init(), "enabled"]);
}
function package_version() {
if (qwdtt_installed()) {
let ver = trim(fs.readfile("/etc/qwdtt/.version") || "");
if (ver != "")
return ver;
}
return "";
}
function first_http_url(values) {
for (let v in values) {
v = trim(as_string(v));
if (substr(v, 0, 7) == "http://")
return v;
if (substr(v, 0, 8) == "https://")
return v;
}
return "";
}
function first_local_file(values) {
for (let v in values) {
v = trim(as_string(v));
if (substr(v, 0, 1) == "/")
return v;
}
return "";
}
function first_wdtt_link(values) {
for (let v in values) {
v = trim(as_string(v));
if (substr(v, 0, 7) == "wdtt://")
return v;
}
return "";
}
function write_wdtt_hashes_file(hashes_value) {
let dir = cfg.hashes_dir;
system("mkdir -p " + common.shell_quote(dir));
let path = dir + "/vk-calls.txt";
let tmp = path + ".tmp";
let f = fs.open(tmp, "w");
if (!f)
return false;
if (type(hashes_value) == "array") {
for (let h in hashes_value)
f.write(trim(as_string(h)) + "\n");
}
else {
let hashes_str = trim(as_string(hashes_value));
if (hashes_str != "") {
let parts = split(hashes_str, " ,\n\r\t");
for (let h in parts) {
h = trim(h);
if (h != "")
f.write(h + "\n");
}
}
}
f.close();
rename(tmp, path);
return true;
}
function qwdtt_hashes_list(section) {
let hashes = [];
let links = list_option(section, "subscription_links");
let wdtt_link = first_wdtt_link(links);
if (wdtt_link != "") {
let parsed = wdtt_validator.parse_wdtt_uri(wdtt_link);
if (parsed.valid && parsed.hashes != "") {
let parts = split(parsed.hashes, ",");
for (let h in parts) {
h = trim(h);
if (h != "")
push(hashes, h);
}
}
}
if (length(hashes) == 0) {
let hashes_file = first_local_file(links);
if (hashes_file != "" && fs.stat(hashes_file) != null) {
let data = trim(fs.readfile(hashes_file) || "");
if (data != "") {
let lines = split(data, "\n\r");
for (let line in lines) {
line = trim(line);
if (line != "")
push(hashes, line);
}
}
}
}
return hashes;
}
function write_qwdtt_json_config(section, name) {
let links = list_option(section, "subscription_links");
let wdtt_link = first_wdtt_link(links);
let parsed = null;
if (wdtt_link != "")
parsed = wdtt_validator.parse_wdtt_uri(wdtt_link);
let peer = option(section, "peer", "");
if (peer == "" && parsed != null && parsed.valid)
peer = parsed.peer;
if (peer == "")
peer = cfg.default_peer;
let password = option(section, "password", "");
if (password == "" && parsed != null && parsed.valid)
password = parsed.pass;
let device_id = option(section, "device_id", "");
if (device_id == "")
device_id = lan_mac_address();
let workers = option(section, "workers", "");
if (workers == "" && parsed != null && parsed.valid && parsed.workers != "")
workers = parsed.workers;
if (workers == "")
workers = cfg.default_workers_qwdtt;
let hashes = qwdtt_hashes_list(section);
let hashes_file = first_local_file(links);
if (hashes_file != "" && length(hashes) == 0)
write_wdtt_hashes_file([]);
let qwdtt_mode = option(section, "qwdtt_mode", "rawtun");
let config = {
peer: peer,
password: password,
device_id: device_id,
workers: int(workers) || 9,
dns: option(section, "dns", "yandex"),
obfs: option(section, "obfs", "audio"),
captcha_mode: option(section, "captcha_mode", "auto"),
vk_auth: option(section, "vk_auth", "anonymous"),
vk_anon_path: option(section, "vk_anon_path", "vkcalls"),
no_dtls: bool_option(section, "no_dtls", false),
turn_tcp: bool_option(section, "turn_tcp", false),
tun_name: option(section, "tun_name", "qwdtt0"),
lan_interface: option(section, "lan_iface", "br-lan"),
mode: qwdtt_mode
};
if (password != "")
config.vk_creds_file = option(section, "vk_creds_file", "");
config.captcha_token_file = option(section, "captcha_token_file", cfg.captcha_token_default);
if (length(hashes) > 0)
config.hashes = hashes;
else if (hashes_file != "")
config.hashes_file = hashes_file;
if (qwdtt_mode == "socks") {
let socks_port = option(section, "socks_port", "");
if (socks_port == "" && parsed != null && parsed.valid && parsed.port != "")
socks_port = parsed.port;
if (socks_port == "")
socks_port = as_string(cfg.default_socks_port_base);
config.socks = option(section, "socks_addr", "127.0.0.1:" + socks_port);
}
let result = write_json_file(cfg.qwdtt_config_path, config);
if (result)
system("chmod 0600 " + common.shell_quote(cfg.qwdtt_config_path));
system("uci set " + MODE + ".wdtt_state.enabled=" + (result ? "1" : "0"));
system("uci commit " + MODE);
return result;
}
function write_wdtt_uci_config(section) {
let links = list_option(section, "subscription_links");
let wdtt_link = first_wdtt_link(links);
let parsed = null;
if (wdtt_link != "")
parsed = wdtt_validator.parse_wdtt_uri(wdtt_link);
let peer = option(section, "peer", "");
if (peer == "" && parsed != null && parsed.valid)
peer = parsed.peer;
if (peer == "")
peer = cfg.default_peer;
let password = option(section, "password", "");
if (password == "" && parsed != null && parsed.valid)
password = parsed.pass;
let device_id = option(section, "device_id", "");
if (device_id == "")
device_id = lan_mac_address();
let workers = option(section, "workers", "");
if (workers == "" && parsed != null && parsed.valid && parsed.workers != "")
workers = parsed.workers;
if (workers == "")
workers = cfg.default_workers_wdtt;
let hashes_url = first_http_url(links);
let hashes_file = first_local_file(links);
let uci_cmds = [
"uci set wdtt.settings.enabled='1'",
"uci set wdtt.settings.peer='" + peer + "'",
"uci set wdtt.settings.password='" + password + "'",
"uci set wdtt.settings.device_id='" + device_id + "'",
"uci set wdtt.settings.workers='" + workers + "'",
"uci set wdtt.settings.max_hashes='" + option(section, "max_hashes", cfg.default_max_hashes) + "'",
"uci set wdtt.settings.mode='" + option(section, "mode", cfg.default_mode) + "'",
"uci set wdtt.settings.mtu='" + option(section, "mtu", cfg.default_mtu) + "'",
"uci set wdtt.settings.refresh='" + option(section, "refresh", cfg.default_refresh) + "'",
"uci set wdtt.settings.auto_update='" + (bool_option(section, "auto_update", true) ? "1" : "0") + "'",
"uci set wdtt.settings.block_doh='" + (bool_option(section, "block_doh", false) ? "1" : "0") + "'",
"uci set wdtt.settings.block_ipv6='" + (bool_option(section, "block_ipv6", false) ? "1" : "0") + "'"
];
if (hashes_url != "")
push(uci_cmds, "uci set wdtt.settings.hashes_url='" + hashes_url + "'");
if (hashes_file != "")
push(uci_cmds, "uci set wdtt.settings.hashes_file='" + hashes_file + "'");
let port = "";
if (parsed != null && parsed.valid && parsed.port != "")
port = parsed.port;
if (port != "")
push(uci_cmds, "uci set wdtt.settings.listen='127.0.0.1:" + port + "'");
for (let cmd in uci_cmds)
system(cmd);
system("uci commit wdtt");
return true;
}
function udp_port_in_use(port) {
let hex_port = sprintf("%04X", port);
for (let proc_file in ["/proc/net/udp", "/proc/net/udp6"]) {
let data = fs.readfile(proc_file);
if (data == null)
continue;
let lines = split(data, "\n");
for (let i = 1; i < length(lines); i++) {
let parts = split(trim(lines[i]), " \t");
if (length(parts) >= 2) {
let local = parts[1];
let colon_idx = index(local, ":");
if (colon_idx >= 0) {
let port_hex = substr(local, colon_idx + 1);
if (port_hex == hex_port)
return true;
}
}
}
}
return false;
}
function restart_qwdtt_service() {
command_success_from_args([cfg.qwdtt_service_init, "stop"]);
let wait_ms = 0;
while (wait_ms < 5000) {
if (command_output_from_args(["pidof", "qwdtt-client"]) == "")
break;
system("sleep 0.1");
wait_ms += 100;
}
if (wait_ms >= 5000)
system("killall -9 qwdtt-client 2>/dev/null || true");
let port = cfg.default_socks_port_base;
wait_ms = 0;
while (wait_ms < 10000) {
if (!udp_port_in_use(port))
break;
system("sleep 0.5");
wait_ms += 500;
}
system("sleep 0.5");
return command_success_from_args([cfg.qwdtt_service_init, "start"]);
}
function start_runtime() {
let sections = enabled_sections();
if (length(sections) == 0) {
log_message("No enabled WDTT sections found, disabling provider");
if (qwdtt_installed()) {
system("uci set " + MODE + ".wdtt_state.enabled='0'");
system("uci commit " + MODE);
command_success_from_args([cfg.qwdtt_service_init, "stop"]);
}
else {
system("uci set wdtt.settings.enabled='0'");
system("uci commit wdtt");
command_success_from_args([cfg.service_init, "stop"]);
}
return true;
}
if (!provider_available()) {
log_message("WDTT provider not available (no binary or config found)", "fatal");
return false;
}
if (!service_init_exists()) {
log_message("WDTT service init script not found: " + active_service_init(), "fatal");
return false;
}
if (length(sections) > 1)
log_message("Multiple WDTT sections found, using first: " + section_name(sections[0]), "warn");
let section = sections[0];
if (qwdtt_installed()) {
if (!write_qwdtt_json_config(section, section_name(section))) {
log_message("Failed to write qwdtt config", "fatal");
return false;
}
if (!restart_qwdtt_service()) {
log_message("Failed to start qwdtt service", "fatal");
return false;
}
}
else {
if (!write_wdtt_uci_config(section)) {
log_message("Failed to write wdtt UCI config", "fatal");
return false;
}
if (!command_success_from_args([cfg.service_init, "restart"])) {
log_message("Failed to restart wdtt-client service", "fatal");
return false;
}
}
if (fs.stat(cfg.genlists_bin) != null)
command_output_from_args([cfg.genlists_bin]);
if (fs.stat(cfg.resolve_bin) != null)
command_output_from_args([cfg.resolve_bin]);
log_message("WDTT provider started successfully");
return true;
}
function stop_runtime() {
if (qwdtt_installed()) {
system("uci set " + MODE + ".wdtt_state.enabled='0'");
system("uci commit " + MODE);
command_success_from_args([cfg.qwdtt_service_init, "stop"]);
}
else {
system("uci set wdtt.settings.enabled='0'");
system("uci commit wdtt");
command_success_from_args([cfg.service_init, "stop"]);
}
log_message("WDTT provider stopped");
return true;
}
function status_json() {
let installed = provider_available();
let running = installed ? service_running() : false;
let enabled = installed ? service_enabled() : false;
let rule_count = enabled_rule_count();
let client = qwdtt_installed() ? "qwdtt" : "wdtt-openwrt";
let config_path = qwdtt_installed() ? cfg.qwdtt_config_path : cfg.config_path;
let version = package_version();
write_json({
installed: installed,
configured: rule_count > 0,
enabled_rule_count: rule_count,
service_enabled: enabled,
service_running: running,
config_path: config_path,
service_init: active_service_init(),
client: client,
package_version: version,
ready: installed && running && rule_count > 0,
status_message: running ? "wdtt provider status is normal" : (installed ? "wdtt service is not running" : "wdtt is not installed")
});
return true;
}
function check_json() {
write_json({
wdtt_installed: provider_available(),
wdtt_config_path: cfg.config_path,
qwdtt_installed: qwdtt_installed(),
qwdtt_version: qwdtt_installed() ? package_version() : ""
});
return true;
}
function captcha_token_file() {
if (qwdtt_installed()) {
let config = read_json_file(cfg.qwdtt_config_path);
if (config != null && config.captcha_token_file != null)
return config.captcha_token_file;
}
return cfg.captcha_token_default;
}
function wdtt_captcha_status() {
let running = service_running();
let token_file = captcha_token_file();
let has_token = fs.stat(token_file) != null;
let token = "";
if (has_token)
token = trim(fs.readfile(token_file) || "");
write_json({
running: running,
token_file: token_file,
has_token: has_token,
token: token
});
return true;
}
function wdtt_captcha_submit(token) {
let token_file = captcha_token_file();
let dir = "";
let last_slash = rindex(token_file, "/");
if (last_slash >= 0)
dir = substr(token_file, 0, last_slash);
if (dir != "")
system("mkdir -p " + common.shell_quote(dir));
let f = fs.open(token_file, "w");
if (!f) {
write_json({ success: false, error: "Failed to write token file" });
return false;
}
f.write(trim(as_string(token)));
f.close();
system("chmod 0600 " + common.shell_quote(token_file));
write_json({ success: true, path: token_file });
return true;
}
let mode = as_string(ARGV[0]);
if (mode == "start-runtime")
start_runtime();
else if (mode == "stop-runtime")
stop_runtime();
else if (mode == "status")
status_json();
else if (mode == "check")
check_json();
else if (mode == "installed" || mode == "provider-available")
exit(provider_available() ? 0 : 1);
else if (mode == "package-version")
printf("%s\n", package_version());
else if (mode == "enabled-rule-count")
printf("%d\n", enabled_rule_count());
else if (mode == "wdtt-captcha-status")
wdtt_captcha_status();
else if (mode == "wdtt-captcha-submit")
wdtt_captcha_submit(ARGV[1]);
else {
warn("Usage: wdtt/runtime.uc <operation>\n");
exit(1);
}
@@ -0,0 +1,239 @@
let common = require("core.common");
let fs = require("fs");
let as_string = common.as_string;
const WDTT_MODES = [ "selective", "lan-all", "full" ];
const QWDTT_MODES = [ "rawtun", "socks", "vpn" ];
const WDTT_COMMUNITY_LISTS = [
"russia-inside", "russia-outside", "ukraine", "telegram", "meta",
"youtube", "discord", "tiktok", "twitter", "hdrezka", "roblox",
"cloudflare", "cloudfront", "google_ai", "google_meet", "google_play",
"hetzner", "ovh", "digitalocean", "anime", "news", "geoblock",
"block", "porn", "hodca"
];
function url_decode(value) {
let result = "";
let len = length(value);
let i = 0;
while (i < len) {
if (value[i] == '%' && i + 2 < len) {
let hex = substr(value, i + 1, 2);
let code = parseInt(hex, 16);
if (code != null && !isNaN(code))
result += chr(code);
else
result += '%';
i += 3;
}
else if (value[i] == '+') {
result += ' ';
i += 1;
}
else {
result += value[i];
i += 1;
}
}
return result;
}
function parse_query_string(query) {
let params = {};
if (query == "" || query == null)
return params;
let pairs = split(query, "&");
for (let pair in pairs) {
let kv = split(pair, "=", 2);
if (length(kv) == 2)
params[kv[0]] = url_decode(kv[1]);
else if (length(kv) == 1)
params[kv[0]] = "";
}
return params;
}
function parse_wdtt_uri(value) {
value = trim(as_string(value));
if (substr(value, 0, 7) != "wdtt://")
return { valid: false, reason: "URI must start with wdtt://" };
let rest = substr(value, 7);
let hash_idx = index(rest, "#");
let label = "";
if (hash_idx >= 0) {
label = substr(rest, hash_idx + 1);
rest = substr(rest, 0, hash_idx);
}
let params = parse_query_string(rest);
let peer = params.peer || "";
let hashes = params.hashes || "";
let pass = params.pass || "";
let workers = params.workers || "";
let port = params.port || "";
if (peer == "")
return { valid: false, reason: "wdtt:// URI must contain peer parameter" };
return {
valid: true,
peer: peer,
hashes: hashes,
pass: pass,
workers: workers,
port: port,
label: label
};
}
function valid_peer(value) {
value = trim(as_string(value));
if (value == "" || value == null)
return true;
return match(value, /^[^ \t\r\n:]+:[0-9]{1,5}$/) != null;
}
function valid_url(value) {
value = trim(as_string(value));
if (value == "" || value == null)
return true;
return match(value, /^https?:\/\/[^ \t\r\n]+$/) != null;
}
function valid_number(value, min, max) {
value = as_string(value);
if (value == "" || value == null)
return true;
let num = int(value);
if (num == null || isNaN(num))
return false;
return num >= min && num <= max;
}
function valid_mode(value) {
if (value == "" || value == null)
return true;
return array_contains(WDTT_MODES, value);
}
function valid_qwdtt_mode(value) {
if (value == "" || value == null)
return true;
return array_contains(QWDTT_MODES, value);
}
function valid_community_list(value) {
return array_contains(WDTT_COMMUNITY_LISTS, value);
}
function list_values(value) {
if (type(value) == "array")
return value;
value = trim(as_string(value));
if (value == "" || value == null)
return [];
return split(value, " \t\n\r");
}
function validation_result(valid, message, options) {
let result = { valid: valid, message: message || "" };
if (options != null)
for (let key in options)
result[key] = options[key];
return result;
}
function validate_section(section) {
let errors = [];
let peer = as_string(section.peer || "");
if (peer != "" && !valid_peer(peer))
errors.push("peer must be in HOST:PORT format");
let mode = as_string(section.mode || "");
if (!valid_mode(mode))
errors.push("mode must be one of: " + join(", ", WDTT_MODES));
let qwdtt_mode = as_string(section.qwdtt_mode || "");
if (!valid_qwdtt_mode(qwdtt_mode))
errors.push("qwdtt_mode must be one of: " + join(", ", QWDTT_MODES));
let workers = as_string(section.workers || "");
if (workers != "" && !valid_number(workers, 1, 1024))
errors.push("workers must be 1-1024");
let max_hashes = as_string(section.max_hashes || "");
if (max_hashes != "" && !valid_number(max_hashes, 1, 64))
errors.push("max_hashes must be 1-64");
let mtu = as_string(section.mtu || "");
if (mtu != "" && !valid_number(mtu, 576, 1500))
errors.push("mtu must be 576-1500");
let subscription_links = list_values(section.subscription_links);
for (let link in subscription_links) {
link = trim(as_string(link));
if (link == "")
continue;
if (substr(link, 0, 7) == "wdtt://") {
let parsed = parse_wdtt_uri(link);
if (!parsed.valid)
errors.push("invalid wdtt:// URI: " + parsed.reason);
}
else if (substr(link, 0, 1) == "/") {
/* local file — ok */
}
else if (!valid_url(link))
errors.push("subscription link must be HTTP URL, wdtt:// URI, or local file path: " + link);
}
let community_lists = list_values(section.community_lists);
for (let cl in community_lists) {
if (!valid_community_list(cl))
errors.push("unknown community list: " + cl);
}
return errors;
}
function validate_section_json() {
let input = read_stdin();
let data = object_or_empty(json(input));
let errors = validate_section(data);
if (length(errors) > 0)
write_json({ valid: false, message: join("; ", errors) });
else
write_json({ valid: true, message: "" });
return length(errors) == 0;
}
function read_stdin() {
let input = fs.open("/dev/stdin", "r");
if (!input)
return "";
let data = input.read("all");
input.close();
return data == null ? "" : data;
}
function write_json(value) {
printf("%s\n", sprintf("%J", value));
}
return {
validate_section,
validate_section_json,
parse_wdtt_uri,
url_decode,
valid_peer,
valid_mode,
valid_qwdtt_mode,
valid_url,
valid_community_list,
list_values,
modes: WDTT_MODES,
qwdtt_modes: QWDTT_MODES,
community_lists: WDTT_COMMUNITY_LISTS
};
@@ -63,6 +63,7 @@ function candidates() {
let action = option(section, "action", ""); let action = option(section, "action", "");
if (action == "connection" || action == "proxy" || action == "outbound" || action == "vpn" || if (action == "connection" || action == "proxy" || action == "outbound" || action == "vpn" ||
action == "awg" || action == "warp" || action == "byedpi" || action == "zapret" || action == "zapret2" || action == "awg" || action == "warp" || action == "byedpi" || action == "zapret" || action == "zapret2" ||
action == "wdtt" || action == "olcrtc" ||
action == "anytls" || action == "snell" || action == "mieru" || action == "sudoku" || action == "anytls" || action == "snell" || action == "mieru" || action == "sudoku" ||
action == "masque" || action == "openvpn") action == "masque" || action == "openvpn")
push(result, as_string(section[".name"])); push(result, as_string(section[".name"]));
+6
View File
@@ -11,6 +11,9 @@ let strings = {
bootstrap_dns_server: "Bootstrap DNS", bootstrap_dns_server: "Bootstrap DNS",
dns_strategy: "DNS Strategy", dns_strategy: "DNS Strategy",
dns_detour_enabled: "DNS Detour", dns_detour_enabled: "DNS Detour",
routed_dns_enabled: "Routed DNS",
routed_dns_type: "Routed DNS Protocol",
routed_dns_server: "Routed DNS Servers",
source_network_interfaces: "Inbound Interfaces", source_network_interfaces: "Inbound Interfaces",
enable_output_network_interface: "Bind to WAN", enable_output_network_interface: "Bind to WAN",
enable_badwan_interface_monitoring: "WAN Monitoring", enable_badwan_interface_monitoring: "WAN Monitoring",
@@ -561,6 +564,9 @@ let strings = {
bootstrap_dns_server: "Bootstrap DNS", bootstrap_dns_server: "Bootstrap DNS",
dns_strategy: "Стратегия DNS", dns_strategy: "Стратегия DNS",
dns_detour_enabled: "DNS Detour", dns_detour_enabled: "DNS Detour",
routed_dns_enabled: "Маршрутизированный DNS",
routed_dns_type: "Протокол маршрутизированного DNS",
routed_dns_server: "Серверы маршрутизированного DNS",
source_network_interfaces: "Входящие интерфейсы", source_network_interfaces: "Входящие интерфейсы",
enable_output_network_interface: "Привязка к WAN", enable_output_network_interface: "Привязка к WAN",
enable_badwan_interface_monitoring: "Мониторинг WAN", enable_badwan_interface_monitoring: "Мониторинг WAN",
@@ -120,6 +120,8 @@ const ZAPRET_UC = LIB_DIR + "/providers/zapret/runtime.uc";
const ZAPRET2_UC = LIB_DIR + "/providers/zapret2/runtime.uc"; const ZAPRET2_UC = LIB_DIR + "/providers/zapret2/runtime.uc";
const BYEDPI_UC = LIB_DIR + "/providers/byedpi/runtime.uc"; const BYEDPI_UC = LIB_DIR + "/providers/byedpi/runtime.uc";
const TAILSCALE_UC = LIB_DIR + "/providers/tailscale/runtime.uc"; const TAILSCALE_UC = LIB_DIR + "/providers/tailscale/runtime.uc";
const WDTT_UC = LIB_DIR + "/providers/wdtt/runtime.uc";
const OLCRTC_UC = LIB_DIR + "/providers/olcrtc/runtime.uc";
const PARENTAL_QUOTA_UC = LIB_DIR + "/service/parental_quota.uc"; const PARENTAL_QUOTA_UC = LIB_DIR + "/service/parental_quota.uc";
const PACKAGES_UC = LIB_DIR + "/core/packages.uc"; const PACKAGES_UC = LIB_DIR + "/core/packages.uc";
const WATCHDOG_UC = LIB_DIR + "/service/watchdog.uc"; const WATCHDOG_UC = LIB_DIR + "/service/watchdog.uc";
@@ -935,7 +937,10 @@ function start_main() {
release_start_subscription_update_lock(); release_start_subscription_update_lock();
module_success(ZAPRET_UC, [ "start-runtime" ]); module_success(ZAPRET_UC, [ "start-runtime" ]);
module_success(ZAPRET2_UC, [ "start-runtime" ]); module_success(ZAPRET2_UC, [ "start-runtime" ]);
module_success(WDTT_UC, [ "start-runtime" ]);
module_success(OLCRTC_UC, [ "start-runtime" ]);
module_background(UI_UC, [ "latency-boot-sweep" ]);
module_background(UPDATES_UC, [ "list-update" ]); module_background(UPDATES_UC, [ "list-update" ]);
module_background(LIB_DIR + "/service/lifecycle.uc", [ "discover-awg-mtu" ]); module_background(LIB_DIR + "/service/lifecycle.uc", [ "discover-awg-mtu" ]);
return 0; return 0;
@@ -1021,6 +1026,10 @@ function stop_main() {
log_message("Zapret2 stop failed (non-fatal)", "warn"); log_message("Zapret2 stop failed (non-fatal)", "warn");
if (!module_success(BYEDPI_UC, [ "stop-runtime" ])) if (!module_success(BYEDPI_UC, [ "stop-runtime" ]))
log_message("ByeDPI stop failed (non-fatal)", "warn"); log_message("ByeDPI stop failed (non-fatal)", "warn");
if (!module_success(WDTT_UC, [ "stop-runtime" ]))
log_message("WDTT stop failed (non-fatal)", "warn");
if (!module_success(OLCRTC_UC, [ "stop-runtime" ]))
log_message("OlcRTC stop failed (non-fatal)", "warn");
if (!module_success(TAILSCALE_UC, [ "stop-runtime" ])) if (!module_success(TAILSCALE_UC, [ "stop-runtime" ]))
log_message("Tailscale stop failed (non-fatal)", "warn"); log_message("Tailscale stop failed (non-fatal)", "warn");
module_success(PARENTAL_QUOTA_UC, [ "remove-cron" ]); module_success(PARENTAL_QUOTA_UC, [ "remove-cron" ]);
@@ -1209,6 +1218,8 @@ function parse_reload_plan(output) {
needs_zapret_restart: 0, needs_zapret_restart: 0,
needs_zapret2_restart: 0, needs_zapret2_restart: 0,
needs_byedpi_restart: 0, needs_byedpi_restart: 0,
needs_wdtt_restart: 0,
needs_olcrtc_restart: 0,
needs_dnsmasq_configure: 0, needs_dnsmasq_configure: 0,
needs_dnsmasq_restore: 0, needs_dnsmasq_restore: 0,
needs_cron_refresh: 0, needs_cron_refresh: 0,
@@ -1245,6 +1256,8 @@ function reload_actions_summary(plan) {
actions = append_reload_action(actions, plan.needs_zapret_restart, "Zapret"); actions = append_reload_action(actions, plan.needs_zapret_restart, "Zapret");
actions = append_reload_action(actions, plan.needs_zapret2_restart, "Zapret2"); actions = append_reload_action(actions, plan.needs_zapret2_restart, "Zapret2");
actions = append_reload_action(actions, plan.needs_byedpi_restart, "ByeDPI"); actions = append_reload_action(actions, plan.needs_byedpi_restart, "ByeDPI");
actions = append_reload_action(actions, plan.needs_wdtt_restart, "WDTT");
actions = append_reload_action(actions, plan.needs_olcrtc_restart, "OlcRTC");
actions = append_reload_action(actions, plan.needs_dnsmasq_configure || plan.needs_dnsmasq_restore, "dnsmasq"); actions = append_reload_action(actions, plan.needs_dnsmasq_configure || plan.needs_dnsmasq_restore, "dnsmasq");
actions = append_reload_action(actions, plan.needs_cron_refresh, "scheduled jobs"); actions = append_reload_action(actions, plan.needs_cron_refresh, "scheduled jobs");
actions = append_reload_action(actions, plan.needs_list_update, "remote lists"); actions = append_reload_action(actions, plan.needs_list_update, "remote lists");
@@ -1435,6 +1448,10 @@ function reload(reason) {
module_success(ZAPRET2_UC, [ "stop-runtime" ]); module_success(ZAPRET2_UC, [ "stop-runtime" ]);
if (plan.needs_byedpi_restart == 1) if (plan.needs_byedpi_restart == 1)
module_success(BYEDPI_UC, [ "stop-runtime" ]); module_success(BYEDPI_UC, [ "stop-runtime" ]);
if (plan.needs_wdtt_restart == 1)
module_success(WDTT_UC, [ "stop-runtime" ]);
if (plan.needs_olcrtc_restart == 1)
module_success(OLCRTC_UC, [ "stop-runtime" ]);
if (plan.needs_hosts_update == 1 && module_success(STATE_UC, [ "has-hosts-list-update-sources" ])) if (plan.needs_hosts_update == 1 && module_success(STATE_UC, [ "has-hosts-list-update-sources" ]))
module_success(HOSTS_UC, [ "list-update" ]); module_success(HOSTS_UC, [ "list-update" ]);
@@ -1503,6 +1520,10 @@ function reload(reason) {
module_success(ZAPRET_UC, [ "start-runtime" ]); module_success(ZAPRET_UC, [ "start-runtime" ]);
if (plan.needs_zapret2_restart == 1) if (plan.needs_zapret2_restart == 1)
module_success(ZAPRET2_UC, [ "start-runtime" ]); module_success(ZAPRET2_UC, [ "start-runtime" ]);
if (plan.needs_wdtt_restart == 1)
module_success(WDTT_UC, [ "start-runtime" ]);
if (plan.needs_olcrtc_restart == 1)
module_success(OLCRTC_UC, [ "start-runtime" ]);
if (plan.needs_byedpi_restart == 1) if (plan.needs_byedpi_restart == 1)
module_success(BYEDPI_UC, [ "start-runtime" ]); module_success(BYEDPI_UC, [ "start-runtime" ]);
// Native Tailscale must be up before sing-box so tailnet routes win over // Native Tailscale must be up before sing-box so tailnet routes win over
+14
View File
@@ -63,6 +63,8 @@ function emit_reload_plan(previous, current, context) {
zapret2_queue: current.zapret2_queue != previous.zapret2_queue, zapret2_queue: current.zapret2_queue != previous.zapret2_queue,
zapret2_runtime: current.zapret2_runtime != previous.zapret2_runtime, zapret2_runtime: current.zapret2_runtime != previous.zapret2_runtime,
byedpi_runtime: current.byedpi_runtime != previous.byedpi_runtime, byedpi_runtime: current.byedpi_runtime != previous.byedpi_runtime,
wdtt_runtime: current.wdtt_runtime != previous.wdtt_runtime,
olcrtc_runtime: current.olcrtc_runtime != previous.olcrtc_runtime,
list: current.list != previous.list, list: current.list != previous.list,
hosts_list: current.hosts_list != previous.hosts_list, hosts_list: current.hosts_list != previous.hosts_list,
cron: current.cron != previous.cron cron: current.cron != previous.cron
@@ -74,6 +76,8 @@ function emit_reload_plan(previous, current, context) {
zapret_restart: false, zapret_restart: false,
zapret2_restart: false, zapret2_restart: false,
byedpi_restart: false, byedpi_restart: false,
wdtt_restart: false,
olcrtc_restart: false,
dnsmasq_configure: false, dnsmasq_configure: false,
dnsmasq_restore: false, dnsmasq_restore: false,
cron_refresh: false, cron_refresh: false,
@@ -112,6 +116,12 @@ function emit_reload_plan(previous, current, context) {
if (changed.byedpi_runtime) if (changed.byedpi_runtime)
needs.byedpi_restart = true; needs.byedpi_restart = true;
if (changed.wdtt_runtime)
needs.wdtt_restart = true;
if (changed.olcrtc_runtime)
needs.olcrtc_restart = true;
if (changed.dnsmasq) { if (changed.dnsmasq) {
if (!current.dont_touch_dhcp) if (!current.dont_touch_dhcp)
needs.dnsmasq_configure = true; needs.dnsmasq_configure = true;
@@ -175,6 +185,8 @@ function emit_reload_plan(previous, current, context) {
emit_bool("changed_zapret2_queue", changed.zapret2_queue); emit_bool("changed_zapret2_queue", changed.zapret2_queue);
emit_bool("changed_zapret2_runtime", changed.zapret2_runtime); emit_bool("changed_zapret2_runtime", changed.zapret2_runtime);
emit_bool("changed_byedpi_runtime", changed.byedpi_runtime); emit_bool("changed_byedpi_runtime", changed.byedpi_runtime);
emit_bool("changed_wdtt_runtime", changed.wdtt_runtime);
emit_bool("changed_olcrtc_runtime", changed.olcrtc_runtime);
emit_bool("changed_cron", changed.cron); emit_bool("changed_cron", changed.cron);
emit_bool("changed_list", changed.list); emit_bool("changed_list", changed.list);
emit_bool("changed_hosts_list", changed.hosts_list); emit_bool("changed_hosts_list", changed.hosts_list);
@@ -184,6 +196,8 @@ function emit_reload_plan(previous, current, context) {
emit_bool("needs_zapret_restart", needs.zapret_restart); emit_bool("needs_zapret_restart", needs.zapret_restart);
emit_bool("needs_zapret2_restart", needs.zapret2_restart); emit_bool("needs_zapret2_restart", needs.zapret2_restart);
emit_bool("needs_byedpi_restart", needs.byedpi_restart); emit_bool("needs_byedpi_restart", needs.byedpi_restart);
emit_bool("needs_wdtt_restart", needs.wdtt_restart);
emit_bool("needs_olcrtc_restart", needs.olcrtc_restart);
emit_bool("needs_dnsmasq_configure", needs.dnsmasq_configure); emit_bool("needs_dnsmasq_configure", needs.dnsmasq_configure);
emit_bool("needs_dnsmasq_restore", needs.dnsmasq_restore); emit_bool("needs_dnsmasq_restore", needs.dnsmasq_restore);
emit_bool("needs_cron_refresh", needs.cron_refresh); emit_bool("needs_cron_refresh", needs.cron_refresh);
+70
View File
@@ -138,6 +138,8 @@ const RELOAD_STATE_FIELDS = [
"zapret2_queue_signature", "zapret2_queue_signature",
"zapret2_runtime_signature", "zapret2_runtime_signature",
"byedpi_runtime_signature", "byedpi_runtime_signature",
"wdtt_runtime_signature",
"olcrtc_runtime_signature",
"list_signature", "list_signature",
"hosts_list_signature", "hosts_list_signature",
"cron_signature", "cron_signature",
@@ -1008,6 +1010,9 @@ function nft_runtime_signature_body(settings, sections, schedules, profiles, gue
body = signature_add_value(body, "rule." + name + ".fully_routed_ips", option(section, "fully_routed_ips", "")); body = signature_add_value(body, "rule." + name + ".fully_routed_ips", option(section, "fully_routed_ips", ""));
body = signature_add_value(body, "rule." + name + ".excluded_ips", option(section, "excluded_ips", "")); body = signature_add_value(body, "rule." + name + ".excluded_ips", option(section, "excluded_ips", ""));
body = signature_add_value(body, "rule." + name + ".excluded_protocol", option(section, "excluded_protocol", "")); body = signature_add_value(body, "rule." + name + ".excluded_protocol", option(section, "excluded_protocol", ""));
body = signature_add_value(body, "rule." + name + ".routed_dns_enabled", bool_option(section, "routed_dns_enabled", false) ? "1" : "0");
body = signature_add_value(body, "rule." + name + ".routed_dns_type", option(section, "routed_dns_type", ""));
body = signature_add_value(body, "rule." + name + ".routed_dns_server", option(section, "routed_dns_server", ""));
body = signature_add_value(body, "rule." + name + ".protocol", option(section, "protocol", "")); body = signature_add_value(body, "rule." + name + ".protocol", option(section, "protocol", ""));
let comm_subnets = bool_option(section, "community_subnets", true) ? rule_config.filter_community_subnet_lists_value(connections.community_lists_value(section)) : ""; let comm_subnets = bool_option(section, "community_subnets", true) ? rule_config.filter_community_subnet_lists_value(connections.community_lists_value(section)) : "";
body = signature_add_value(body, "rule." + name + ".community_subnet_lists", comm_subnets); body = signature_add_value(body, "rule." + name + ".community_subnet_lists", comm_subnets);
@@ -1781,6 +1786,57 @@ function byedpi_runtime_signature_body(sections) {
return body; return body;
} }
function wdtt_runtime_signature_body(sections) {
let body = "";
for (let section in sections) {
section = object_or_empty(section);
if (!section_action_is_enabled(section, "wdtt"))
continue;
let name = section_name(section);
body = signature_add_value(body, "wdtt." + name + ".peer", option(section, "peer", ""));
body = signature_add_value(body, "wdtt." + name + ".password", option(section, "password", ""));
body = signature_add_value(body, "wdtt." + name + ".device_id", option(section, "device_id", ""));
body = signature_add_value(body, "wdtt." + name + ".mode", option(section, "mode", ""));
body = signature_add_value(body, "wdtt." + name + ".workers", option(section, "workers", ""));
body = signature_add_value(body, "wdtt." + name + ".max_hashes", option(section, "max_hashes", ""));
body = signature_add_value(body, "wdtt." + name + ".mtu", option(section, "mtu", ""));
body = signature_add_value(body, "wdtt." + name + ".refresh", option(section, "refresh", ""));
body = signature_add_value(body, "wdtt." + name + ".auto_update", bool_option(section, "auto_update", true) ? "1" : "0");
body = signature_add_value(body, "wdtt." + name + ".block_doh", bool_option(section, "block_doh", false) ? "1" : "0");
body = signature_add_value(body, "wdtt." + name + ".block_ipv6", bool_option(section, "block_ipv6", false) ? "1" : "0");
body = signature_add_value(body, "wdtt." + name + ".subscription_links", join(" ", list_option(section, "subscription_links")));
body = signature_add_value(body, "wdtt." + name + ".community_lists", join(" ", list_option(section, "community_lists")));
body = signature_add_value(body, "wdtt." + name + ".remote_domain_list", join(" ", list_option(section, "remote_domain_list")));
body = signature_add_value(body, "wdtt." + name + ".qwdtt_mode", option(section, "qwdtt_mode", ""));
}
return body;
}
function olcrtc_runtime_signature_body(sections) {
let body = "";
for (let section in sections) {
section = object_or_empty(section);
if (!section_action_is_enabled(section, "olcrtc"))
continue;
let name = section_name(section);
body = signature_add_value(body, "olcrtc." + name + ".provider", option(section, "provider", ""));
body = signature_add_value(body, "olcrtc." + name + ".transport", option(section, "transport", ""));
body = signature_add_value(body, "olcrtc." + name + ".room_id", option(section, "room_id", ""));
body = signature_add_value(body, "olcrtc." + name + ".crypto_key", option(section, "crypto_key", ""));
body = signature_add_value(body, "olcrtc." + name + ".socks_host", option(section, "socks_host", ""));
body = signature_add_value(body, "olcrtc." + name + ".socks_port", option(section, "socks_port", ""));
body = signature_add_value(body, "olcrtc." + name + ".dns_server", option(section, "dns_server", ""));
body = signature_add_value(body, "olcrtc." + name + ".subscription_links", join(" ", list_option(section, "subscription_links")));
}
return body;
}
function reload_state_values_from_sources(format, settings, sections, servers, dnsmasq, legacy_dnsmasq_present, mwan3_active_value, schedules, profiles, guest_modes) { function reload_state_values_from_sources(format, settings, sections, servers, dnsmasq, legacy_dnsmasq_present, mwan3_active_value, schedules, profiles, guest_modes) {
return { return {
format: as_string(format), format: as_string(format),
@@ -1793,6 +1849,8 @@ function reload_state_values_from_sources(format, settings, sections, servers, d
zapret2_queue_signature: signature_hash(action_queue_signature_body(sections, "zapret2", "zapret2_queue.section")), zapret2_queue_signature: signature_hash(action_queue_signature_body(sections, "zapret2", "zapret2_queue.section")),
zapret2_runtime_signature: signature_hash(zapret2_runtime_signature_body(sections)), zapret2_runtime_signature: signature_hash(zapret2_runtime_signature_body(sections)),
byedpi_runtime_signature: signature_hash(byedpi_runtime_signature_body(sections)), byedpi_runtime_signature: signature_hash(byedpi_runtime_signature_body(sections)),
wdtt_runtime_signature: signature_hash(wdtt_runtime_signature_body(sections)),
olcrtc_runtime_signature: signature_hash(olcrtc_runtime_signature_body(sections)),
list_signature: signature_hash(list_update_signature_body(sections)), list_signature: signature_hash(list_update_signature_body(sections)),
hosts_list_signature: signature_hash(hosts_list_signature_body(sections)), hosts_list_signature: signature_hash(hosts_list_signature_body(sections)),
cron_signature: signature_hash(cron_signature_body(settings, sections)), cron_signature: signature_hash(cron_signature_body(settings, sections)),
@@ -2189,6 +2247,18 @@ else if (mode == "byedpi-runtime-signature-fixture") {
let data = fixture_data(ARGV[1]); let data = fixture_data(ARGV[1]);
exit(print_signature_hash(byedpi_runtime_signature_body(fixture_section_list(data))) ? 0 : 1); exit(print_signature_hash(byedpi_runtime_signature_body(fixture_section_list(data))) ? 0 : 1);
} }
else if (mode == "wdtt-runtime-signature")
exit(print_signature_hash(wdtt_runtime_signature_body(uci_sections("section"))) ? 0 : 1);
else if (mode == "wdtt-runtime-signature-fixture") {
let data = fixture_data(ARGV[1]);
exit(print_signature_hash(wdtt_runtime_signature_body(fixture_section_list(data))) ? 0 : 1);
}
else if (mode == "olcrtc-runtime-signature")
exit(print_signature_hash(olcrtc_runtime_signature_body(uci_sections("section"))) ? 0 : 1);
else if (mode == "olcrtc-runtime-signature-fixture") {
let data = fixture_data(ARGV[1]);
exit(print_signature_hash(olcrtc_runtime_signature_body(fixture_section_list(data))) ? 0 : 1);
}
else if (mode == "dont-touch-dhcp") else if (mode == "dont-touch-dhcp")
print(dont_touch_dhcp_value(uci_settings()), "\n"); print(dont_touch_dhcp_value(uci_settings()), "\n");
else if (mode == "dont-touch-dhcp-fixture") { else if (mode == "dont-touch-dhcp-fixture") {
@@ -1801,6 +1801,8 @@ function view_system_info(token, chat_id, msg_id) {
if (info.zapret_installed == "1") text += "🛡 Zapret: <code>" + as_string(info.zapret_version || "?") + "</code>\n"; if (info.zapret_installed == "1") text += "🛡 Zapret: <code>" + as_string(info.zapret_version || "?") + "</code>\n";
if (info.zapret2_installed == "1") text += "🛡 Zapret2: <code>" + as_string(info.zapret2_version || "?") + "</code>\n"; if (info.zapret2_installed == "1") text += "🛡 Zapret2: <code>" + as_string(info.zapret2_version || "?") + "</code>\n";
if (info.byedpi_installed == "1") text += "🛡 ByeDPI: <code>" + as_string(info.byedpi_version || "?") + "</code>\n"; if (info.byedpi_installed == "1") text += "🛡 ByeDPI: <code>" + as_string(info.byedpi_version || "?") + "</code>\n";
if (info.wdtt_installed == "1") text += "🛡 WDTT: <code>" + as_string(info.wdtt_version || "?") + "</code>\n";
if (info.olcrtc_installed == "1") text += "🛡 OlcRTC: <code>" + as_string(info.olcrtc_version || "?") + "</code>\n";
} catch(e) { } catch(e) {
text += t("info_read_error") + " " + e; text += t("info_read_error") + " " + e;
} }
+62
View File
@@ -3,6 +3,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 connections = require("config.connections");
let as_string = common.as_string; let as_string = common.as_string;
@@ -1485,6 +1486,65 @@ function action_dir(kind) {
return ""; return "";
} }
function latency_boot_sweep() {
let guard_file = "/var/run/tachyon/boot-sweep-done";
if (fs.stat(guard_file) != null)
return;
ensure_dirs();
let sections = uci.sections(CONFIG_NAME, "section");
let has_groups = false;
for (let section in sections) {
if (!bool_option(section, "enabled", true))
continue;
if (!bool_option(section, "urltest_enabled", false))
continue;
has_groups = true;
break;
}
if (!has_groups) {
fs.writefile(guard_file, as_string(time()));
return;
}
let clash_api_check = command_status("curl -s -o /dev/null -w '%{http_code}' http://127.0.0.1:9090/ 2>/dev/null | grep -q '200\\|404'") == 0;
if (!clash_api_check) {
fs.writefile(guard_file, as_string(time()));
return;
}
for (let section in sections) {
if (!bool_option(section, "enabled", true))
continue;
if (!bool_option(section, "urltest_enabled", false))
continue;
let section_name = section[".name"];
let urltest_id = "";
for (let ut in connections.urltests(section)) {
urltest_id = ut;
break;
}
if (urltest_id == "")
continue;
let group_tag = section_name + "-urltest-" + urltest_id;
let id = "boot-sweep-" + section_name;
let path = job_state_path_value(LATENCY_ACTION_DIR, id);
if (path == "")
continue;
write_state_file(path, running_latency_action_value("group", section_name, group_tag, now_seconds()));
let method_plan = latency_clash_method("group");
command_status(command_from_args([ BIN_PATH, "clash_api", "get_group_latency", group_tag, method_plan.timeout, path ]) + " >/dev/null 2>&1");
if (fs.stat(path) != null)
write_finished_action_state(path, true, "Boot latency sweep completed", 0);
}
fs.writefile(guard_file, as_string(time()));
}
function action_ack(kind, job_id_value) { function action_ack(kind, job_id_value) {
let dir = action_dir(kind); let dir = action_dir(kind);
if (dir == "") { if (dir == "") {
@@ -1594,6 +1654,8 @@ else if (mode == "latency-test-status")
latency_test_status(ARGV[1]); latency_test_status(ARGV[1]);
else if (mode == "action-ack") else if (mode == "action-ack")
action_ack(ARGV[1], ARGV[2]); action_ack(ARGV[1], ARGV[2]);
else if (mode == "latency-boot-sweep")
latency_boot_sweep();
else if (mode == "cleanup-action-dir-fixture") else if (mode == "cleanup-action-dir-fixture")
cleanup_dir(ARGV[1]); cleanup_dir(ARGV[1]);
else { else {
+2 -1
View File
@@ -619,7 +619,8 @@ function base_config(settings, service_address, runtime_context) {
function mixed_proxy_enabled_action(action) { function mixed_proxy_enabled_action(action) {
return action == "connection" || action == "proxy" || action == "outbound" || action == "vpn" || return action == "connection" || action == "proxy" || action == "outbound" || action == "vpn" ||
action == "awg" || action == "byedpi" || action == "zapret" || action == "zapret2"; action == "awg" || action == "byedpi" || action == "zapret" || action == "zapret2" ||
action == "wdtt" || action == "olcrtc";
} }
function add_mixed_proxy_for_section(config, section, service_address) { function add_mixed_proxy_for_section(config, section, service_address) {
@@ -1802,6 +1802,32 @@ function add_byedpi_outbound(config, section, sections) {
}); });
} }
function add_wdtt_outbound(config, section, sections) {
let index = enabled_action_index(sections, section, "wdtt");
if (index <= 0)
ctx.runtime_generate_unsupported("unable to resolve WDTT index for " + section[".name"]);
push(config.outbounds, {
type: "socks",
tag: outbound_tag(section[".name"]),
server: runtime_constants.WDTT_LISTEN_ADDRESS,
server_port: runtime_constants.WDTT_PORT_BASE + index - 1,
version: "5"
});
}
function add_olcrtc_outbound(config, section, sections) {
let index = enabled_action_index(sections, section, "olcrtc");
if (index <= 0)
ctx.runtime_generate_unsupported("unable to resolve OlcRTC index for " + section[".name"]);
push(config.outbounds, {
type: "socks",
tag: outbound_tag(section[".name"]),
server: runtime_constants.OLCRTC_LISTEN_ADDRESS,
server_port: runtime_constants.OLCRTC_PORT_BASE + index - 1,
version: "5"
});
}
return { return {
init, init,
reserved_runtime_tag_set, reserved_runtime_tag_set,
@@ -1836,5 +1862,7 @@ return {
add_openvpn_endpoint, add_openvpn_endpoint,
add_zapret_outbound, add_zapret_outbound,
add_zapret2_outbound, add_zapret2_outbound,
add_byedpi_outbound add_byedpi_outbound,
add_wdtt_outbound,
add_olcrtc_outbound
}; };
@@ -936,7 +936,14 @@ function domain_ip_list_ruleset_tag(section_name) {
function domain_ip_list_ruleset_path(section_name) { function domain_ip_list_ruleset_path(section_name) {
let folder = ctx.runtime_ruleset_folder || runtime_ruleset_folder; let folder = ctx.runtime_ruleset_folder || runtime_ruleset_folder;
return folder + "/" + domain_ip_list_ruleset_tag(section_name) + ".json"; let tag = domain_ip_list_ruleset_tag(section_name);
let tmp_path = folder + "/" + tag + ".json";
let etc_path = "/etc/tachyon/rulesets/" + tag + ".json";
if (source_rulesets.has_rules(tmp_path))
return tmp_path;
if (source_rulesets.has_rules(etc_path))
return etc_path;
return tmp_path;
} }
function reference_is_local(reference) { function reference_is_local(reference) {
@@ -948,7 +955,8 @@ function source_file_exists(path) {
} }
function rebuild_local_domain_ip_list_ruleset(section_name, references, domains_only) { function rebuild_local_domain_ip_list_ruleset(section_name, references, domains_only) {
let ruleset_path = domain_ip_list_ruleset_path(section_name); let folder = ctx.runtime_ruleset_folder || runtime_ruleset_folder;
let ruleset_path = folder + "/" + domain_ip_list_ruleset_tag(section_name) + ".json";
let has_local = false; let has_local = false;
for (let reference in references) { for (let reference in references) {
@@ -1086,9 +1094,15 @@ function push_dns_matcher_rule(config, rule) {
} }
function section_dns_server(section) { function section_dns_server(section) {
return option(section, "action", "") == "bypass" if (option(section, "action", "") == "bypass")
? runtime_constants.DNS_SERVER_TAG return runtime_constants.DNS_SERVER_TAG;
: runtime_constants.FAKEIP_DNS_SERVER_TAG; if (connections.routed_dns_enabled(section))
return runtime_constants.tag(section[".name"], "routed-dns-server");
return runtime_constants.FAKEIP_DNS_SERVER_TAG;
}
function routed_dns_server_tag(section_name) {
return runtime_constants.tag(section_name, "routed-dns-server");
} }
function single_or_array(values) { function single_or_array(values) {
@@ -1143,6 +1157,32 @@ function add_dns_server_for_section(config, section) {
return server_tags; return server_tags;
} }
function add_routed_dns_server_for_section(config, section) {
if (!connections.routed_dns_enabled(section))
return;
let section_name = section[".name"];
let servers = connections.routed_dns_servers(section);
let dns_type = connections.routed_dns_type(section);
let tag_name = routed_dns_server_tag(section_name);
let detour = outbound_tag(section_name);
for (let i = 0; i < length(servers); i++) {
let s_val = servers[i];
let server_tag = length(servers) <= 1
? tag_name
: runtime_constants.tag(section_name, "routed-dns-server-" + (i + 1));
let server = runtime_dns.server_from_options(
server_tag,
dns_type,
s_val,
detour
);
if (server.unsupported)
ctx.runtime_generate_unsupported(server.unsupported);
push(config.dns.servers, server);
}
}
function source_dns_inbound_matcher() { function source_dns_inbound_matcher() {
return [ runtime_constants.SOURCE_DNS_INBOUND_TAG ]; return [ runtime_constants.SOURCE_DNS_INBOUND_TAG ];
} }
@@ -1369,11 +1409,26 @@ function add_excluded_ips_rule(config, section) {
if (length(excluded) == 0) if (length(excluded) == 0)
return; return;
let resolved = [];
for (let item in excluded) {
let val = trim(as_string(item));
if (val == "") continue;
let is_mac = match(val, /^([0-9a-fA-F]{2}[:-]){5}[0-9a-fA-F]{2}$/) != null;
if (is_mac) {
for (let res_ip in core_ip.resolve_mac_to_ips(val))
push(resolved, res_ip + (index(res_ip, ":") != -1 ? "/128" : "/32"));
} else {
push(resolved, val);
}
}
if (length(resolved) == 0)
return;
let route_rule = { let route_rule = {
action: "route", action: "route",
inbound: tproxy_inbound_matcher(), inbound: tproxy_inbound_matcher(),
outbound: runtime_constants.DIRECT_OUTBOUND_TAG, outbound: runtime_constants.DIRECT_OUTBOUND_TAG,
source_ip_cidr: single_or_array(excluded) source_ip_cidr: single_or_array(resolved)
}; };
push(config.route.rules, route_rule); push(config.route.rules, route_rule);
} }
@@ -1642,6 +1697,10 @@ function add_outbound_for_section(config, section, taken, sections) {
ctx.outbounds.add_zapret2_outbound(config, section, sections); ctx.outbounds.add_zapret2_outbound(config, section, sections);
else if (action == "byedpi") else if (action == "byedpi")
ctx.outbounds.add_byedpi_outbound(config, section, sections); ctx.outbounds.add_byedpi_outbound(config, section, sections);
else if (action == "wdtt")
ctx.outbounds.add_wdtt_outbound(config, section, sections);
else if (action == "olcrtc")
ctx.outbounds.add_olcrtc_outbound(config, section, sections);
else if (action == "bypass") { else if (action == "bypass") {
/* route-only action */ /* route-only action */
} }
@@ -1657,6 +1716,9 @@ function add_outbound_for_section(config, section, taken, sections) {
else { else {
ctx.runtime_generate_unsupported("unsupported action " + action); ctx.runtime_generate_unsupported("unsupported action " + action);
} }
if (action != "dns" && action != "hosts" && action != "bypass" && action != "block")
add_routed_dns_server_for_section(config, section);
} }
function reserve_section_outbound_tags(sections, taken) { function reserve_section_outbound_tags(sections, taken) {
@@ -1664,6 +1726,7 @@ function reserve_section_outbound_tags(sections, taken) {
let action = option(section, "action", ""); let action = option(section, "action", "");
if (connections.is_connections_action(action) || if (connections.is_connections_action(action) ||
action == "awg" || action == "warp" || action == "byedpi" || action == "zapret" || action == "zapret2" || action == "awg" || action == "warp" || action == "byedpi" || action == "zapret" || action == "zapret2" ||
action == "wdtt" || action == "olcrtc" ||
action == "anytls" || action == "snell" || action == "mieru" || action == "sudoku" || action == "anytls" || action == "snell" || action == "mieru" || action == "sudoku" ||
action == "masque" || action == "openvpn") action == "masque" || action == "openvpn")
taken[outbound_tag(section[".name"])] = true; taken[outbound_tag(section[".name"])] = true;
@@ -1695,6 +1758,7 @@ function failover_candidate(sections) {
let action = option(section, "action", ""); let action = option(section, "action", "");
if (connections.is_connections_action(action) || if (connections.is_connections_action(action) ||
action == "awg" || action == "warp" || action == "byedpi" || action == "zapret" || action == "zapret2" || action == "awg" || action == "warp" || action == "byedpi" || action == "zapret" || action == "zapret2" ||
action == "wdtt" || action == "olcrtc" ||
action == "anytls" || action == "snell" || action == "mieru" || action == "sudoku" || action == "anytls" || action == "snell" || action == "mieru" || action == "sudoku" ||
action == "masque" || action == "openvpn") { action == "masque" || action == "openvpn") {
push(result, section); push(result, section);
+10 -5
View File
@@ -72,13 +72,16 @@ function config(settings, runtime) {
if (type(runtime) == "object" && type(runtime.dns_health_inbounds) == "array") if (type(runtime) == "object" && type(runtime.dns_health_inbounds) == "array")
for (let inbound in runtime.dns_health_inbounds) for (let inbound in runtime.dns_health_inbounds)
push(sniff_inbounds, inbound); push(sniff_inbounds, inbound);
let dns_hijack_inbounds = [ runtime_constants.DNS_INBOUND_TAG ];
if (type(runtime) == "object" && bool_value(runtime.source_aware_dns))
push(dns_hijack_inbounds, runtime_constants.SOURCE_DNS_INBOUND_TAG);
if (type(runtime) == "object" && type(runtime.dns_health_inbounds) == "array")
for (let inbound in runtime.dns_health_inbounds)
push(dns_hijack_inbounds, inbound);
let rules = [ let rules = [
{ action: "sniff", inbound: sniff_inbounds }, { action: "sniff", inbound: sniff_inbounds },
{ action: "hijack-dns", port: 53 }, { action: "hijack-dns", inbound: dns_hijack_inbounds }
{ action: "hijack-dns", protocol: "dns" }
]; ];
if (type(runtime) == "object" && bool_value(runtime.source_aware_dns))
push(rules, { action: "hijack-dns", inbound: [ runtime_constants.SOURCE_DNS_INBOUND_TAG ] });
let result = { let result = {
rules, rules,
rule_set: [], rule_set: [],
@@ -120,7 +123,8 @@ function target(section, outbound_tag_name) {
action == "awg" || action == "warp" || action == "awg" || action == "warp" ||
action == "anytls" || action == "snell" || action == "mieru" || action == "sudoku" || action == "anytls" || action == "snell" || action == "mieru" || action == "sudoku" ||
action == "masque" || action == "openvpn" || action == "masque" || action == "openvpn" ||
action == "byedpi" || action == "zapret" || action == "zapret2") action == "byedpi" || action == "zapret" || action == "zapret2" ||
action == "wdtt" || action == "olcrtc")
return { action: "route", outbound: outbound_tag_name }; return { action: "route", outbound: outbound_tag_name };
if (action == "bypass") if (action == "bypass")
return { action: "route", outbound: runtime_constants.BYPASS_OUTBOUND_TAG }; return { action: "route", outbound: runtime_constants.BYPASS_OUTBOUND_TAG };
@@ -139,6 +143,7 @@ function has_resolve_matchers(rule) {
function resolve_rule_for_section(section, route_rule) { function resolve_rule_for_section(section, route_rule) {
let action = option(section, "action", ""); let action = option(section, "action", "");
let should_resolve = action == "byedpi" || action == "zapret" || action == "zapret2" || let should_resolve = action == "byedpi" || action == "zapret" || action == "zapret2" ||
action == "wdtt" || action == "olcrtc" ||
(connections.is_connections_action(action) && (connections.is_connections_action(action) &&
bool_option(section, "resolve_real_ip_for_routing", false)); bool_option(section, "resolve_real_ip_for_routing", false));
+18
View File
@@ -703,6 +703,12 @@ bypass
[rule.text_rule.excluded_protocol] [rule.text_rule.excluded_protocol]
[rule.text_rule.routed_dns_enabled]
0
[rule.text_rule.routed_dns_type]
[rule.text_rule.routed_dns_server]
[rule.text_rule.protocol] [rule.text_rule.protocol]
[rule.text_rule.community_subnet_lists] [rule.text_rule.community_subnet_lists]
@@ -731,6 +737,12 @@ proxy
[rule.default_enabled.excluded_protocol] [rule.default_enabled.excluded_protocol]
[rule.default_enabled.routed_dns_enabled]
0
[rule.default_enabled.routed_dns_type]
[rule.default_enabled.routed_dns_server]
[rule.default_enabled.protocol] [rule.default_enabled.protocol]
[rule.default_enabled.community_subnet_lists] [rule.default_enabled.community_subnet_lists]
@@ -799,6 +811,12 @@ bypass
[rule.enabled.excluded_protocol] [rule.enabled.excluded_protocol]
[rule.enabled.routed_dns_enabled]
0
[rule.enabled.routed_dns_type]
[rule.enabled.routed_dns_server]
[rule.enabled.protocol] [rule.enabled.protocol]
[rule.enabled.community_subnet_lists] [rule.enabled.community_subnet_lists]
+2
View File
@@ -1451,6 +1451,8 @@ zapret_runtime_signature=$(state_ucode zapret-runtime-signature-fixture "$WORK_D
zapret2_queue_signature=$(state_ucode zapret2-queue-signature-fixture "$WORK_DIR/reload-state-signatures.json") zapret2_queue_signature=$(state_ucode zapret2-queue-signature-fixture "$WORK_DIR/reload-state-signatures.json")
zapret2_runtime_signature=$(state_ucode zapret2-runtime-signature-fixture "$WORK_DIR/reload-state-signatures.json") zapret2_runtime_signature=$(state_ucode zapret2-runtime-signature-fixture "$WORK_DIR/reload-state-signatures.json")
byedpi_runtime_signature=$(state_ucode byedpi-runtime-signature-fixture "$WORK_DIR/reload-state-signatures.json") byedpi_runtime_signature=$(state_ucode byedpi-runtime-signature-fixture "$WORK_DIR/reload-state-signatures.json")
wdtt_runtime_signature=$(state_ucode wdtt-runtime-signature-fixture "$WORK_DIR/reload-state-signatures.json")
olcrtc_runtime_signature=$(state_ucode olcrtc-runtime-signature-fixture "$WORK_DIR/reload-state-signatures.json")
list_signature=$(state_ucode list-update-signature-fixture "$WORK_DIR/reload-state-signatures.json") list_signature=$(state_ucode list-update-signature-fixture "$WORK_DIR/reload-state-signatures.json")
hosts_list_signature=$(state_ucode hosts-list-signature-fixture "$WORK_DIR/reload-state-signatures.json") hosts_list_signature=$(state_ucode hosts-list-signature-fixture "$WORK_DIR/reload-state-signatures.json")
cron_signature=$(state_ucode cron-signature-fixture "$WORK_DIR/reload-state-signatures.json") cron_signature=$(state_ucode cron-signature-fixture "$WORK_DIR/reload-state-signatures.json")