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.",
"key": "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, CIDRs, or MAC addresses will never be routed through this section, even if other conditions match.",
"places": [
"../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"
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
#: src/tachyon/tabs/diagnostic/partials/renderLeakCheckModal.ts:170
msgid "Bypasses proxy (SO_MARK 0x08000000)"
@@ -8155,7 +8230,7 @@ msgid "Traffic for this level goes directly."
msgstr ""
#: ../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 ""
#: ../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"
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)"
msgstr "В обход прокси (SO_MARK 0x08000000)"
@@ -1445,6 +1520,24 @@ msgstr "DNS через прокси"
msgid "DNS through section"
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"
msgstr "Быстрый DNS-кэш"
@@ -5096,8 +5189,8 @@ msgstr "Трафик"
msgid "Traffic for this level goes directly."
msgstr "Трафик этого уровня идёт напрямую."
msgid "Traffic from these IP addresses will never be routed through this section, even if other conditions match."
msgstr "Трафик от этих IP-адресов никогда не будет маршрутизирован через эту секцию, даже если другие условия совпадают."
msgid "Traffic from these IP addresses, CIDRs, or MAC addresses will never be routed through this section, even if other conditions match."
msgstr "Трафик от этих IP-адресов, CIDR или MAC-адресов никогда не будет маршрутизирован через эту секцию, даже если другие условия совпадают."
msgid "Traffic of specified protocols (e.g. BitTorrent, QUIC, STUN) will bypass this section."
msgstr "Трафик указанных протоколов (например BitTorrent, QUIC, STUN) будет обходить эту секцию."
@@ -33,6 +33,16 @@ const componentActionKeyMap: Record<string, UpdatesActionKey> = {
'byedpi:install_version': 'byedpiInstall',
'byedpi:remove': 'byedpiRemove',
'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:install': 'tailscaleInstall',
'tailscale:install_version': 'tailscaleInstall',
@@ -415,7 +415,10 @@ function isConnectionAction(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) {
@@ -1517,7 +1520,9 @@ export async function getDashboardSections(
Promise<Tachyon.ServiceStatus | undefined>
>();
const getServiceStatus = (serviceType: 'zapret' | 'zapret2' | 'byedpi') => {
const getServiceStatus = (
serviceType: 'zapret' | 'zapret2' | 'byedpi' | 'wdtt' | 'olcrtc',
) => {
if (!serviceStatusCache.has(serviceType)) {
serviceStatusCache.set(
serviceType,
@@ -1571,6 +1576,38 @@ export async function getDashboardSections(
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) {
// Status fetch failed — show section without status
@@ -316,6 +316,20 @@ export const TachyonShellMethods = {
'/usr/bin/tachyon',
{ 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 () =>
callBaseMethod<ClashAPI.Proxies>(Tachyon.AvailableMethods.CLASH_API, [
Tachyon.AvailableClashAPIMethods.GET_PROXIES,
@@ -28,6 +28,8 @@ function componentDisplayName(component: string) {
zapret: 'Zapret',
zapret2: 'Zapret2',
byedpi: 'ByeDPI',
wdtt: 'WDTT',
olcrtc: 'OlcRTC',
};
return names[component] || component;
@@ -86,7 +86,7 @@ export function getTachyonLogNotification(
}
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) {
@@ -233,6 +233,14 @@ export interface StoreType {
byedpi_installed: number;
byedpi_backup_version?: string;
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_installed: number;
tailscale_backup_version?: string;
@@ -273,6 +281,14 @@ export interface StoreType {
byedpiInstall: { loading: boolean };
byedpiRemove: { 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 };
tailscaleInstall: { loading: boolean };
tailscaleRemove: { loading: boolean };
@@ -24,6 +24,10 @@ const UNKNOWN_SYSTEM_INFO: StoreType['diagnosticsSystemInfo'] = {
zapret2_installed: 0,
byedpi_version: _('unknown'),
byedpi_installed: 0,
wdtt_version: _('unknown'),
wdtt_installed: 0,
olcrtc_version: _('unknown'),
olcrtc_installed: 0,
tailscale_version: _('unknown'),
tailscale_installed: 0,
server_inbounds_enabled_count: -1,
@@ -108,6 +112,8 @@ export async function ensureSystemInfo({
zapret_installed: latestSystemInfo.zapret_installed,
zapret2_installed: latestSystemInfo.zapret2_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,
};
@@ -43,6 +43,8 @@ function createUiState(
zapret_installed: 1,
zapret2_installed: 1,
byedpi_installed: 0,
wdtt_installed: 0,
olcrtc_installed: 0,
server_inbounds_enabled_count: 0,
},
actions: {
@@ -37,6 +37,8 @@ function createUiState(
zapret_installed: 1,
zapret2_installed: 0,
byedpi_installed: 1,
wdtt_installed: 0,
olcrtc_installed: 0,
server_inbounds_enabled_count: 0,
...capabilities,
},
@@ -37,6 +37,14 @@ function getEmptyUpdatesActions(): StoreType['updatesActions'] {
byedpiInstall: { loading: false },
byedpiRemove: { 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 },
tailscaleInstall: { loading: false },
tailscaleRemove: { loading: false },
@@ -88,6 +96,8 @@ function applyServiceState(uiState: Tachyon.UiState) {
zapret_installed: uiState.capabilities.zapret_installed,
zapret2_installed: uiState.capabilities.zapret2_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,
};
@@ -102,6 +102,14 @@ export const initialDiagnosticStore: Pick<
byedpi_installed: 0,
byedpi_backup_version: '',
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_installed: 0,
tailscale_backup_version: '',
@@ -182,6 +190,14 @@ export const initialDiagnosticStore: Pick<
byedpiInstall: { loading: false },
byedpiRemove: { 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 },
tailscaleInstall: { loading: false },
tailscaleRemove: { loading: false },
+87 -1
View File
@@ -69,6 +69,8 @@ export namespace Tachyon {
GET_TAILSCALE_PEERS = 'get_tailscale_peers',
GET_ZAPRET2_STATUS = 'get_zapret2_status',
GET_BYEDPI_STATUS = 'get_byedpi_status',
GET_WDTT_STATUS = 'get_wdtt_status',
GET_OLCRTC_STATUS = 'get_olcrtc_status',
CLASH_API = 'clash_api',
ENABLE = 'enable',
DISABLE = 'disable',
@@ -200,7 +202,7 @@ export namespace Tachyon {
}
export interface ServiceStatus {
serviceType: 'zapret' | 'zapret2' | 'byedpi';
serviceType: 'zapret' | 'zapret2' | 'byedpi' | 'wdtt' | 'olcrtc';
configured: boolean;
ready: boolean;
conflict: boolean;
@@ -275,6 +277,8 @@ export namespace Tachyon {
| 'zapret'
| 'zapret2'
| 'byedpi'
| 'wdtt'
| 'olcrtc'
| 'awg'
| 'warp'
| 'anytls'
@@ -321,6 +325,9 @@ export namespace Tachyon {
ports?: string[];
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;
proxy_string?: string;
@@ -328,6 +335,31 @@ export namespace Tachyon {
nfqws2_opt?: string;
byedpi_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[];
subscription_urls?: string[];
interfaces?: string[];
@@ -610,6 +642,10 @@ export namespace Tachyon {
zapret2_installed: 0 | 1;
byedpi_version: string;
byedpi_installed: 0 | 1;
wdtt_version: string;
wdtt_installed: 0 | 1;
olcrtc_version: string;
olcrtc_installed: 0 | 1;
tailscale_version: string;
tailscale_installed: 0 | 1;
direct_bypass_enabled?: 0 | 1;
@@ -639,6 +675,8 @@ export namespace Tachyon {
zapret_installed: 0 | 1;
zapret2_installed: 0 | 1;
byedpi_installed: 0 | 1;
wdtt_installed: 0 | 1;
olcrtc_installed: 0 | 1;
server_inbounds_enabled_count: number;
}
@@ -887,6 +925,54 @@ export namespace Tachyon {
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 {
byedpi_installed: 0 | 1;
byedpi_package_installed: 0 | 1;
@@ -3407,6 +3407,8 @@ var Tachyon;
AvailableMethods2["GET_TAILSCALE_PEERS"] = "get_tailscale_peers";
AvailableMethods2["GET_ZAPRET2_STATUS"] = "get_zapret2_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["ENABLE"] = "enable";
AvailableMethods2["DISABLE"] = "disable";
@@ -3734,6 +3736,18 @@ var TachyonShellMethods = {
"/usr/bin/tachyon",
{ 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, [
Tachyon.AvailableClashAPIMethods.GET_PROXIES
]),
@@ -5045,7 +5059,9 @@ function isConnectionAction(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) {
return getSubscriptionSourceCount(section) > 0;
@@ -5826,6 +5842,38 @@ async function getDashboardSections(options = {}) {
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) {
}
@@ -6227,6 +6275,14 @@ var initialDiagnosticStore = {
byedpi_installed: 0,
byedpi_backup_version: "",
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_installed: 0,
tailscale_backup_version: "",
@@ -6307,6 +6363,14 @@ var initialDiagnosticStore = {
byedpiInstall: { loading: false },
byedpiRemove: { 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 },
tailscaleInstall: { loading: false },
tailscaleRemove: { loading: false },
@@ -6632,7 +6696,7 @@ function getTachyonLogNotification(line) {
return { kind: "error", line };
}
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) {
return null;
@@ -6697,6 +6761,16 @@ var componentActionKeyMap = {
"byedpi:install_version": "byedpiInstall",
"byedpi:remove": "byedpiRemove",
"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:install": "tailscaleInstall",
"tailscale:install_version": "tailscaleInstall",
@@ -6872,6 +6946,14 @@ function getEmptyUpdatesActions() {
byedpiInstall: { loading: false },
byedpiRemove: { 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 },
tailscaleInstall: { loading: false },
tailscaleRemove: { loading: false },
@@ -6913,6 +6995,8 @@ function applyServiceState(uiState) {
zapret_installed: uiState.capabilities.zapret_installed,
zapret2_installed: uiState.capabilities.zapret2_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
};
nextSystemInfo.sing_box_extended = uiState.capabilities.sing_box_extended;
@@ -7146,7 +7230,9 @@ function componentDisplayName(component) {
sing_box: "sing-box",
zapret: "Zapret",
zapret2: "Zapret2",
byedpi: "ByeDPI"
byedpi: "ByeDPI",
wdtt: "WDTT",
olcrtc: "OlcRTC"
};
return names[component] || component;
}
@@ -10767,6 +10853,7 @@ async function runZapret2Check() {
const tachyonRuntimeReady = !hasZapret2Rules || runningProcesses === expectedProcesses && supervisorProcesses === expectedProcesses;
const unexpectedRuntime = !hasZapret2Rules && (runningProcesses > 0 || supervisorProcesses > 0);
const outboundsConfigured = Boolean(data.outbounds_configured);
const routesConfigured = Boolean(data.routes_configured);
const standaloneServiceEnabled = Boolean(data.standalone_service_enabled);
const standaloneServiceRunning = Boolean(data.standalone_service_running);
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"),
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",
key: standaloneServiceRunning ? hasZapret2Rules ? _(
@@ -10925,6 +11017,10 @@ var UNKNOWN_SYSTEM_INFO = {
zapret2_installed: 0,
byedpi_version: _("unknown"),
byedpi_installed: 0,
wdtt_version: _("unknown"),
wdtt_installed: 0,
olcrtc_version: _("unknown"),
olcrtc_installed: 0,
tailscale_version: _("unknown"),
tailscale_installed: 0,
server_inbounds_enabled_count: -1,
@@ -10990,6 +11086,8 @@ async function ensureSystemInfo({
zapret_installed: latestSystemInfo.zapret_installed,
zapret2_installed: latestSystemInfo.zapret2_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
};
store.set({
@@ -23,6 +23,8 @@ const ROUTING_ACTIONS = [
"zapret",
"zapret2",
"byedpi",
"wdtt",
"olcrtc",
"awg",
"warp",
"anytls",
@@ -487,6 +489,8 @@ const actionProvidersAvailabilityState = {
zapretInstalled: false,
zapret2Installed: false,
byedpiInstalled: false,
wdttInstalled: false,
olcrtcInstalled: false,
torInstalled: 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") {
actionProvidersAvailabilityState.singBoxExtended = Boolean(
nextState.singBoxExtended,
@@ -693,6 +709,8 @@ function updateActionProvidersAvailabilityFromSystemInfo(systemInfo) {
zapretInstalled: Boolean(systemInfo.zapret_installed),
zapret2Installed: Boolean(systemInfo.zapret2_installed),
byedpiInstalled: Boolean(systemInfo.byedpi_installed),
wdttInstalled: Boolean(systemInfo.wdtt_installed),
olcrtcInstalled: Boolean(systemInfo.olcrtc_installed),
singBoxExtended: Boolean(systemInfo.sing_box_extended),
torInstalled: Boolean(systemInfo.tor_installed),
});
@@ -2102,6 +2120,14 @@ function isDownloadThroughTargetSection(section, currentSectionId) {
return isByedpiInstalledForUi();
}
if (action === "wdtt") {
return isWdttInstalledForUi();
}
if (action === "olcrtc") {
return isOlcrtcInstalledForUi();
}
return false;
}
@@ -4534,6 +4560,14 @@ function isByedpiInstalledForUi() {
return actionProvidersAvailabilityState.byedpiInstalled;
}
function isWdttInstalledForUi() {
return actionProvidersAvailabilityState.wdttInstalled;
}
function isOlcrtcInstalledForUi() {
return actionProvidersAvailabilityState.olcrtcInstalled;
}
function isSingBoxExtendedForUi() {
return actionProvidersAvailabilityState.singBoxExtended;
}
@@ -4565,6 +4599,10 @@ function getActionOptionLabel(action) {
return "Zapret2";
case "byedpi":
return "ByeDPI";
case "wdtt":
return "WDTT";
case "olcrtc":
return "OlcRTC";
case "awg":
return "AmneziaWG";
@@ -4607,6 +4645,14 @@ function getRuleActionDisplayValue(section_id) {
return "ByeDPI";
}
if (action === "wdtt") {
return "WDTT";
}
if (action === "olcrtc") {
return "OlcRTC";
}
if (action === "awg") {
return "AmneziaWG";
}
@@ -4673,6 +4719,12 @@ function populateActionOptionValues(option) {
if (isByedpiInstalledForUi()) {
option.value("byedpi", getActionOptionLabel("byedpi"));
}
if (isWdttInstalledForUi()) {
option.value("wdtt", getActionOptionLabel("wdtt"));
}
if (isOlcrtcInstalledForUi()) {
option.value("olcrtc", getActionOptionLabel("olcrtc"));
}
option.value("hosts", getActionOptionLabel("hosts"));
}
@@ -7819,6 +7871,227 @@ function createSectionContent(section) {
};
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 ──────────────────────────────────────────────
function parseHostsFile(text, options) {
@@ -10333,6 +10606,8 @@ function createSectionContent(section) {
o.depends("action", "connection");
o.depends("action", "awg");
o.depends("action", "byedpi");
o.depends("action", "wdtt");
o.depends("action", "olcrtc");
o.depends("action", "zapret");
o.depends("action", "zapret2");
o.modalonly = true;
@@ -10814,11 +11089,47 @@ function createSectionContent(section) {
key: "excluded_ips",
label: _("Excluded devices"),
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);
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, {
key: "ports",
label: _("Ports"),
@@ -10914,6 +11225,8 @@ const ACTION_COLORS = {
zapret: "#8e44ad",
zapret2: "#9b59b6",
byedpi: "#d35400",
wdtt: "#27ae60",
olcrtc: "#2980b9",
awg: "#008080",
warp: "#e67e22",
anytls: "#16a085",
+95 -2
View File
@@ -839,6 +839,81 @@ msgstr "Стратегия ByeDPI"
msgid "ByeDPI strategy cannot be empty"
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)"
msgstr "В обход прокси (SO_MARK 0x08000000)"
@@ -1445,6 +1520,24 @@ msgstr "DNS через прокси"
msgid "DNS through section"
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"
msgstr "Быстрый DNS-кэш"
@@ -5096,8 +5189,8 @@ msgstr "Трафик"
msgid "Traffic for this level goes directly."
msgstr "Трафик этого уровня идёт напрямую."
msgid "Traffic from these IP addresses will never be routed through this section, even if other conditions match."
msgstr "Трафик от этих IP-адресов никогда не будет маршрутизирован через эту секцию, даже если другие условия совпадают."
msgid "Traffic from these IP addresses, CIDRs, or MAC addresses will never be routed through this section, even if other conditions match."
msgstr "Трафик от этих IP-адресов, CIDR или MAC-адресов никогда не будет маршрутизирован через эту секцию, даже если другие условия совпадают."
msgid "Traffic of specified protocols (e.g. BitTorrent, QUIC, STUN) will bypass this section."
msgstr "Трафик указанных протоколов (например BitTorrent, QUIC, STUN) будет обходить эту секцию."
+94 -1
View File
@@ -1329,6 +1329,81 @@ msgstr ""
msgid "ByeDPI strategy cannot be empty"
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
#: src/tachyon/tabs/diagnostic/partials/renderLeakCheckModal.ts:170
msgid "Bypasses proxy (SO_MARK 0x08000000)"
@@ -2355,6 +2430,24 @@ msgstr ""
msgid "DNS through section"
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
msgid "DNS Turbo Cache"
msgstr ""
@@ -8155,7 +8248,7 @@ msgid "Traffic for this level goes directly."
msgstr ""
#: ../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 ""
#: ../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_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_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_server_capabilities Get server 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_zapret2_status: [ "diagnostics/runtime.uc", "get-zapret2-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_peers: [ "diagnostics/runtime.uc", "get-tailscale-peers", 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;
}
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) {
let package_file = tmp_dir + "/" + release.package_name;
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 || "");
}
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) {
let component = "tailscale";
let label = "Tailscale";
@@ -3115,6 +3263,26 @@ function rollback_component(component) {
restart_tachyon_after_successful_change();
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") {
let backup_bin = bdir + "/nfqws";
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);
else if (component == "byedpi" && action == "remove")
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"))
install_tailscale(action);
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) {
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) {
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() {
for (let release in releases_array_or_wrapped(read_stdin_json())) {
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]);
else if (mode == "byedpi-select-asset")
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")
sing_box_extended_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) {
component = normalize_component_name(component);
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) {
@@ -1604,7 +1604,7 @@ function component_update_check_cache() {
let results = [];
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));
if (component_update_check_result_cacheable(value))
push(results, value);
@@ -2030,6 +2030,10 @@ function automatic_component_check_names() {
push(result, "zapret2");
if (module_success([ LIB_DIR + "/providers/byedpi/runtime.uc", "installed" ]))
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;
}
+56 -1
View File
@@ -306,6 +306,34 @@ function is_connections_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) {
action = as_string(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));
}
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) {
if (type(arr) != "array") return false;
for (let item in arr) {
@@ -1552,6 +1600,13 @@ return {
subscription_download_targets,
subscription_download_target_port,
has_dns_matchers,
routed_dns_enabled,
routed_dns_type,
routed_dns_servers,
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 zapret2_validator_module = null;
let byedpi_validator_module = null;
let wdtt_validator_module = null;
let olcrtc_validator_module = null;
let constants_module = null;
let core_url = require("core.url");
let core_ip = require("core.ip");
@@ -695,6 +697,18 @@ function byedpi_validator() {
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() {
if (constants_module == null)
constants_module = require("core.constants");
@@ -769,13 +783,14 @@ function rule_action(section) {
function rule_action_supported(action) {
return contains([ "connection", "proxy", "outbound", "vpn", "awg", "warp",
"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) {
return contains([ "connection", "proxy", "outbound", "vpn", "awg", "warp",
"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) {
@@ -1367,6 +1382,53 @@ function validate_provider_strategy(kind, section, context) {
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) {
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)
@@ -1467,6 +1529,22 @@ function validate_rule(section, sections, 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)) {
validate_dashboard_filter(section);
@@ -1536,6 +1614,23 @@ function validate_rule(section, sections, context) {
for (let value in list_option(section, "domain_suffix"))
validate_combined_domain_value(value, 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);
}
@@ -1992,6 +2087,8 @@ function context_from_runtime() {
byedpi_installed: file_executable(constant_value(constants, "BYEDPI_BIN")),
zapret_installed: file_executable(constant_value(constants, "ZAPRET_PROVIDER_NFQWS_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"),
zapret2_provider_nfqws2_bin: constant_value(constants, "ZAPRET2_PROVIDER_NFQWS2_BIN"),
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_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;
}
@@ -57,6 +57,8 @@ const SINGBOX_RUNTIME_UC = LIB_DIR + "/singbox/runtime.uc";
const ZAPRET_RUNTIME_UC = LIB_DIR + "/providers/zapret/runtime.uc";
const ZAPRET2_RUNTIME_UC = LIB_DIR + "/providers/zapret2/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 ZAPRET_VALIDATOR_UC = LIB_DIR + "/providers/zapret/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
let resolv_link = "";
// 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
// lists while the subscription URL itself is reachable.
if (has_sections && cfg.subscription_url) {
@@ -5020,6 +5108,10 @@ else if (mode == "get-zapret2-status")
exit(module_passthrough(ZAPRET2_RUNTIME_UC, [ "status" ]));
else if (mode == "get-byedpi-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")
exit(module_passthrough(TAILSCALE_RUNTIME_UC, [ "status" ]));
else if (mode == "get-tailscale-peers")
+26 -4
View File
@@ -584,7 +584,8 @@ function section_action(section) {
function action_captures_traffic(action) {
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) {
@@ -698,9 +699,18 @@ function nft_excluded_source_match_args(section, family) {
return [];
let ip_key = family == 6 ? "ip6" : "ip";
let addrs = [];
for (let ip in excluded)
if (core_ip.ip_family(as_string(ip)) == family)
push(addrs, as_string(ip));
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))
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)
return [];
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 + ".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 + ".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", ""));
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);
@@ -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();
@@ -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", "");
if (action == "connection" || action == "proxy" || action == "outbound" || action == "vpn" ||
action == "awg" || action == "warp" || action == "byedpi" || action == "zapret" || action == "zapret2" ||
action == "wdtt" || action == "olcrtc" ||
action == "anytls" || action == "snell" || action == "mieru" || action == "sudoku" ||
action == "masque" || action == "openvpn")
push(result, as_string(section[".name"]));
+6
View File
@@ -11,6 +11,9 @@ let strings = {
bootstrap_dns_server: "Bootstrap DNS",
dns_strategy: "DNS Strategy",
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",
enable_output_network_interface: "Bind to WAN",
enable_badwan_interface_monitoring: "WAN Monitoring",
@@ -561,6 +564,9 @@ let strings = {
bootstrap_dns_server: "Bootstrap DNS",
dns_strategy: "Стратегия DNS",
dns_detour_enabled: "DNS Detour",
routed_dns_enabled: "Маршрутизированный DNS",
routed_dns_type: "Протокол маршрутизированного DNS",
routed_dns_server: "Серверы маршрутизированного DNS",
source_network_interfaces: "Входящие интерфейсы",
enable_output_network_interface: "Привязка к 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 BYEDPI_UC = LIB_DIR + "/providers/byedpi/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 PACKAGES_UC = LIB_DIR + "/core/packages.uc";
const WATCHDOG_UC = LIB_DIR + "/service/watchdog.uc";
@@ -935,7 +937,10 @@ function start_main() {
release_start_subscription_update_lock();
module_success(ZAPRET_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(LIB_DIR + "/service/lifecycle.uc", [ "discover-awg-mtu" ]);
return 0;
@@ -1021,6 +1026,10 @@ function stop_main() {
log_message("Zapret2 stop failed (non-fatal)", "warn");
if (!module_success(BYEDPI_UC, [ "stop-runtime" ]))
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" ]))
log_message("Tailscale stop failed (non-fatal)", "warn");
module_success(PARENTAL_QUOTA_UC, [ "remove-cron" ]);
@@ -1209,6 +1218,8 @@ function parse_reload_plan(output) {
needs_zapret_restart: 0,
needs_zapret2_restart: 0,
needs_byedpi_restart: 0,
needs_wdtt_restart: 0,
needs_olcrtc_restart: 0,
needs_dnsmasq_configure: 0,
needs_dnsmasq_restore: 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_zapret2_restart, "Zapret2");
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_cron_refresh, "scheduled jobs");
actions = append_reload_action(actions, plan.needs_list_update, "remote lists");
@@ -1435,6 +1448,10 @@ function reload(reason) {
module_success(ZAPRET2_UC, [ "stop-runtime" ]);
if (plan.needs_byedpi_restart == 1)
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" ]))
module_success(HOSTS_UC, [ "list-update" ]);
@@ -1503,6 +1520,10 @@ function reload(reason) {
module_success(ZAPRET_UC, [ "start-runtime" ]);
if (plan.needs_zapret2_restart == 1)
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)
module_success(BYEDPI_UC, [ "start-runtime" ]);
// 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_runtime: current.zapret2_runtime != previous.zapret2_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,
hosts_list: current.hosts_list != previous.hosts_list,
cron: current.cron != previous.cron
@@ -74,6 +76,8 @@ function emit_reload_plan(previous, current, context) {
zapret_restart: false,
zapret2_restart: false,
byedpi_restart: false,
wdtt_restart: false,
olcrtc_restart: false,
dnsmasq_configure: false,
dnsmasq_restore: false,
cron_refresh: false,
@@ -112,6 +116,12 @@ function emit_reload_plan(previous, current, context) {
if (changed.byedpi_runtime)
needs.byedpi_restart = true;
if (changed.wdtt_runtime)
needs.wdtt_restart = true;
if (changed.olcrtc_runtime)
needs.olcrtc_restart = true;
if (changed.dnsmasq) {
if (!current.dont_touch_dhcp)
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_runtime", changed.zapret2_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_list", changed.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_zapret2_restart", needs.zapret2_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_restore", needs.dnsmasq_restore);
emit_bool("needs_cron_refresh", needs.cron_refresh);
+70
View File
@@ -138,6 +138,8 @@ const RELOAD_STATE_FIELDS = [
"zapret2_queue_signature",
"zapret2_runtime_signature",
"byedpi_runtime_signature",
"wdtt_runtime_signature",
"olcrtc_runtime_signature",
"list_signature",
"hosts_list_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 + ".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 + ".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", ""));
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);
@@ -1781,6 +1786,57 @@ function byedpi_runtime_signature_body(sections) {
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) {
return {
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_runtime_signature: signature_hash(zapret2_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)),
hosts_list_signature: signature_hash(hosts_list_signature_body(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]);
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")
print(dont_touch_dhcp_value(uci_settings()), "\n");
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.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.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) {
text += t("info_read_error") + " " + e;
}
+62
View File
@@ -3,6 +3,7 @@
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;
@@ -1485,6 +1486,65 @@ function action_dir(kind) {
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) {
let dir = action_dir(kind);
if (dir == "") {
@@ -1594,6 +1654,8 @@ else if (mode == "latency-test-status")
latency_test_status(ARGV[1]);
else if (mode == "action-ack")
action_ack(ARGV[1], ARGV[2]);
else if (mode == "latency-boot-sweep")
latency_boot_sweep();
else if (mode == "cleanup-action-dir-fixture")
cleanup_dir(ARGV[1]);
else {
+2 -1
View File
@@ -619,7 +619,8 @@ function base_config(settings, service_address, runtime_context) {
function mixed_proxy_enabled_action(action) {
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) {
@@ -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 {
init,
reserved_runtime_tag_set,
@@ -1836,5 +1862,7 @@ return {
add_openvpn_endpoint,
add_zapret_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) {
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) {
@@ -948,7 +955,8 @@ function source_file_exists(path) {
}
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;
for (let reference in references) {
@@ -1086,9 +1094,15 @@ function push_dns_matcher_rule(config, rule) {
}
function section_dns_server(section) {
return option(section, "action", "") == "bypass"
? runtime_constants.DNS_SERVER_TAG
: runtime_constants.FAKEIP_DNS_SERVER_TAG;
if (option(section, "action", "") == "bypass")
return runtime_constants.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) {
@@ -1143,6 +1157,32 @@ function add_dns_server_for_section(config, section) {
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() {
return [ runtime_constants.SOURCE_DNS_INBOUND_TAG ];
}
@@ -1369,11 +1409,26 @@ function add_excluded_ips_rule(config, section) {
if (length(excluded) == 0)
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 = {
action: "route",
inbound: tproxy_inbound_matcher(),
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);
}
@@ -1642,6 +1697,10 @@ function add_outbound_for_section(config, section, taken, sections) {
ctx.outbounds.add_zapret2_outbound(config, section, sections);
else if (action == "byedpi")
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") {
/* route-only action */
}
@@ -1657,6 +1716,9 @@ function add_outbound_for_section(config, section, taken, sections) {
else {
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) {
@@ -1664,6 +1726,7 @@ function reserve_section_outbound_tags(sections, taken) {
let action = option(section, "action", "");
if (connections.is_connections_action(action) ||
action == "awg" || action == "warp" || action == "byedpi" || action == "zapret" || action == "zapret2" ||
action == "wdtt" || action == "olcrtc" ||
action == "anytls" || action == "snell" || action == "mieru" || action == "sudoku" ||
action == "masque" || action == "openvpn")
taken[outbound_tag(section[".name"])] = true;
@@ -1695,6 +1758,7 @@ function failover_candidate(sections) {
let action = option(section, "action", "");
if (connections.is_connections_action(action) ||
action == "awg" || action == "warp" || action == "byedpi" || action == "zapret" || action == "zapret2" ||
action == "wdtt" || action == "olcrtc" ||
action == "anytls" || action == "snell" || action == "mieru" || action == "sudoku" ||
action == "masque" || action == "openvpn") {
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")
for (let inbound in runtime.dns_health_inbounds)
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 = [
{ action: "sniff", inbound: sniff_inbounds },
{ action: "hijack-dns", port: 53 },
{ action: "hijack-dns", protocol: "dns" }
{ action: "hijack-dns", inbound: dns_hijack_inbounds }
];
if (type(runtime) == "object" && bool_value(runtime.source_aware_dns))
push(rules, { action: "hijack-dns", inbound: [ runtime_constants.SOURCE_DNS_INBOUND_TAG ] });
let result = {
rules,
rule_set: [],
@@ -120,7 +123,8 @@ function target(section, outbound_tag_name) {
action == "awg" || action == "warp" ||
action == "anytls" || action == "snell" || action == "mieru" || action == "sudoku" ||
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 };
if (action == "bypass")
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) {
let action = option(section, "action", "");
let should_resolve = action == "byedpi" || action == "zapret" || action == "zapret2" ||
action == "wdtt" || action == "olcrtc" ||
(connections.is_connections_action(action) &&
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.routed_dns_enabled]
0
[rule.text_rule.routed_dns_type]
[rule.text_rule.routed_dns_server]
[rule.text_rule.protocol]
[rule.text_rule.community_subnet_lists]
@@ -731,6 +737,12 @@ proxy
[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.community_subnet_lists]
@@ -799,6 +811,12 @@ bypass
[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.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_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")
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")
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")