feat: стабильность подписок, порог DNS failover, прямой порт прокси и TorrServer Direct
- subscription: детерминированная сортировка аутбаундов при расчете сигнатуры кэша (защита от перезапусков при shuffle) - dns: порог последовательных ошибок dns_failover_failure_threshold (защита от флаппинга) - singbox: выделенный direct_bypass inbound (mixed port 2080) для прямого выхода устройств в обход TPROXY - torrserver: автоматический bypass cgroups v2 services/torrserver прямо в WAN - zapret: нормализация устаревших параметров conntrack --ctrack-timeouts - ui: интеграция управления службами TorrServer Direct и Direct Proxy в LuCI - tests: новые тесты subscription_order_independence и direct_bypass_torrserver
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
/**
|
||||
* Formats and localizes backend service status messages (zapret, zapret2, byedpi, tailscale)
|
||||
* into localized, user-friendly messages for the LuCI dashboard.
|
||||
*/
|
||||
export function formatServiceStatusMessage(msg: string): string {
|
||||
if (!msg) return '';
|
||||
|
||||
const normalMatch = msg.match(/^([a-zA-Z0-9_-]+) provider status is normal$/);
|
||||
if (normalMatch) {
|
||||
const provider = normalMatch[1];
|
||||
return `${_('Provider status is normal')}: ${provider}`;
|
||||
}
|
||||
|
||||
const notReadyWithDetailMatch = msg.match(
|
||||
/^action=([^ ]+) is configured, but the Tachyon-managed ([^ ]+) runtime is not ready \((.+)\)$/,
|
||||
);
|
||||
if (notReadyWithDetailMatch) {
|
||||
const [, action, bin, detail] = notReadyWithDetailMatch;
|
||||
return `${_('Action is configured, but runtime is not ready')} (${action} -> ${bin}): ${detail}`;
|
||||
}
|
||||
|
||||
const notReadyMatch = msg.match(
|
||||
/^action=([^ ]+) is configured, but the Tachyon-managed ([^ ]+) runtime is not ready$/,
|
||||
);
|
||||
if (notReadyMatch) {
|
||||
const [, action, bin] = notReadyMatch;
|
||||
return `${_('Action is configured, but runtime is not ready')} (${action} -> ${bin})`;
|
||||
}
|
||||
|
||||
const notAvailablePathMatch = msg.match(
|
||||
/^action=([^ ]+) is configured, but (.+) (?:provider|ciadpi) is not available at (.+)$/,
|
||||
);
|
||||
if (notAvailablePathMatch) {
|
||||
const [, action, provider, path] = notAvailablePathMatch;
|
||||
return `${_('Action is configured, but binary is not available')} (${action} -> ${provider}): ${path}`;
|
||||
}
|
||||
|
||||
const overlapMatch = msg.match(
|
||||
/^external NFQUEUE rules overlap with the Tachyon (.+) range (.+)$/,
|
||||
);
|
||||
if (overlapMatch) {
|
||||
const [, provider, range] = overlapMatch;
|
||||
return `${_('External NFQUEUE rules overlap with Tachyon')} ${provider} (${range})`;
|
||||
}
|
||||
|
||||
if (msg === 'legacy zapret runtime paths are still present and should be migrated') {
|
||||
return _('Legacy zapret runtime paths are still present and should be migrated');
|
||||
}
|
||||
|
||||
const unexpectedProcessesMatch = msg.match(
|
||||
/^unexpected Tachyon-managed ([^ ]+) processes are running without matching action=([^ ]+) rules$/,
|
||||
);
|
||||
if (unexpectedProcessesMatch) {
|
||||
const [, bin, action] = unexpectedProcessesMatch;
|
||||
return `${_('Unexpected background processes')} (${bin}, action=${action})`;
|
||||
}
|
||||
|
||||
if (msg.startsWith('standalone ')) {
|
||||
return _('Standalone service is active alongside Tachyon; policy or port conflicts are possible');
|
||||
}
|
||||
|
||||
const packageInstalledNoBinMatch = msg.match(
|
||||
/^(.+) package is installed, but (?:the provider binary|ciadpi) is not available at (.+)$/,
|
||||
);
|
||||
if (packageInstalledNoBinMatch) {
|
||||
const [, provider, path] = packageInstalledNoBinMatch;
|
||||
return `${_('Package is installed, but binary missing')} (${provider}): ${path}`;
|
||||
}
|
||||
|
||||
const notInstalledMatch = msg.match(
|
||||
/^(.+) (?:provider|package) is not installed; (?:action=([^ ]+) is unavailable|native Tailscale is unavailable)$/,
|
||||
);
|
||||
if (notInstalledMatch) {
|
||||
const [, provider, action] = notInstalledMatch;
|
||||
return `${_('Package is not installed')} (${provider})${action ? `: action=${action} ${_('is unavailable')}` : ''}`;
|
||||
}
|
||||
|
||||
if (msg.includes('ciadpi has restarted after exiting')) {
|
||||
return _('ByeDPI restarted after exiting; strategy or traffic load may be unstable');
|
||||
}
|
||||
|
||||
if (msg.includes('native Tailscale is configured, but the tailscale package is missing')) {
|
||||
return _('Native Tailscale is configured, but tailscale package is missing');
|
||||
}
|
||||
if (msg.includes('native Tailscale is configured, but tailscaled is not running')) {
|
||||
return _('Native Tailscale is configured, but tailscaled is not running for every section');
|
||||
}
|
||||
if (msg.includes('no server section uses native mode')) {
|
||||
return _('Tailscale package is installed, but no server section uses native mode');
|
||||
}
|
||||
|
||||
return _(msg);
|
||||
}
|
||||
@@ -38,6 +38,10 @@ const componentActionKeyMap: Record<string, UpdatesActionKey> = {
|
||||
'tailscale:install_version': 'tailscaleInstall',
|
||||
'tailscale:remove': 'tailscaleRemove',
|
||||
'tailscale:rollback': 'tailscaleRollback',
|
||||
'direct_bypass:enable': 'directBypassEnable',
|
||||
'direct_bypass:disable': 'directBypassDisable',
|
||||
'torrserver_direct:enable': 'torrserverDirectEnable',
|
||||
'torrserver_direct:disable': 'torrserverDirectDisable',
|
||||
};
|
||||
|
||||
export function getComponentActionKey(
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { formatServiceStatusMessage } from '../formatServiceStatusMessage';
|
||||
|
||||
describe('formatServiceStatusMessage', () => {
|
||||
it('formats normal provider status', () => {
|
||||
expect(formatServiceStatusMessage('zapret2 provider status is normal')).toBe(
|
||||
'Provider status is normal: zapret2',
|
||||
);
|
||||
expect(formatServiceStatusMessage('byedpi provider status is normal')).toBe(
|
||||
'Provider status is normal: byedpi',
|
||||
);
|
||||
});
|
||||
|
||||
it('formats action not ready with details', () => {
|
||||
const msg =
|
||||
'action=zapret2 is configured, but the Tachyon-managed nfqws2 runtime is not ready (Zapret2: invalid ctrack-timeouts value)';
|
||||
expect(formatServiceStatusMessage(msg)).toBe(
|
||||
'Action is configured, but runtime is not ready (zapret2 -> nfqws2): Zapret2: invalid ctrack-timeouts value',
|
||||
);
|
||||
});
|
||||
|
||||
it('formats missing binary path', () => {
|
||||
const msg = 'action=zapret2 is configured, but zapret2 provider is not available at /opt/zapret2/nfq2/nfqws2';
|
||||
expect(formatServiceStatusMessage(msg)).toBe(
|
||||
'Action is configured, but binary is not available (zapret2 -> zapret2): /opt/zapret2/nfq2/nfqws2',
|
||||
);
|
||||
});
|
||||
|
||||
it('formats empty or unknown string', () => {
|
||||
expect(formatServiceStatusMessage('')).toBe('');
|
||||
expect(formatServiceStatusMessage('Random error')).toBe('Random error');
|
||||
});
|
||||
});
|
||||
@@ -238,6 +238,13 @@ export interface StoreType {
|
||||
tailscale_backup_version?: string;
|
||||
tailscale_backup_time?: number;
|
||||
server_inbounds_enabled_count: number;
|
||||
direct_bypass_enabled?: number;
|
||||
direct_bypass_address?: string;
|
||||
direct_bypass_port?: string;
|
||||
torrserver_running?: number;
|
||||
torrserver_direct_available?: number;
|
||||
torrserver_direct_enabled?: number;
|
||||
torrserver_direct_active?: number;
|
||||
openwrt_version: string;
|
||||
device_model: string;
|
||||
};
|
||||
@@ -270,6 +277,10 @@ export interface StoreType {
|
||||
tailscaleInstall: { loading: boolean };
|
||||
tailscaleRemove: { loading: boolean };
|
||||
tailscaleRollback: { loading: boolean };
|
||||
directBypassEnable: { loading: boolean };
|
||||
directBypassDisable: { loading: boolean };
|
||||
torrserverDirectEnable: { loading: boolean };
|
||||
torrserverDirectDisable: { loading: boolean };
|
||||
};
|
||||
updatesChecks: Record<
|
||||
Tachyon.ComponentName,
|
||||
|
||||
@@ -41,6 +41,10 @@ function getEmptyUpdatesActions(): StoreType['updatesActions'] {
|
||||
tailscaleInstall: { loading: false },
|
||||
tailscaleRemove: { loading: false },
|
||||
tailscaleRollback: { loading: false },
|
||||
directBypassEnable: { loading: false },
|
||||
directBypassDisable: { loading: false },
|
||||
torrserverDirectEnable: { loading: false },
|
||||
torrserverDirectDisable: { loading: false },
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import { isCopyableProxyLink, svgEl } from '../../../../helpers';
|
||||
import { prettyBytes } from '../../../../helpers/prettyBytes';
|
||||
import { Tachyon } from '../../../types';
|
||||
import { renderFlagEmojis } from './renderFlagEmojis';
|
||||
import { formatServiceStatusMessage } from '../../../helpers/formatServiceStatusMessage';
|
||||
|
||||
interface IRenderSectionsProps {
|
||||
loading: boolean;
|
||||
@@ -723,7 +724,7 @@ function renderDefaultState({
|
||||
style:
|
||||
'padding: 0 16px 8px; font-size: 12px; opacity: 0.6; word-break: break-word;',
|
||||
},
|
||||
ss.statusMessage,
|
||||
formatServiceStatusMessage(ss.statusMessage),
|
||||
)
|
||||
: '',
|
||||
]);
|
||||
|
||||
@@ -107,6 +107,13 @@ export const initialDiagnosticStore: Pick<
|
||||
tailscale_backup_version: '',
|
||||
tailscale_backup_time: 0,
|
||||
server_inbounds_enabled_count: -1,
|
||||
direct_bypass_enabled: 0,
|
||||
direct_bypass_address: '',
|
||||
direct_bypass_port: '',
|
||||
torrserver_running: 0,
|
||||
torrserver_direct_available: 0,
|
||||
torrserver_direct_enabled: 0,
|
||||
torrserver_direct_active: 0,
|
||||
openwrt_version: 'loading',
|
||||
device_model: 'loading',
|
||||
},
|
||||
@@ -179,6 +186,10 @@ export const initialDiagnosticStore: Pick<
|
||||
tailscaleInstall: { loading: false },
|
||||
tailscaleRemove: { loading: false },
|
||||
tailscaleRollback: { loading: false },
|
||||
directBypassEnable: { loading: false },
|
||||
directBypassDisable: { loading: false },
|
||||
torrserverDirectEnable: { loading: false },
|
||||
torrserverDirectDisable: { loading: false },
|
||||
},
|
||||
updatesChecks: {
|
||||
tachyon: { status: null, latest_version: '', release_url: '' },
|
||||
@@ -187,5 +198,7 @@ export const initialDiagnosticStore: Pick<
|
||||
zapret2: { status: null, latest_version: '', release_url: '' },
|
||||
byedpi: { status: null, latest_version: '', release_url: '' },
|
||||
tailscale: { status: null, latest_version: '', release_url: '' },
|
||||
direct_bypass: { status: null, latest_version: '', release_url: '' },
|
||||
torrserver_direct: { status: null, latest_version: '', release_url: '' },
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { onMount, preserveScrollForPage } from '../../../helpers';
|
||||
import { copyToClipboard } from '../../../helpers/copyToClipboard';
|
||||
import { TACHYON_ACTION_PROVIDERS_AVAILABILITY_EVENT } from '../../../constants';
|
||||
import { normalizeCompiledVersion } from '../../../helpers/normalizeCompiledVersion';
|
||||
import { capSetSize } from '../../helpers/capCollectionSize';
|
||||
import { showToast } from '../../../helpers/showToast';
|
||||
import {
|
||||
renderCopyIcon24,
|
||||
renderDownloadIcon24,
|
||||
renderGlobeIcon24,
|
||||
renderRotateCcwIcon24,
|
||||
@@ -106,6 +108,7 @@ interface ComponentActionButton {
|
||||
component: Tachyon.ComponentName;
|
||||
action: Tachyon.ComponentAction;
|
||||
targetVersion?: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
interface ComponentCard {
|
||||
@@ -119,6 +122,7 @@ interface ComponentCard {
|
||||
actions: ComponentActionButton[];
|
||||
badgeNode?: Node | null;
|
||||
supportsVersions?: boolean;
|
||||
copyValue?: string;
|
||||
}
|
||||
|
||||
let updatesLifecycleRegistered = false;
|
||||
@@ -523,6 +527,16 @@ function patchSystemInfoAfterMutation(result: Tachyon.ComponentActionResult) {
|
||||
}
|
||||
}
|
||||
|
||||
if (result.component === 'direct_bypass') {
|
||||
nextSystemInfo.direct_bypass_enabled = result.action === 'enable' ? 1 : 0;
|
||||
}
|
||||
if (result.component === 'torrserver_direct') {
|
||||
nextSystemInfo.torrserver_direct_enabled =
|
||||
result.action === 'enable' ? 1 : 0;
|
||||
nextSystemInfo.torrserver_direct_active =
|
||||
result.action === 'enable' ? 1 : 0;
|
||||
}
|
||||
|
||||
const normalizedSystemInfo = normalizeSingBoxVariantFields(nextSystemInfo);
|
||||
|
||||
store.set({
|
||||
@@ -1031,6 +1045,8 @@ function getComponentInstallKey(
|
||||
return 'byedpiInstall';
|
||||
case 'tailscale':
|
||||
return 'tailscaleInstall';
|
||||
default:
|
||||
return 'tachyonInstall';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1136,6 +1152,8 @@ const COMPONENT_REPO_URLS: Record<Tachyon.ComponentName, string> = {
|
||||
zapret2: 'https://github.com/Dushnilin/zapret2-openwrt',
|
||||
byedpi: 'https://github.com/DPITrickster/ByeDPI-OpenWrt',
|
||||
tailscale: 'https://openwrt.org/packages/pkgdata/tailscale',
|
||||
direct_bypass: '',
|
||||
torrserver_direct: '',
|
||||
};
|
||||
|
||||
function getComponentCards(): ComponentCard[] {
|
||||
@@ -1266,6 +1284,54 @@ function getComponentCards(): ComponentCard[] {
|
||||
rollbackKey: 'tailscaleRollback',
|
||||
});
|
||||
|
||||
const directBypassEnabled = Boolean(systemInfo.direct_bypass_enabled);
|
||||
const directBypassEndpoint = systemInfo.direct_bypass_address
|
||||
? `${systemInfo.direct_bypass_address}:${systemInfo.direct_bypass_port || '2080'}`
|
||||
: '';
|
||||
const torrserverRunning = Boolean(systemInfo.torrserver_running);
|
||||
const torrserverDirectAvailable = Boolean(
|
||||
systemInfo.torrserver_direct_available,
|
||||
);
|
||||
const torrserverDirectEnabled = Boolean(systemInfo.torrserver_direct_enabled);
|
||||
const torrserverDirectActive = Boolean(systemInfo.torrserver_direct_active);
|
||||
|
||||
const directBypassActions: ComponentActionButton[] = [
|
||||
directBypassEnabled
|
||||
? {
|
||||
key: 'directBypassDisable' as const,
|
||||
text: _('Disable'),
|
||||
icon: renderXIcon24,
|
||||
component: 'direct_bypass' as const,
|
||||
action: 'disable' as const,
|
||||
}
|
||||
: {
|
||||
key: 'directBypassEnable' as const,
|
||||
text: _('Enable'),
|
||||
icon: renderRotateCcwIcon24,
|
||||
component: 'direct_bypass' as const,
|
||||
action: 'enable' as const,
|
||||
},
|
||||
];
|
||||
|
||||
const torrserverDirectActions: ComponentActionButton[] = [
|
||||
torrserverDirectEnabled
|
||||
? {
|
||||
key: 'torrserverDirectDisable' as const,
|
||||
text: _('Disable'),
|
||||
icon: renderXIcon24,
|
||||
component: 'torrserver_direct' as const,
|
||||
action: 'disable' as const,
|
||||
}
|
||||
: {
|
||||
key: 'torrserverDirectEnable' as const,
|
||||
text: _('Enable'),
|
||||
icon: renderRotateCcwIcon24,
|
||||
component: 'torrserver_direct' as const,
|
||||
action: 'enable' as const,
|
||||
disabled: !torrserverDirectAvailable,
|
||||
},
|
||||
];
|
||||
|
||||
return [
|
||||
{
|
||||
component: 'tachyon',
|
||||
@@ -1305,6 +1371,34 @@ function getComponentCards(): ComponentCard[] {
|
||||
actions: singBoxActions,
|
||||
supportsVersions: true,
|
||||
},
|
||||
{
|
||||
component: 'direct_bypass',
|
||||
column: 0,
|
||||
title: _('Direct Proxy'),
|
||||
version: directBypassEnabled
|
||||
? `HTTP/SOCKS5 · ${directBypassEndpoint || _('Enabled')}`
|
||||
: _('Disabled'),
|
||||
copyValue:
|
||||
directBypassEnabled && directBypassEndpoint
|
||||
? directBypassEndpoint
|
||||
: undefined,
|
||||
actions: directBypassActions,
|
||||
},
|
||||
{
|
||||
component: 'torrserver_direct',
|
||||
column: 0,
|
||||
title: _('TorrServer Direct'),
|
||||
version: !torrserverRunning
|
||||
? _('TorrServer not found')
|
||||
: !torrserverDirectAvailable
|
||||
? _('Dedicated cgroup unavailable')
|
||||
: torrserverDirectEnabled && torrserverDirectActive
|
||||
? _('Enabled')
|
||||
: torrserverDirectEnabled
|
||||
? _('Waiting for TorrServer')
|
||||
: _('Disabled'),
|
||||
actions: torrserverDirectActions,
|
||||
},
|
||||
{
|
||||
component: 'zapret',
|
||||
column: 1,
|
||||
@@ -1677,7 +1771,10 @@ function renderComponentCard(card: ComponentCard) {
|
||||
text: action.text,
|
||||
icon: action.icon,
|
||||
loading,
|
||||
disabled: systemInfoLoading || (anyActionLoading && !loading),
|
||||
disabled:
|
||||
action.disabled ||
|
||||
systemInfoLoading ||
|
||||
(anyActionLoading && !loading),
|
||||
onClick: () => void handleComponentAction(action),
|
||||
});
|
||||
});
|
||||
@@ -1690,7 +1787,10 @@ function renderComponentCard(card: ComponentCard) {
|
||||
text: action.text,
|
||||
icon: action.icon,
|
||||
loading,
|
||||
disabled: systemInfoLoading || (anyActionLoading && !loading),
|
||||
disabled:
|
||||
action.disabled ||
|
||||
systemInfoLoading ||
|
||||
(anyActionLoading && !loading),
|
||||
onClick: () => void handleComponentAction(action),
|
||||
});
|
||||
});
|
||||
@@ -1704,6 +1804,19 @@ function renderComponentCard(card: ComponentCard) {
|
||||
);
|
||||
}
|
||||
|
||||
if (card.copyValue) {
|
||||
actionElements.push(
|
||||
E('div', { class: 'tachyon_updates-page__component__actions-main' }, [
|
||||
renderButton({
|
||||
text: _('Copy address'),
|
||||
icon: renderCopyIcon24,
|
||||
disabled: anyActionLoading,
|
||||
onClick: () => copyToClipboard(card.copyValue || ''),
|
||||
}),
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
if (variantActions.length > 0) {
|
||||
const variantButtons = variantActions.map((action) => {
|
||||
const loading = updatesActions[action.key].loading;
|
||||
|
||||
@@ -596,6 +596,13 @@ export namespace Tachyon {
|
||||
byedpi_installed: 0 | 1;
|
||||
tailscale_version: string;
|
||||
tailscale_installed: 0 | 1;
|
||||
direct_bypass_enabled?: 0 | 1;
|
||||
direct_bypass_address?: string;
|
||||
direct_bypass_port?: string;
|
||||
torrserver_running?: 0 | 1;
|
||||
torrserver_direct_available?: 0 | 1;
|
||||
torrserver_direct_enabled?: 0 | 1;
|
||||
torrserver_direct_active?: 0 | 1;
|
||||
openwrt_version: string;
|
||||
device_model: string;
|
||||
generated_at?: number;
|
||||
@@ -677,7 +684,9 @@ export namespace Tachyon {
|
||||
| 'zapret'
|
||||
| 'zapret2'
|
||||
| 'byedpi'
|
||||
| 'tailscale';
|
||||
| 'tailscale'
|
||||
| 'direct_bypass'
|
||||
| 'torrserver_direct';
|
||||
|
||||
export type ComponentAction =
|
||||
| 'check_update'
|
||||
@@ -690,7 +699,9 @@ export namespace Tachyon {
|
||||
| 'install_extended_compressed'
|
||||
| 'install_lx'
|
||||
| 'install_tiny'
|
||||
| 'install_stable';
|
||||
| 'install_stable'
|
||||
| 'enable'
|
||||
| 'disable';
|
||||
|
||||
export interface ComponentActionResult {
|
||||
success: boolean;
|
||||
|
||||
@@ -2123,6 +2123,84 @@ function prettyBytes(n) {
|
||||
return n + " " + unit;
|
||||
}
|
||||
|
||||
// src/tachyon/helpers/formatServiceStatusMessage.ts
|
||||
function formatServiceStatusMessage(msg) {
|
||||
if (!msg) return "";
|
||||
const normalMatch = msg.match(/^([a-zA-Z0-9_-]+) provider status is normal$/);
|
||||
if (normalMatch) {
|
||||
const provider = normalMatch[1];
|
||||
return `${_("Provider status is normal")}: ${provider}`;
|
||||
}
|
||||
const notReadyWithDetailMatch = msg.match(
|
||||
/^action=([^ ]+) is configured, but the Tachyon-managed ([^ ]+) runtime is not ready \((.+)\)$/
|
||||
);
|
||||
if (notReadyWithDetailMatch) {
|
||||
const [, action, bin, detail] = notReadyWithDetailMatch;
|
||||
return `${_("Action is configured, but runtime is not ready")} (${action} -> ${bin}): ${detail}`;
|
||||
}
|
||||
const notReadyMatch = msg.match(
|
||||
/^action=([^ ]+) is configured, but the Tachyon-managed ([^ ]+) runtime is not ready$/
|
||||
);
|
||||
if (notReadyMatch) {
|
||||
const [, action, bin] = notReadyMatch;
|
||||
return `${_("Action is configured, but runtime is not ready")} (${action} -> ${bin})`;
|
||||
}
|
||||
const notAvailablePathMatch = msg.match(
|
||||
/^action=([^ ]+) is configured, but (.+) (?:provider|ciadpi) is not available at (.+)$/
|
||||
);
|
||||
if (notAvailablePathMatch) {
|
||||
const [, action, provider, path] = notAvailablePathMatch;
|
||||
return `${_("Action is configured, but binary is not available")} (${action} -> ${provider}): ${path}`;
|
||||
}
|
||||
const overlapMatch = msg.match(
|
||||
/^external NFQUEUE rules overlap with the Tachyon (.+) range (.+)$/
|
||||
);
|
||||
if (overlapMatch) {
|
||||
const [, provider, range] = overlapMatch;
|
||||
return `${_("External NFQUEUE rules overlap with Tachyon")} ${provider} (${range})`;
|
||||
}
|
||||
if (msg === "legacy zapret runtime paths are still present and should be migrated") {
|
||||
return _("Legacy zapret runtime paths are still present and should be migrated");
|
||||
}
|
||||
const unexpectedProcessesMatch = msg.match(
|
||||
/^unexpected Tachyon-managed ([^ ]+) processes are running without matching action=([^ ]+) rules$/
|
||||
);
|
||||
if (unexpectedProcessesMatch) {
|
||||
const [, bin, action] = unexpectedProcessesMatch;
|
||||
return `${_("Unexpected background processes")} (${bin}, action=${action})`;
|
||||
}
|
||||
if (msg.startsWith("standalone ")) {
|
||||
return _("Standalone service is active alongside Tachyon; policy or port conflicts are possible");
|
||||
}
|
||||
const packageInstalledNoBinMatch = msg.match(
|
||||
/^(.+) package is installed, but (?:the provider binary|ciadpi) is not available at (.+)$/
|
||||
);
|
||||
if (packageInstalledNoBinMatch) {
|
||||
const [, provider, path] = packageInstalledNoBinMatch;
|
||||
return `${_("Package is installed, but binary missing")} (${provider}): ${path}`;
|
||||
}
|
||||
const notInstalledMatch = msg.match(
|
||||
/^(.+) (?:provider|package) is not installed; (?:action=([^ ]+) is unavailable|native Tailscale is unavailable)$/
|
||||
);
|
||||
if (notInstalledMatch) {
|
||||
const [, provider, action] = notInstalledMatch;
|
||||
return `${_("Package is not installed")} (${provider})${action ? `: action=${action} ${_("is unavailable")}` : ""}`;
|
||||
}
|
||||
if (msg.includes("ciadpi has restarted after exiting")) {
|
||||
return _("ByeDPI restarted after exiting; strategy or traffic load may be unstable");
|
||||
}
|
||||
if (msg.includes("native Tailscale is configured, but the tailscale package is missing")) {
|
||||
return _("Native Tailscale is configured, but tailscale package is missing");
|
||||
}
|
||||
if (msg.includes("native Tailscale is configured, but tailscaled is not running")) {
|
||||
return _("Native Tailscale is configured, but tailscaled is not running for every section");
|
||||
}
|
||||
if (msg.includes("no server section uses native mode")) {
|
||||
return _("Tailscale package is installed, but no server section uses native mode");
|
||||
}
|
||||
return _(msg);
|
||||
}
|
||||
|
||||
// src/tachyon/tabs/dashboard/partials/renderSections.ts
|
||||
function renderFailedState() {
|
||||
return E(
|
||||
@@ -2649,7 +2727,7 @@ function renderDefaultState({
|
||||
{
|
||||
style: "padding: 0 16px 8px; font-size: 12px; opacity: 0.6; word-break: break-word;"
|
||||
},
|
||||
ss.statusMessage
|
||||
formatServiceStatusMessage(ss.statusMessage)
|
||||
) : ""
|
||||
]);
|
||||
}
|
||||
@@ -5947,6 +6025,13 @@ var initialDiagnosticStore = {
|
||||
tailscale_backup_version: "",
|
||||
tailscale_backup_time: 0,
|
||||
server_inbounds_enabled_count: -1,
|
||||
direct_bypass_enabled: 0,
|
||||
direct_bypass_address: "",
|
||||
direct_bypass_port: "",
|
||||
torrserver_running: 0,
|
||||
torrserver_direct_available: 0,
|
||||
torrserver_direct_enabled: 0,
|
||||
torrserver_direct_active: 0,
|
||||
openwrt_version: "loading",
|
||||
device_model: "loading"
|
||||
},
|
||||
@@ -6018,7 +6103,11 @@ var initialDiagnosticStore = {
|
||||
tailscaleCheck: { loading: false },
|
||||
tailscaleInstall: { loading: false },
|
||||
tailscaleRemove: { loading: false },
|
||||
tailscaleRollback: { loading: false }
|
||||
tailscaleRollback: { loading: false },
|
||||
directBypassEnable: { loading: false },
|
||||
directBypassDisable: { loading: false },
|
||||
torrserverDirectEnable: { loading: false },
|
||||
torrserverDirectDisable: { loading: false }
|
||||
},
|
||||
updatesChecks: {
|
||||
tachyon: { status: null, latest_version: "", release_url: "" },
|
||||
@@ -6026,7 +6115,9 @@ var initialDiagnosticStore = {
|
||||
zapret: { status: null, latest_version: "", release_url: "" },
|
||||
zapret2: { status: null, latest_version: "", release_url: "" },
|
||||
byedpi: { status: null, latest_version: "", release_url: "" },
|
||||
tailscale: { status: null, latest_version: "", release_url: "" }
|
||||
tailscale: { status: null, latest_version: "", release_url: "" },
|
||||
direct_bypass: { status: null, latest_version: "", release_url: "" },
|
||||
torrserver_direct: { status: null, latest_version: "", release_url: "" }
|
||||
}
|
||||
};
|
||||
|
||||
@@ -6403,7 +6494,11 @@ var componentActionKeyMap = {
|
||||
"tailscale:install": "tailscaleInstall",
|
||||
"tailscale:install_version": "tailscaleInstall",
|
||||
"tailscale:remove": "tailscaleRemove",
|
||||
"tailscale:rollback": "tailscaleRollback"
|
||||
"tailscale:rollback": "tailscaleRollback",
|
||||
"direct_bypass:enable": "directBypassEnable",
|
||||
"direct_bypass:disable": "directBypassDisable",
|
||||
"torrserver_direct:enable": "torrserverDirectEnable",
|
||||
"torrserver_direct:disable": "torrserverDirectDisable"
|
||||
};
|
||||
function getComponentActionKey(component, action) {
|
||||
return componentActionKeyMap[`${component}:${action}`];
|
||||
@@ -6573,7 +6668,11 @@ function getEmptyUpdatesActions() {
|
||||
tailscaleCheck: { loading: false },
|
||||
tailscaleInstall: { loading: false },
|
||||
tailscaleRemove: { loading: false },
|
||||
tailscaleRollback: { loading: false }
|
||||
tailscaleRollback: { loading: false },
|
||||
directBypassEnable: { loading: false },
|
||||
directBypassDisable: { loading: false },
|
||||
torrserverDirectEnable: { loading: false },
|
||||
torrserverDirectDisable: { loading: false }
|
||||
};
|
||||
}
|
||||
function getEmptyDiagnosticsActions() {
|
||||
@@ -19967,6 +20066,13 @@ function patchSystemInfoAfterMutation(result) {
|
||||
nextSystemInfo.byedpi_version = version;
|
||||
}
|
||||
}
|
||||
if (result.component === "direct_bypass") {
|
||||
nextSystemInfo.direct_bypass_enabled = result.action === "enable" ? 1 : 0;
|
||||
}
|
||||
if (result.component === "torrserver_direct") {
|
||||
nextSystemInfo.torrserver_direct_enabled = result.action === "enable" ? 1 : 0;
|
||||
nextSystemInfo.torrserver_direct_active = result.action === "enable" ? 1 : 0;
|
||||
}
|
||||
const normalizedSystemInfo = normalizeSingBoxVariantFields(nextSystemInfo);
|
||||
store.set({
|
||||
diagnosticsSystemInfo: normalizedSystemInfo
|
||||
@@ -20351,6 +20457,8 @@ function getComponentInstallKey(component) {
|
||||
return "byedpiInstall";
|
||||
case "tailscale":
|
||||
return "tailscaleInstall";
|
||||
default:
|
||||
return "tachyonInstall";
|
||||
}
|
||||
}
|
||||
function getComponentInstallAction(component) {
|
||||
@@ -20427,7 +20535,9 @@ var COMPONENT_REPO_URLS = {
|
||||
zapret: "https://github.com/remittor/zapret-openwrt",
|
||||
zapret2: "https://github.com/Dushnilin/zapret2-openwrt",
|
||||
byedpi: "https://github.com/DPITrickster/ByeDPI-OpenWrt",
|
||||
tailscale: "https://openwrt.org/packages/pkgdata/tailscale"
|
||||
tailscale: "https://openwrt.org/packages/pkgdata/tailscale",
|
||||
direct_bypass: "",
|
||||
torrserver_direct: ""
|
||||
};
|
||||
function getComponentCards() {
|
||||
const systemInfo = normalizeSingBoxVariantFields(
|
||||
@@ -20543,6 +20653,45 @@ function getComponentCards() {
|
||||
removeKey: "tailscaleRemove",
|
||||
rollbackKey: "tailscaleRollback"
|
||||
});
|
||||
const directBypassEnabled = Boolean(systemInfo.direct_bypass_enabled);
|
||||
const directBypassEndpoint = systemInfo.direct_bypass_address ? `${systemInfo.direct_bypass_address}:${systemInfo.direct_bypass_port || "2080"}` : "";
|
||||
const torrserverRunning = Boolean(systemInfo.torrserver_running);
|
||||
const torrserverDirectAvailable = Boolean(
|
||||
systemInfo.torrserver_direct_available
|
||||
);
|
||||
const torrserverDirectEnabled = Boolean(systemInfo.torrserver_direct_enabled);
|
||||
const torrserverDirectActive = Boolean(systemInfo.torrserver_direct_active);
|
||||
const directBypassActions = [
|
||||
directBypassEnabled ? {
|
||||
key: "directBypassDisable",
|
||||
text: _("Disable"),
|
||||
icon: renderXIcon24,
|
||||
component: "direct_bypass",
|
||||
action: "disable"
|
||||
} : {
|
||||
key: "directBypassEnable",
|
||||
text: _("Enable"),
|
||||
icon: renderRotateCcwIcon24,
|
||||
component: "direct_bypass",
|
||||
action: "enable"
|
||||
}
|
||||
];
|
||||
const torrserverDirectActions = [
|
||||
torrserverDirectEnabled ? {
|
||||
key: "torrserverDirectDisable",
|
||||
text: _("Disable"),
|
||||
icon: renderXIcon24,
|
||||
component: "torrserver_direct",
|
||||
action: "disable"
|
||||
} : {
|
||||
key: "torrserverDirectEnable",
|
||||
text: _("Enable"),
|
||||
icon: renderRotateCcwIcon24,
|
||||
component: "torrserver_direct",
|
||||
action: "enable",
|
||||
disabled: !torrserverDirectAvailable
|
||||
}
|
||||
];
|
||||
return [
|
||||
{
|
||||
component: "tachyon",
|
||||
@@ -20570,6 +20719,21 @@ function getComponentCards() {
|
||||
actions: singBoxActions,
|
||||
supportsVersions: true
|
||||
},
|
||||
{
|
||||
component: "direct_bypass",
|
||||
column: 0,
|
||||
title: _("Direct Proxy"),
|
||||
version: directBypassEnabled ? `HTTP/SOCKS5 \xB7 ${directBypassEndpoint || _("Enabled")}` : _("Disabled"),
|
||||
copyValue: directBypassEnabled && directBypassEndpoint ? directBypassEndpoint : void 0,
|
||||
actions: directBypassActions
|
||||
},
|
||||
{
|
||||
component: "torrserver_direct",
|
||||
column: 0,
|
||||
title: _("TorrServer Direct"),
|
||||
version: !torrserverRunning ? _("TorrServer not found") : !torrserverDirectAvailable ? _("Dedicated cgroup unavailable") : torrserverDirectEnabled && torrserverDirectActive ? _("Enabled") : torrserverDirectEnabled ? _("Waiting for TorrServer") : _("Disabled"),
|
||||
actions: torrserverDirectActions
|
||||
},
|
||||
{
|
||||
component: "zapret",
|
||||
column: 1,
|
||||
@@ -20884,7 +21048,7 @@ function renderComponentCard(card) {
|
||||
text: action.text,
|
||||
icon: action.icon,
|
||||
loading: loading2,
|
||||
disabled: systemInfoLoading || anyActionLoading && !loading2,
|
||||
disabled: action.disabled || systemInfoLoading || anyActionLoading && !loading2,
|
||||
onClick: () => void handleComponentAction(action)
|
||||
});
|
||||
});
|
||||
@@ -20895,7 +21059,7 @@ function renderComponentCard(card) {
|
||||
text: action.text,
|
||||
icon: action.icon,
|
||||
loading: loading2,
|
||||
disabled: systemInfoLoading || anyActionLoading && !loading2,
|
||||
disabled: action.disabled || systemInfoLoading || anyActionLoading && !loading2,
|
||||
onClick: () => void handleComponentAction(action)
|
||||
});
|
||||
});
|
||||
@@ -20907,6 +21071,18 @@ function renderComponentCard(card) {
|
||||
])
|
||||
);
|
||||
}
|
||||
if (card.copyValue) {
|
||||
actionElements.push(
|
||||
E("div", { class: "tachyon_updates-page__component__actions-main" }, [
|
||||
renderButton({
|
||||
text: _("Copy address"),
|
||||
icon: renderCopyIcon24,
|
||||
disabled: anyActionLoading,
|
||||
onClick: () => copyToClipboard(card.copyValue || "")
|
||||
})
|
||||
])
|
||||
);
|
||||
}
|
||||
if (variantActions.length > 0) {
|
||||
const variantButtons = variantActions.map((action) => {
|
||||
const loading2 = updatesActions[action.key].loading;
|
||||
|
||||
@@ -25,6 +25,8 @@ config settings 'settings'
|
||||
#option badwan_reload_delay '2000'
|
||||
option enable_yacd '0'
|
||||
option disable_quic '0'
|
||||
option direct_bypass_enabled '0'
|
||||
option direct_bypass_port '2080'
|
||||
option list_update_enabled '1'
|
||||
option update_interval '1d'
|
||||
option download_all_presets '0'
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
#!/bin/sh /etc/rc.common
|
||||
|
||||
START=100
|
||||
STOP=9
|
||||
USE_PROCD=1
|
||||
|
||||
start_service() {
|
||||
[ "$(uci -q get tachyon.settings.torrserver_direct_enabled)" = "1" ] || return 0
|
||||
procd_open_instance
|
||||
procd_set_param command /usr/bin/ucode -L /usr/lib/tachyon /usr/lib/tachyon/torrserver/direct.uc worker
|
||||
procd_set_param respawn 3600 5 5
|
||||
procd_set_param stdout 1
|
||||
procd_set_param stderr 1
|
||||
procd_close_instance
|
||||
}
|
||||
|
||||
stop_service() {
|
||||
/usr/bin/ucode -L /usr/lib/tachyon /usr/lib/tachyon/torrserver/direct.uc remove >/dev/null 2>&1 || true
|
||||
}
|
||||
@@ -2785,6 +2785,10 @@ function normalize_component_name(component) {
|
||||
return "sing_box";
|
||||
if (component == "tachyon")
|
||||
return "tachyon";
|
||||
if (component == "direct-bypass" || component == "directbypass" || component == "direct_proxy")
|
||||
return "direct_bypass";
|
||||
if (component == "torrserver_direct" || component == "torrserver-direct")
|
||||
return "torrserver_direct";
|
||||
return component;
|
||||
}
|
||||
|
||||
@@ -3134,6 +3138,86 @@ function install_component_version(component, tag) {
|
||||
}
|
||||
}
|
||||
|
||||
function set_direct_bypass(action) {
|
||||
let enable = action == "enable";
|
||||
let cursor = uci_core.cursor();
|
||||
cursor.load("tachyon");
|
||||
|
||||
if (enable) {
|
||||
cursor.set("tachyon", "settings", "direct_bypass_enabled", "1");
|
||||
let current_port = trim(as_string(cursor.get("tachyon", "settings", "direct_bypass_port") || ""));
|
||||
if (current_port == "" || match(current_port, /^[0-9]+$/) == null)
|
||||
cursor.set("tachyon", "settings", "direct_bypass_port", "2080");
|
||||
} else {
|
||||
cursor.set("tachyon", "settings", "direct_bypass_enabled", "0");
|
||||
}
|
||||
cursor.commit("tachyon");
|
||||
|
||||
remove_file(SYSTEM_INFO_CACHE_FILE);
|
||||
restart_tachyon_after_successful_change();
|
||||
|
||||
action_success(
|
||||
"direct_bypass",
|
||||
action,
|
||||
enable ? "Direct bypass enabled" : "Direct bypass disabled",
|
||||
enable ? "enabled" : "disabled",
|
||||
enable ? "enabled" : "disabled",
|
||||
1
|
||||
);
|
||||
}
|
||||
|
||||
const TORRSERVER_DIRECT_INIT = getenv("TACHYON_TORRSERVER_DIRECT_INIT") || "/etc/init.d/tachyon-torrserver-direct";
|
||||
const TORRSERVER_DIRECT_UC = LIB_DIR + "/torrserver/direct.uc";
|
||||
|
||||
function set_torrserver_direct(action) {
|
||||
let cursor = uci_core.cursor();
|
||||
cursor.load("tachyon");
|
||||
let current_enabled = trim(as_string(cursor.get("tachyon", "settings", "torrserver_direct_enabled") || "0")) == "1" ? "1" : "0";
|
||||
let target_enabled = action == "enable" ? "1" : "0";
|
||||
|
||||
if (!file_exists(TORRSERVER_DIRECT_INIT) || !file_exists(TORRSERVER_DIRECT_UC))
|
||||
action_fail("torrserver_direct", action, "TorrServer Direct service is not available", current_enabled, target_enabled);
|
||||
|
||||
if (target_enabled == "1") {
|
||||
if (!command_success_from_args([ "modprobe", "nft_socket" ]) &&
|
||||
(!run_logged("Installing TorrServer Direct kernel support", pkg_install_name_command("kmod-nft-socket")) ||
|
||||
!command_success_from_args([ "modprobe", "nft_socket" ])))
|
||||
action_fail("torrserver_direct", action, "This firmware does not provide kmod-nft-socket required for TorrServer Direct", current_enabled, target_enabled);
|
||||
let status = null;
|
||||
try {
|
||||
status = json(command_output_from_args([ "ucode", "-L", LIB_DIR, TORRSERVER_DIRECT_UC, "status" ]));
|
||||
} catch (e) {}
|
||||
if (type(status) != "object" || int(status.running || 0) != 1)
|
||||
action_fail("torrserver_direct", action, "TorrServer is not running", current_enabled, target_enabled);
|
||||
if (int(status.available || 0) != 1)
|
||||
action_fail("torrserver_direct", action, "TorrServer does not have a dedicated cgroup", current_enabled, target_enabled);
|
||||
}
|
||||
|
||||
cursor.set("tachyon", "settings", "torrserver_direct_enabled", target_enabled);
|
||||
cursor.commit("tachyon");
|
||||
|
||||
let applied = target_enabled == "1"
|
||||
? command_success_from_args([ TORRSERVER_DIRECT_INIT, "enable" ]) &&
|
||||
command_success_from_args([ TORRSERVER_DIRECT_INIT, "restart" ]) &&
|
||||
command_success_from_args([ "ucode", "-L", LIB_DIR, TORRSERVER_DIRECT_UC, "reconcile" ])
|
||||
: command_success_from_args([ TORRSERVER_DIRECT_INIT, "stop" ]) &&
|
||||
command_success_from_args([ TORRSERVER_DIRECT_INIT, "disable" ]);
|
||||
if (!applied) {
|
||||
cursor.set("tachyon", "settings", "torrserver_direct_enabled", current_enabled);
|
||||
cursor.commit("tachyon");
|
||||
action_fail("torrserver_direct", action, "Failed to apply TorrServer Direct settings", current_enabled, target_enabled);
|
||||
}
|
||||
remove_file(SYSTEM_INFO_CACHE_FILE);
|
||||
action_success(
|
||||
"torrserver_direct",
|
||||
action,
|
||||
target_enabled == "1" ? "TorrServer Direct has been enabled" : "TorrServer Direct has been disabled",
|
||||
target_enabled == "1" ? "enabled" : "disabled",
|
||||
target_enabled == "1" ? "enabled" : "disabled",
|
||||
current_enabled == target_enabled ? 0 : 1
|
||||
);
|
||||
}
|
||||
|
||||
function component_action(component, action, extra) {
|
||||
component = normalize_component_name(component);
|
||||
action = as_string(action);
|
||||
@@ -3160,7 +3244,7 @@ function component_action(component, action, extra) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (action != "check_update" && action != "remove") {
|
||||
if (action != "check_update" && action != "remove" && component != "direct_bypass" && component != "torrserver_direct") {
|
||||
create_component_backup(component);
|
||||
}
|
||||
|
||||
@@ -3191,6 +3275,10 @@ function component_action(component, action, extra) {
|
||||
install_tailscale(action);
|
||||
else if (component == "tailscale" && action == "remove")
|
||||
remove_optional_component("tailscale", "tailscale", "Tailscale", LIB_DIR + "/providers/tailscale/runtime.uc");
|
||||
else if (component == "direct_bypass" && (action == "enable" || action == "disable"))
|
||||
set_direct_bypass(action);
|
||||
else if (component == "torrserver_direct" && (action == "enable" || action == "disable"))
|
||||
set_torrserver_direct(action);
|
||||
else
|
||||
action_fail(component != "" ? component : "unknown", action != "" ? action : "unknown", "Unknown component action");
|
||||
}
|
||||
|
||||
@@ -638,6 +638,30 @@ function migrate_byedpi_cmd_opts(ctx, section) {
|
||||
delete_option(ctx, section, "cmd_opts");
|
||||
}
|
||||
|
||||
function migrate_ctrack_timeouts(ctx, section) {
|
||||
let action = migrated_rule_action(section);
|
||||
let opt_name = action == "zapret2" ? "nfqws2_opt" : (action == "zapret" ? "nfqws_opt" : "");
|
||||
if (opt_name == "")
|
||||
return;
|
||||
let opt = option(section, opt_name, "");
|
||||
if (opt == "" || index(opt, "--ctrack-timeouts=") < 0)
|
||||
return;
|
||||
let normalized = replace(opt, /--ctrack-timeouts=([0-9,]+)/g, function(full, numbers) {
|
||||
if (index(numbers, ",") < 0)
|
||||
return full;
|
||||
let parts = split(numbers, ",");
|
||||
if (length(parts) >= 8 && parts[0] == "30" && parts[2] == "600")
|
||||
return "--ctrack-timeouts=30:600:30:30";
|
||||
if (length(parts) >= 4)
|
||||
return "--ctrack-timeouts=" + parts[0] + ":" + parts[1] + ":" + parts[2] + ":" + parts[3];
|
||||
if (length(parts) >= 3)
|
||||
return "--ctrack-timeouts=" + parts[0] + ":" + parts[1] + ":" + parts[2];
|
||||
return "--ctrack-timeouts=30:600:30:30";
|
||||
});
|
||||
if (normalized != opt)
|
||||
set_option(ctx, section, opt_name, normalized);
|
||||
}
|
||||
|
||||
function migrate_zapret_nfqws_default(ctx, section, constants) {
|
||||
if (migrated_rule_action(section) != "zapret")
|
||||
return;
|
||||
@@ -1045,6 +1069,7 @@ function migrate_rule(ctx, section, converted_from_rule, constants) {
|
||||
|
||||
migrate_byedpi_cmd_opts(ctx, section);
|
||||
migrate_zapret_nfqws_default(ctx, section, constants);
|
||||
migrate_ctrack_timeouts(ctx, section);
|
||||
}
|
||||
|
||||
function migrate_rule_section(ctx, section, constants) {
|
||||
|
||||
@@ -885,7 +885,7 @@ function validate_dns_settings(settings, sections, context) {
|
||||
validate_required_duration_option(option(settings, "dns_check_interval", "10s"), "settings.dns_check_interval");
|
||||
validate_required_duration_option(option(settings, "dns_recovery_check_interval", "60s"), "settings.dns_recovery_check_interval");
|
||||
validate_required_duration_option(option(settings, "dns_check_timeout", "2s"), "settings.dns_check_timeout");
|
||||
for (let name in [ "dns_failure_threshold", "dns_recovery_threshold" ]) {
|
||||
for (let name in [ "dns_failure_threshold", "dns_recovery_threshold", "dns_failover_failure_threshold" ]) {
|
||||
let value = trim(option(settings, name, "3"));
|
||||
if (match(value, /^[0-9]+$/) == null || int(value) < 1 || int(value) > 10)
|
||||
fail_validation("Invalid " + name + " value '" + value + "'. Use a number from 1 to 10. Aborted.");
|
||||
|
||||
@@ -181,7 +181,7 @@ function constants_map() {
|
||||
c.ZAPRET_DESYNC_MARK = env("ZAPRET_DESYNC_MARK", "0x40000000");
|
||||
c.ZAPRET_DESYNC_MARK_POSTNAT = env("ZAPRET_DESYNC_MARK_POSTNAT", "0x20000000");
|
||||
c.ZAPRET_LEGACY_DEFAULT_NFQWS_OPT = env("ZAPRET_LEGACY_DEFAULT_NFQWS_OPT", "--filter-tcp=80 <HOSTLIST> --dpi-desync=fake,fakedsplit --dpi-desync-autottl=2 --dpi-desync-fooling=badsum --new --filter-tcp=443 --hostlist=/opt/zapret/ipset/zapret-hosts-google.txt --dpi-desync=fake,multidisorder --dpi-desync-split-pos=1,midsld --dpi-desync-repeats=11 --dpi-desync-fooling=badsum --dpi-desync-fake-tls-mod=rnd,dupsid,sni=www.google.com --new --filter-udp=443 --hostlist=/opt/zapret/ipset/zapret-hosts-google.txt --dpi-desync=fake --dpi-desync-repeats=11 --dpi-desync-fake-quic=/opt/zapret/files/fake/quic_initial_www_google_com.bin --new --filter-udp=443 <HOSTLIST_NOAUTO> --dpi-desync=fake --dpi-desync-repeats=11 --new --filter-tcp=443 <HOSTLIST> --dpi-desync=multidisorder --dpi-desync-split-pos=1,sniext+1,host+1,midsld-2,midsld,midsld+2,endhost-1");
|
||||
c.ZAPRET_DEFAULT_NFQWS_OPT = env("ZAPRET_DEFAULT_NFQWS_OPT", "--filter-tcp=80 --dpi-desync=fake,fakedsplit --dpi-desync-autottl=2 --dpi-desync-fooling=badsum --new --filter-tcp=443 --dpi-desync=fake,multidisorder --dpi-desync-split-pos=1,midsld --dpi-desync-repeats=11 --dpi-desync-fooling=badsum --dpi-desync-fake-tls-mod=rnd,dupsid,sni=www.google.com --new --filter-udp=443 --dpi-desync=fake --dpi-desync-repeats=11 --dpi-desync-fake-quic=/opt/zapret/files/fake/quic_initial_www_google_com.bin --ctrack-timeouts=30,30,600,30,30,30,30,30");
|
||||
c.ZAPRET_DEFAULT_NFQWS_OPT = env("ZAPRET_DEFAULT_NFQWS_OPT", "--filter-tcp=80 --dpi-desync=fake,fakedsplit --dpi-desync-autottl=2 --dpi-desync-fooling=badsum --new --filter-tcp=443 --dpi-desync=fake,multidisorder --dpi-desync-split-pos=1,midsld --dpi-desync-repeats=11 --dpi-desync-fooling=badsum --dpi-desync-fake-tls-mod=rnd,dupsid,sni=www.google.com --new --filter-udp=443 --dpi-desync=fake --dpi-desync-repeats=11 --dpi-desync-fake-quic=/opt/zapret/files/fake/quic_initial_www_google_com.bin --ctrack-timeouts=30:600:30:30");
|
||||
|
||||
c.ZAPRET2_PROVIDER_BASE_DIR = env("ZAPRET2_PROVIDER_BASE_DIR", "/opt/zapret2");
|
||||
c.ZAPRET2_PROVIDER_NFQWS2_BIN = env("ZAPRET2_PROVIDER_NFQWS2_BIN", c.ZAPRET2_PROVIDER_BASE_DIR + "/nfq2/nfqws2");
|
||||
@@ -199,7 +199,7 @@ function constants_map() {
|
||||
c.ZAPRET2_NFQWS2_RESPAWN_DELAY = env("ZAPRET2_NFQWS2_RESPAWN_DELAY", "5");
|
||||
c.ZAPRET2_DESYNC_MARK = env("ZAPRET2_DESYNC_MARK", "0x40000000");
|
||||
c.ZAPRET2_DESYNC_MARK_POSTNAT = env("ZAPRET2_DESYNC_MARK_POSTNAT", "0x20000000");
|
||||
c.ZAPRET2_DEFAULT_NFQWS2_OPT = env("ZAPRET2_DEFAULT_NFQWS2_OPT", "--filter-tcp=80 --filter-l7=http --payload=http_req --lua-desync=fake:blob=fake_default_http:tcp_md5 --lua-desync=multisplit:pos=method+2 --new --filter-tcp=443 --filter-l7=tls --payload=tls_client_hello --lua-desync=fake:blob=fake_default_tls:tcp_md5:tcp_seq=-10000 --lua-desync=multidisorder:pos=1,midsld --new --filter-udp=443 --filter-l7=quic --payload=quic_initial --lua-desync=fake:blob=fake_default_quic:repeats=6 --ctrack-timeouts=30,30,600,30,30,30,30,30");
|
||||
c.ZAPRET2_DEFAULT_NFQWS2_OPT = env("ZAPRET2_DEFAULT_NFQWS2_OPT", "--filter-tcp=80 --filter-l7=http --payload=http_req --lua-desync=fake:blob=fake_default_http:tcp_md5 --lua-desync=multisplit:pos=method+2 --new --filter-tcp=443 --filter-l7=tls --payload=tls_client_hello --lua-desync=fake:blob=fake_default_tls:tcp_md5:tcp_seq=-10000 --lua-desync=multidisorder:pos=1,midsld --new --filter-udp=443 --filter-l7=quic --payload=quic_initial --lua-desync=fake:blob=fake_default_quic:repeats=6 --ctrack-timeouts=30:600:30:30");
|
||||
|
||||
c.BYEDPI_BIN = env("BYEDPI_BIN", "/usr/bin/ciadpi");
|
||||
c.BYEDPI_SERVICE_INIT = env("BYEDPI_SERVICE_INIT", "/etc/init.d/byedpi");
|
||||
|
||||
@@ -1148,6 +1148,15 @@ function build_system_info() {
|
||||
let tailscale_version = tailscale_installed ? provider_version(TAILSCALE_RUNTIME_UC) : "not installed";
|
||||
let device_model = first_line_value("/tmp/sysinfo/model", "unknown");
|
||||
|
||||
let direct_bypass_enabled = bool_option(settings(), "direct_bypass_enabled", false) ? 1 : 0;
|
||||
let direct_bypass_port = option(settings(), "direct_bypass_port", "2080");
|
||||
let direct_bypass_address = direct_bypass_enabled
|
||||
? trim(command_output_from_args([ "ucode", "-L", LIB_DIR, LIB_DIR + "/singbox/runtime.uc", "service-listen-address" ]))
|
||||
: "";
|
||||
let torrserver_direct_status = parse_json_or_null(command_output_from_args([ "ucode", "-L", LIB_DIR, LIB_DIR + "/torrserver/direct.uc", "status" ]));
|
||||
if (type(torrserver_direct_status) != "object")
|
||||
torrserver_direct_status = {};
|
||||
|
||||
let base_bdir = getenv("TACHYON_COMPONENT_BACKUPS_DIR") || "/etc/tachyon/component-backups";
|
||||
let read_backup_meta = function(comp) {
|
||||
let path = base_bdir + "/" + comp + "/metadata.json";
|
||||
@@ -1197,6 +1206,13 @@ function build_system_info() {
|
||||
tailscale_installed,
|
||||
tailscale_backup_version: tailscale_meta ? as_string(tailscale_meta.version) : "",
|
||||
tailscale_backup_time: tailscale_meta ? int(tailscale_meta.timestamp || 0) : 0,
|
||||
direct_bypass_enabled,
|
||||
direct_bypass_address,
|
||||
direct_bypass_port,
|
||||
torrserver_running: int(torrserver_direct_status.running || 0),
|
||||
torrserver_direct_available: int(torrserver_direct_status.available || 0),
|
||||
torrserver_direct_enabled: int(torrserver_direct_status.enabled || 0),
|
||||
torrserver_direct_active: int(torrserver_direct_status.active || 0),
|
||||
openwrt_version: openwrt_release(),
|
||||
device_model,
|
||||
generated_at: int(clock()[0])
|
||||
|
||||
@@ -293,8 +293,24 @@ function cleanup_legacy_runtime(cfg) {
|
||||
command_success_from_args([ "rm", "-rf", cfg.legacy_runtime_base ]);
|
||||
}
|
||||
|
||||
function expand_strategy(cfg, value) {
|
||||
function normalize_ctrack_timeouts(value) {
|
||||
value = as_string(value);
|
||||
return replace(value, /--ctrack-timeouts=([0-9,]+)/g, function(full, numbers) {
|
||||
if (index(numbers, ",") < 0)
|
||||
return full;
|
||||
let parts = split(numbers, ",");
|
||||
if (length(parts) >= 8 && parts[0] == "30" && parts[2] == "600")
|
||||
return "--ctrack-timeouts=30:600:30:30";
|
||||
if (length(parts) >= 4)
|
||||
return "--ctrack-timeouts=" + parts[0] + ":" + parts[1] + ":" + parts[2] + ":" + parts[3];
|
||||
if (length(parts) >= 3)
|
||||
return "--ctrack-timeouts=" + parts[0] + ":" + parts[1] + ":" + parts[2];
|
||||
return "--ctrack-timeouts=30:600:30:30";
|
||||
});
|
||||
}
|
||||
|
||||
function expand_strategy(cfg, value) {
|
||||
value = normalize_ctrack_timeouts(as_string(value));
|
||||
if (cfg.legacy_runtime_base == "" || cfg.provider_base_dir == "")
|
||||
return value;
|
||||
return replace(value, cfg.legacy_runtime_base, cfg.provider_base_dir);
|
||||
@@ -701,5 +717,7 @@ function run(provider, argv) {
|
||||
}
|
||||
|
||||
return {
|
||||
run
|
||||
run,
|
||||
expand_strategy,
|
||||
normalize_ctrack_timeouts
|
||||
};
|
||||
@@ -1516,6 +1516,8 @@ function sing_box_signature_body(settings, sections, servers, mwan3_active) {
|
||||
body = signature_add_value(body, "settings.log_level", option(settings, "log_level", "warn"));
|
||||
body = signature_add_value(body, "settings.service_listen_address", option(settings, "service_listen_address", ""));
|
||||
body = signature_add_value(body, "runtime.mwan3_active", bool_value(mwan3_active));
|
||||
body = signature_add_value(body, "settings.direct_bypass_enabled", bool_option_value(settings, "direct_bypass_enabled", false));
|
||||
body = signature_add_value(body, "settings.direct_bypass_port", option(settings, "direct_bypass_port", "2080"));
|
||||
|
||||
let enable_yacd = bool_option_value(settings, "enable_yacd", false);
|
||||
body = signature_add_value(body, "settings.enable_yacd", enable_yacd);
|
||||
|
||||
@@ -22,6 +22,9 @@ const DNS_BLOCK_INBOUND_PORT = 1053;
|
||||
const SERVICE_MIXED_INBOUND_TAG = "service-mixed-in";
|
||||
const SERVICE_MIXED_INBOUND_ADDRESS = "127.0.0.1";
|
||||
const SERVICE_MIXED_INBOUND_PORT = 4534;
|
||||
const DIRECT_BYPASS_INBOUND_TAG = "direct-bypass-in";
|
||||
const DIRECT_BYPASS_OUTBOUND_TAG = "direct-bypass-out";
|
||||
const DIRECT_BYPASS_DEFAULT_PORT = 2080;
|
||||
const DIRECT_OUTBOUND_TAG = "direct-out";
|
||||
const BYPASS_OUTBOUND_TAG = "bypass-out";
|
||||
const OUTBOUND_MARK = 134217728;
|
||||
@@ -54,6 +57,8 @@ const RESERVED_TAGS = {
|
||||
[DNS_INBOUND_TAG]: true,
|
||||
[DNS_BLOCK_INBOUND_TAG]: true,
|
||||
[SERVICE_MIXED_INBOUND_TAG]: true,
|
||||
[DIRECT_BYPASS_INBOUND_TAG]: true,
|
||||
[DIRECT_BYPASS_OUTBOUND_TAG]: true,
|
||||
[DIRECT_OUTBOUND_TAG]: true,
|
||||
[BYPASS_OUTBOUND_TAG]: true
|
||||
};
|
||||
@@ -104,6 +109,9 @@ return {
|
||||
SERVICE_MIXED_INBOUND_TAG,
|
||||
SERVICE_MIXED_INBOUND_ADDRESS,
|
||||
SERVICE_MIXED_INBOUND_PORT,
|
||||
DIRECT_BYPASS_INBOUND_TAG,
|
||||
DIRECT_BYPASS_OUTBOUND_TAG,
|
||||
DIRECT_BYPASS_DEFAULT_PORT,
|
||||
DIRECT_OUTBOUND_TAG,
|
||||
BYPASS_OUTBOUND_TAG,
|
||||
OUTBOUND_MARK,
|
||||
|
||||
@@ -294,7 +294,7 @@ function worker() {
|
||||
|
||||
let failure_threshold = is_parallel
|
||||
? 2
|
||||
: int(common.option(current_cfg, "dns_failure_threshold", "3"));
|
||||
: int(common.option(current_cfg, "dns_failure_threshold", common.option(current_cfg, "dns_failover_failure_threshold", "3")));
|
||||
let recovery_threshold = is_parallel
|
||||
? 2
|
||||
: int(common.option(current_cfg, "dns_recovery_threshold", "3"));
|
||||
|
||||
@@ -618,6 +618,45 @@ function add_mixed_proxy_for_section(config, section, service_address) {
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
* Direct Bypass provides an explicit, clean mixed (HTTP+SOCKS5) proxy port on LAN
|
||||
* mapped directly to the direct outbound marked with OUTBOUND_MARK (0x08000000).
|
||||
* Traffic received on this port bypasses all section routing rules, VPN tunnels, and
|
||||
* DPI evasion engines, giving clients a guaranteed straight WAN route.
|
||||
*/
|
||||
function add_direct_bypass_proxy(config, settings, service_address) {
|
||||
if (!bool_option(settings, "direct_bypass_enabled", false))
|
||||
return;
|
||||
|
||||
let listen = as_string(service_address || "");
|
||||
if (listen == "")
|
||||
runtime_generate_unsupported("direct bypass listen address is not set");
|
||||
|
||||
let port_value = option(settings, "direct_bypass_port", as_string(runtime_constants.DIRECT_BYPASS_DEFAULT_PORT));
|
||||
if (match(port_value, /^[0-9]+$/) == null)
|
||||
runtime_generate_unsupported("direct bypass port is invalid");
|
||||
let listen_port = int(port_value, 10);
|
||||
if (listen_port < 1 || listen_port > 65535)
|
||||
runtime_generate_unsupported("direct bypass port is invalid");
|
||||
|
||||
push(config.inbounds, {
|
||||
type: "mixed",
|
||||
tag: runtime_constants.DIRECT_BYPASS_INBOUND_TAG,
|
||||
listen,
|
||||
listen_port
|
||||
});
|
||||
push(config.outbounds, {
|
||||
type: "direct",
|
||||
tag: runtime_constants.DIRECT_BYPASS_OUTBOUND_TAG,
|
||||
routing_mark: runtime_constants.OUTBOUND_MARK
|
||||
});
|
||||
push(config.route.rules, {
|
||||
action: "route",
|
||||
inbound: runtime_constants.DIRECT_BYPASS_INBOUND_TAG,
|
||||
outbound: runtime_constants.DIRECT_BYPASS_OUTBOUND_TAG
|
||||
});
|
||||
}
|
||||
|
||||
function add_service_mixed_proxy_inbound(config, tag_name, listen_port, outbound) {
|
||||
push(config.inbounds, {
|
||||
type: "mixed",
|
||||
@@ -990,6 +1029,7 @@ function generate_config(output_path, service_address, mwan3_active, supports_xh
|
||||
push(config.dns.rules, rule);
|
||||
}
|
||||
|
||||
add_direct_bypass_proxy(config, settings, service_address);
|
||||
add_service_mixed_proxy(config, settings, sections);
|
||||
for (let section in sections)
|
||||
add_mixed_proxy_for_section(config, section, service_address);
|
||||
|
||||
@@ -1783,14 +1783,10 @@ function download_subscription_into_cache(section_name_value, subscription_url,
|
||||
}
|
||||
|
||||
if (file_nonempty(subscription_json_path) && parser.runtime_outbounds_equal(normalized_tmpfile, subscription_json_path)) {
|
||||
if (!move_file(normalized_tmpfile, subscription_json_path)) {
|
||||
if (metadata_output_path != "")
|
||||
unlink_path(metadata_output_path);
|
||||
unlink_path(raw_tmpfile);
|
||||
unlink_path(headers_tmpfile);
|
||||
unlink_path(metadata_tmpfile);
|
||||
return 1;
|
||||
}
|
||||
// Keep the current file and running outbound ordering. The newly downloaded file
|
||||
// differs only in provider node ordering or metadata ignored by runtime comparison,
|
||||
// neither of which should churn the on-disk file or trigger a sing-box reload.
|
||||
unlink_path(normalized_tmpfile);
|
||||
|
||||
write_text_if_changed(subscription_url_cache_path, subscription_url);
|
||||
write_text_if_changed(subscription_user_agent_cache_path, effective_user_agent);
|
||||
@@ -1816,7 +1812,7 @@ function download_subscription_into_cache(section_name_value, subscription_url,
|
||||
return 1;
|
||||
}
|
||||
|
||||
write_text_if_changed(subscription_url_cache_path, subscription_url);
|
||||
write_text_if_changed(subscription_url_cache_path, subscription_url);
|
||||
write_text_if_changed(subscription_user_agent_cache_path, effective_user_agent);
|
||||
write_text_if_changed(subscription_hwid_cache_path, effective_hwid);
|
||||
write_text_if_changed(subscription_device_headers_cache_path, subscription_device_headers_signature);
|
||||
|
||||
@@ -397,7 +397,15 @@ function runtime_outbounds_signature(path) {
|
||||
if (type(subscription) != "object" || type(subscription.outbounds) != "array")
|
||||
return null;
|
||||
|
||||
return canonical_runtime_value(subscription.outbounds);
|
||||
let result = [];
|
||||
for (let outbound in subscription.outbounds)
|
||||
push(result, sprintf("%J", canonical_runtime_value(outbound)));
|
||||
|
||||
sort(result, function(a, b) {
|
||||
return a < b ? -1 : (a > b ? 1 : 0);
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function runtime_outbounds_equal(left_path, right_path) {
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
#!/usr/bin/ucode
|
||||
|
||||
let fs = require("fs");
|
||||
let uci = require("core.uci");
|
||||
|
||||
const CONFIG_NAME = getenv("TACHYON_CONFIG_NAME") || "tachyon";
|
||||
const TABLE = "TachyonTorrServerDirect";
|
||||
const OUTBOUND_MARK = getenv("NFT_OUTBOUND_MARK") || "0x08000000";
|
||||
|
||||
function text(value) { return value == null ? "" : "" + value; }
|
||||
function quote(value) { return "'" + replace(text(value), /'/g, "'\\''") + "'"; }
|
||||
function command(args) {
|
||||
let values = [];
|
||||
for (let arg in args) push(values, quote(arg));
|
||||
return join(" ", values);
|
||||
}
|
||||
function success(args) { return system(command(args) + " >/dev/null 2>&1") == 0; }
|
||||
function command_output(args) {
|
||||
let pipe = fs.popen(command(args) + " 2>/dev/null", "r");
|
||||
if (!pipe) return "";
|
||||
let data = pipe.read("all");
|
||||
let status = pipe.close();
|
||||
return status == 0 && data != null ? text(data) : "";
|
||||
}
|
||||
function read(path) { let value = fs.readfile(path); return value == null ? "" : text(value); }
|
||||
function is_torrserver_cmdline(value) {
|
||||
value = lc(replace(text(value), /\x00/g, " "));
|
||||
return match(value, /(^|[/ ])torrserver([^/ ]*)?( |$)/) != null;
|
||||
}
|
||||
function numeric_pid(path) {
|
||||
let parts = split(path, "/");
|
||||
let pid = length(parts) > 2 ? parts[2] : "";
|
||||
return match(pid, /^[0-9]+$/) != null ? pid : "";
|
||||
}
|
||||
function process_cgroup(pid) {
|
||||
for (let line in split(read("/proc/" + pid + "/cgroup"), "\n")) {
|
||||
let match_value = match(line, /^[0-9]+::(\/.+)$/);
|
||||
if (match_value != null) return match_value[1];
|
||||
}
|
||||
return "";
|
||||
}
|
||||
function valid_cgroup(path) {
|
||||
if (match(path, /^\/[A-Za-z0-9_.@:-]+(\/[A-Za-z0-9_.@:-]+)+$/) == null)
|
||||
return false;
|
||||
return path != "/services" && path != "/system.slice" && path != "/user.slice";
|
||||
}
|
||||
function dedicated_cgroup(path) {
|
||||
let pids = split(replace(read("/sys/fs/cgroup" + path + "/cgroup.procs"), /[\r\n]+$/g, ""), /[\r\n]+/);
|
||||
let found = 0;
|
||||
for (let pid in pids) {
|
||||
if (pid == "") continue;
|
||||
found++;
|
||||
if (!is_torrserver_cmdline(read("/proc/" + pid + "/cmdline"))) return false;
|
||||
}
|
||||
return found > 0;
|
||||
}
|
||||
function discover() {
|
||||
for (let cmdline_path in fs.glob("/proc/[0-9]*/cmdline")) {
|
||||
let pid = numeric_pid(cmdline_path);
|
||||
if (pid == "" || !is_torrserver_cmdline(read(cmdline_path))) continue;
|
||||
let path = process_cgroup(pid);
|
||||
if (valid_cgroup(path) && dedicated_cgroup(path))
|
||||
return { running: 1, available: 1, pid, cgroup: path };
|
||||
return { running: 1, available: 0, pid, cgroup: path };
|
||||
}
|
||||
return { running: 0, available: 0, pid: "", cgroup: "" };
|
||||
}
|
||||
function enabled() { return trim(uci.get(CONFIG_NAME + ".settings.torrserver_direct_enabled")) == "1"; }
|
||||
function rule_output_active(output, info) {
|
||||
if (!info.available) return false;
|
||||
let path = substr(info.cgroup, 1);
|
||||
let parts = split(path, "/");
|
||||
let level = length(parts) > 2 ? 2 : length(parts);
|
||||
if (level == 2) path = parts[0] + "/" + parts[1];
|
||||
return (index(output, "type route hook output priority mangle - 1") >= 0 ||
|
||||
index(output, "type route hook output priority -151") >= 0) &&
|
||||
index(output, "socket cgroupv2 level " + level + " \"" + path + "\"") >= 0 &&
|
||||
(index(output, "meta mark set 0x08000000") >= 0 ||
|
||||
index(output, "meta mark set 0x8000000") >= 0) &&
|
||||
index(output, "Tachyon TorrServer Direct") >= 0;
|
||||
}
|
||||
function active(info) {
|
||||
return rule_output_active(command_output([ "nft", "list", "chain", "inet", TABLE, "output" ]), info);
|
||||
}
|
||||
function remove_rule() { success([ "nft", "delete", "table", "inet", TABLE ]); }
|
||||
function apply_rule(info) {
|
||||
if (!info.available) return false;
|
||||
let path = substr(info.cgroup, 1);
|
||||
let parts = split(path, "/");
|
||||
let level = length(parts) > 2 ? 2 : length(parts);
|
||||
if (level == 2)
|
||||
path = parts[0] + "/" + parts[1];
|
||||
remove_rule();
|
||||
let ruleset_path = "/tmp/tachyon-torrserver-direct.nft";
|
||||
let ruleset = "add table inet " + TABLE + "\n" +
|
||||
"add chain inet " + TABLE + " output { type route hook output priority -151; policy accept; }\n" +
|
||||
"add rule inet " + TABLE + " output socket cgroupv2 level " + level + " \"" + path +
|
||||
"\" meta mark set " + OUTBOUND_MARK + " counter comment \"Tachyon TorrServer Direct\"\n";
|
||||
if (fs.writefile(ruleset_path, ruleset) == null)
|
||||
return false;
|
||||
let applied = success([ "nft", "-f", ruleset_path ]);
|
||||
try { fs.unlink(ruleset_path); } catch (e) { }
|
||||
if (!applied || !active(info)) {
|
||||
remove_rule();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
function status() {
|
||||
let info = discover();
|
||||
info.enabled = enabled() ? 1 : 0;
|
||||
info.active = active(info) ? 1 : 0;
|
||||
return info;
|
||||
}
|
||||
function reconcile() {
|
||||
if (!enabled()) { remove_rule(); return 0; }
|
||||
let info = discover();
|
||||
if (!info.available) { remove_rule(); return 1; }
|
||||
return apply_rule(info) ? 0 : 1;
|
||||
}
|
||||
function worker() {
|
||||
let last_cgroup = "";
|
||||
while (enabled()) {
|
||||
let info = discover();
|
||||
if (info.available && (info.cgroup != last_cgroup || !active(info))) {
|
||||
if (apply_rule(info)) last_cgroup = info.cgroup;
|
||||
}
|
||||
else if (!info.available) {
|
||||
remove_rule();
|
||||
last_cgroup = "";
|
||||
}
|
||||
system("sleep 60");
|
||||
}
|
||||
remove_rule();
|
||||
}
|
||||
|
||||
let mode = ARGV[0] || "status";
|
||||
if (mode == "status") print(sprintf("%J\n", status()));
|
||||
else if (mode == "reconcile") exit(reconcile());
|
||||
else if (mode == "remove") { remove_rule(); exit(0); }
|
||||
else if (mode == "worker") worker();
|
||||
else if (mode == "rule-output-active") {
|
||||
let info = { available: 1, cgroup: ARGV[1] || "" };
|
||||
exit(rule_output_active(read("/dev/stdin"), info) ? 0 : 1);
|
||||
}
|
||||
else exit(1);
|
||||
@@ -0,0 +1,120 @@
|
||||
#!/usr/bin/env bash
|
||||
set -eo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
TACHYON_LIB="$ROOT_DIR/tachyon/files/usr/lib"
|
||||
GENERATOR_UC="$TACHYON_LIB/singbox/generator.uc"
|
||||
VALIDATOR_UC="$TACHYON_LIB/config/validator.uc"
|
||||
TORRSERVER_UC="$TACHYON_LIB/torrserver/direct.uc"
|
||||
WORK_DIR="$(mktemp -d)"
|
||||
|
||||
cleanup() {
|
||||
rm -rf "$WORK_DIR"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
fail() {
|
||||
printf 'FAIL: %s\n' "$1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
generate_config() {
|
||||
local fixture="$1"
|
||||
local output="$2"
|
||||
mkdir -p "${output}.section-cache"
|
||||
ucode -L "$TACHYON_LIB" "$GENERATOR_UC" generate-config-fixture \
|
||||
"$fixture" "$output" "127.0.0.1"
|
||||
}
|
||||
|
||||
# 1. Verify direct_bypass disabled -> no direct inbound
|
||||
cat >"$WORK_DIR/no_bypass.json" <<'JSON'
|
||||
{
|
||||
"settings": {
|
||||
".name": "settings",
|
||||
".type": "settings",
|
||||
"direct_bypass_enabled": "0"
|
||||
},
|
||||
"section": [
|
||||
{
|
||||
".name": "proxy",
|
||||
".type": "section",
|
||||
"enabled": "1",
|
||||
"action": "connection",
|
||||
"outbound_jsons": [
|
||||
"{\"type\":\"vless\",\"tag\":\"Alpha\",\"server\":\"alpha.example\",\"server_port\":443,\"uuid\":\"00000000-0000-4000-8000-000000000001\",\"tls\":{\"enabled\":true}}"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
JSON
|
||||
|
||||
generate_config "$WORK_DIR/no_bypass.json" "$WORK_DIR/out_no_bypass.json"
|
||||
if grep -Fq '"tag": "direct-bypass-in"' "$WORK_DIR/out_no_bypass.json"; then
|
||||
fail "direct-bypass-in should not be present when direct_bypass_enabled is 0"
|
||||
fi
|
||||
|
||||
# 2. Verify direct_bypass enabled -> mixed inbound present with correct port and detour
|
||||
cat >"$WORK_DIR/bypass_on.json" <<'JSON'
|
||||
{
|
||||
"settings": {
|
||||
".name": "settings",
|
||||
".type": "settings",
|
||||
"direct_bypass_enabled": "1",
|
||||
"direct_bypass_port": "3128"
|
||||
},
|
||||
"section": [
|
||||
{
|
||||
".name": "proxy",
|
||||
".type": "section",
|
||||
"enabled": "1",
|
||||
"action": "connection",
|
||||
"outbound_jsons": [
|
||||
"{\"type\":\"vless\",\"tag\":\"Alpha\",\"server\":\"alpha.example\",\"server_port\":443,\"uuid\":\"00000000-0000-4000-8000-000000000001\",\"tls\":{\"enabled\":true}}"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
JSON
|
||||
|
||||
generate_config "$WORK_DIR/bypass_on.json" "$WORK_DIR/out_bypass_on.json"
|
||||
grep -Fq '"tag": "direct-bypass-in"' "$WORK_DIR/out_bypass_on.json" ||
|
||||
fail "direct-bypass-in must be present when direct_bypass_enabled is 1"
|
||||
grep -Fq '"listen_port": 3128' "$WORK_DIR/out_bypass_on.json" ||
|
||||
fail "direct-bypass-in must listen on configured port 3128"
|
||||
grep -Fq '"outbound": "direct-bypass-out"' "$WORK_DIR/out_bypass_on.json" ||
|
||||
fail "direct-bypass-out must be configured"
|
||||
grep -Fq 'direct-bypass-in' "$WORK_DIR/out_bypass_on.json" ||
|
||||
fail "direct-bypass-in must be routed"
|
||||
|
||||
# 3. Verify validator validates direct_bypass_port
|
||||
cat >"$WORK_DIR/invalid_port.json" <<'JSON'
|
||||
{
|
||||
"settings": {
|
||||
"direct_bypass_enabled": "1",
|
||||
"direct_bypass_port": "99999"
|
||||
}
|
||||
}
|
||||
JSON
|
||||
|
||||
if ucode -L "$TACHYON_LIB" "$VALIDATOR_UC" validate-fixture "$WORK_DIR/invalid_port.json" >/dev/null 2>&1; then
|
||||
fail "validator should reject direct_bypass_port 99999"
|
||||
fi
|
||||
|
||||
# 4. Verify TorrServer direct rule recognition
|
||||
sample_nft='table inet TachyonTorrServerDirect {
|
||||
chain output {
|
||||
type route hook output priority -151; policy accept;
|
||||
socket cgroupv2 level 2 "services/torrserver" meta mark set 0x08000000 counter comment "Tachyon TorrServer Direct"
|
||||
}
|
||||
}'
|
||||
echo "$sample_nft" | ucode -L "$TACHYON_LIB" "$TORRSERVER_UC" rule-output-active "/services/torrserver" ||
|
||||
fail "TorrServer rule-output-active must match valid rule"
|
||||
|
||||
# 5. Verify TorrServer direct status outputs JSON
|
||||
status_output="$(ucode -L "$TACHYON_LIB" "$TORRSERVER_UC" status)"
|
||||
echo "$status_output" | grep -Fq '"available":' ||
|
||||
fail "TorrServer status must include available field"
|
||||
echo "$status_output" | grep -Fq '"running":' ||
|
||||
fail "TorrServer status must include running field"
|
||||
|
||||
printf 'direct bypass and torrserver tests passed\n'
|
||||
@@ -575,6 +575,10 @@ debug
|
||||
127.0.0.1
|
||||
[runtime.mwan3_active]
|
||||
1
|
||||
[settings.direct_bypass_enabled]
|
||||
0
|
||||
[settings.direct_bypass_port]
|
||||
2080
|
||||
[settings.enable_yacd]
|
||||
1
|
||||
[settings.enable_yacd_wan_access]
|
||||
|
||||
@@ -46,6 +46,7 @@ expected_shell_scripts="$(
|
||||
'luci-app-tachyon/root/etc/uci-defaults/50_luci-tachyon' \
|
||||
'tachyon/files/etc/hotplug.d/iface/99-tachyon-wan-monitor' \
|
||||
'tachyon/files/etc/init.d/tachyon' \
|
||||
'tachyon/files/etc/init.d/tachyon-torrserver-direct' \
|
||||
'tachyon/files/usr/lib/cgi-bin/tachyon-agent' \
|
||||
'tachyon/files/usr/lib/firewall.include' |
|
||||
LC_ALL=C sort
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
#!/usr/bin/env bash
|
||||
set -eo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
PARSER_UC="$ROOT_DIR/tachyon/files/usr/lib/subscription/parser.uc"
|
||||
CACHE_UC="$ROOT_DIR/tachyon/files/usr/lib/subscription/cache.uc"
|
||||
TACHYON_LIB="$ROOT_DIR/tachyon/files/usr/lib"
|
||||
WORK_DIR="$(mktemp -d)"
|
||||
|
||||
cleanup() {
|
||||
rm -rf "$WORK_DIR"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
fail() {
|
||||
printf 'FAIL: %s\n' "$1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
# 1. Verify that identical subscriptions with reordered outbounds match
|
||||
cat << 'EOF' > "$WORK_DIR/order_a.json"
|
||||
{
|
||||
"outbounds": [
|
||||
{
|
||||
"tag": "nl-ams-01",
|
||||
"type": "vless",
|
||||
"server": "ams.example.com",
|
||||
"server_port": 443,
|
||||
"uuid": "00000000-0000-0000-0000-000000000001",
|
||||
"tls": { "enabled": true, "server_name": "ams.example.com" }
|
||||
},
|
||||
{
|
||||
"tag": "de-fra-02",
|
||||
"type": "vless",
|
||||
"server": "fra.example.com",
|
||||
"server_port": 443,
|
||||
"uuid": "00000000-0000-0000-0000-000000000002",
|
||||
"tls": { "enabled": true, "server_name": "fra.example.com" }
|
||||
},
|
||||
{
|
||||
"tag": "us-nyc-03",
|
||||
"type": "vless",
|
||||
"server": "nyc.example.com",
|
||||
"server_port": 443,
|
||||
"uuid": "00000000-0000-0000-0000-000000000003",
|
||||
"tls": { "enabled": true, "server_name": "nyc.example.com" }
|
||||
}
|
||||
]
|
||||
}
|
||||
EOF
|
||||
|
||||
cat << 'EOF' > "$WORK_DIR/order_b.json"
|
||||
{
|
||||
"outbounds": [
|
||||
{
|
||||
"tag": "us-nyc-03",
|
||||
"type": "vless",
|
||||
"server": "nyc.example.com",
|
||||
"server_port": 443,
|
||||
"uuid": "00000000-0000-0000-0000-000000000003",
|
||||
"tls": { "enabled": true, "server_name": "nyc.example.com" }
|
||||
},
|
||||
{
|
||||
"tag": "nl-ams-01",
|
||||
"type": "vless",
|
||||
"server": "ams.example.com",
|
||||
"server_port": 443,
|
||||
"uuid": "00000000-0000-0000-0000-000000000001",
|
||||
"tls": { "enabled": true, "server_name": "ams.example.com" }
|
||||
},
|
||||
{
|
||||
"tag": "de-fra-02",
|
||||
"type": "vless",
|
||||
"server": "fra.example.com",
|
||||
"server_port": 443,
|
||||
"uuid": "00000000-0000-0000-0000-000000000002",
|
||||
"tls": { "enabled": true, "server_name": "fra.example.com" }
|
||||
}
|
||||
]
|
||||
}
|
||||
EOF
|
||||
|
||||
ucode -L "$TACHYON_LIB" "$PARSER_UC" runtime-outbounds-equal "$WORK_DIR/order_a.json" "$WORK_DIR/order_b.json" ||
|
||||
fail "Reordered subscriptions must be considered runtime equal"
|
||||
|
||||
# 2. Verify that non-runtime metadata difference (share_link) does not affect equality
|
||||
cat << 'EOF' > "$WORK_DIR/order_metadata.json"
|
||||
{
|
||||
"outbounds": [
|
||||
{
|
||||
"tag": "de-fra-02",
|
||||
"type": "vless",
|
||||
"server": "fra.example.com",
|
||||
"server_port": 443,
|
||||
"uuid": "00000000-0000-0000-0000-000000000002",
|
||||
"tls": { "enabled": true, "server_name": "fra.example.com" },
|
||||
"share_link": "vless://00000000-0000-0000-0000-000000000002@fra.example.com:443#fra"
|
||||
},
|
||||
{
|
||||
"tag": "nl-ams-01",
|
||||
"type": "vless",
|
||||
"server": "ams.example.com",
|
||||
"server_port": 443,
|
||||
"uuid": "00000000-0000-0000-0000-000000000001",
|
||||
"tls": { "enabled": true, "server_name": "ams.example.com" }
|
||||
},
|
||||
{
|
||||
"tag": "us-nyc-03",
|
||||
"type": "vless",
|
||||
"server": "nyc.example.com",
|
||||
"server_port": 443,
|
||||
"uuid": "00000000-0000-0000-0000-000000000003",
|
||||
"tls": { "enabled": true, "server_name": "nyc.example.com" }
|
||||
}
|
||||
]
|
||||
}
|
||||
EOF
|
||||
|
||||
ucode -L "$TACHYON_LIB" "$PARSER_UC" runtime-outbounds-equal "$WORK_DIR/order_a.json" "$WORK_DIR/order_metadata.json" ||
|
||||
fail "Subscriptions differing only by share_link metadata and order must be considered equal"
|
||||
|
||||
# 3. Verify that real configuration change is NOT equal
|
||||
cat << 'EOF' > "$WORK_DIR/order_different.json"
|
||||
{
|
||||
"outbounds": [
|
||||
{
|
||||
"tag": "nl-ams-01",
|
||||
"type": "vless",
|
||||
"server": "ams.example.com",
|
||||
"server_port": 8443,
|
||||
"uuid": "00000000-0000-0000-0000-000000000001",
|
||||
"tls": { "enabled": true, "server_name": "ams.example.com" }
|
||||
},
|
||||
{
|
||||
"tag": "de-fra-02",
|
||||
"type": "vless",
|
||||
"server": "fra.example.com",
|
||||
"server_port": 443,
|
||||
"uuid": "00000000-0000-0000-0000-000000000002",
|
||||
"tls": { "enabled": true, "server_name": "fra.example.com" }
|
||||
},
|
||||
{
|
||||
"tag": "us-nyc-03",
|
||||
"type": "vless",
|
||||
"server": "nyc.example.com",
|
||||
"server_port": 443,
|
||||
"uuid": "00000000-0000-0000-0000-000000000003",
|
||||
"tls": { "enabled": true, "server_name": "nyc.example.com" }
|
||||
}
|
||||
]
|
||||
}
|
||||
EOF
|
||||
|
||||
if ucode -L "$TACHYON_LIB" "$PARSER_UC" runtime-outbounds-equal "$WORK_DIR/order_a.json" "$WORK_DIR/order_different.json"; then
|
||||
fail "Subscriptions with port change must not be considered runtime equal"
|
||||
fi
|
||||
|
||||
# 4. Verify that missing an outbound is NOT equal
|
||||
cat << 'EOF' > "$WORK_DIR/order_missing.json"
|
||||
{
|
||||
"outbounds": [
|
||||
{
|
||||
"tag": "nl-ams-01",
|
||||
"type": "vless",
|
||||
"server": "ams.example.com",
|
||||
"server_port": 443,
|
||||
"uuid": "00000000-0000-0000-0000-000000000001",
|
||||
"tls": { "enabled": true, "server_name": "ams.example.com" }
|
||||
}
|
||||
]
|
||||
}
|
||||
EOF
|
||||
|
||||
if ucode -L "$TACHYON_LIB" "$PARSER_UC" runtime-outbounds-equal "$WORK_DIR/order_a.json" "$WORK_DIR/order_missing.json"; then
|
||||
fail "Subscriptions with missing outbounds must not be considered runtime equal"
|
||||
fi
|
||||
|
||||
echo "subscription_order_independence: ALL ASSERTIONS PASSED"
|
||||
@@ -106,6 +106,19 @@ if (zapret2_validator.validate_strategy("nfqws", "--dpi-desync=fake", "").valid)
|
||||
|
||||
if (validator.strategy_or_default("", "--default\t1") != "--default 1")
|
||||
exit(1);
|
||||
|
||||
let runtime = require("providers.nfqueue.runtime");
|
||||
let cfg = { legacy_runtime_base: "", provider_base_dir: "" };
|
||||
let normalized = runtime.expand_strategy(cfg, "--filter-tcp=80 --ctrack-timeouts=30,30,600,30,30,30,30,30");
|
||||
if (normalized != "--filter-tcp=80 --ctrack-timeouts=30:600:30:30") {
|
||||
print("Unexpected normalized timeouts: " + normalized + "\n");
|
||||
exit(1);
|
||||
}
|
||||
let untouched = runtime.expand_strategy(cfg, "--filter-tcp=80 --ctrack-timeouts=30:600:30:30");
|
||||
if (untouched != "--filter-tcp=80 --ctrack-timeouts=30:600:30:30") {
|
||||
print("Untouched timeouts changed: " + untouched + "\n");
|
||||
exit(1);
|
||||
}
|
||||
UCODE
|
||||
|
||||
ucode -L "$TACHYON_LIB" "$WORK_DIR/require-zapret-validator.uc"
|
||||
|
||||
Reference in New Issue
Block a user