feat: гармонизация секции AWG в дашборде и изоляция маршрутизации FPTN
This commit is contained in:
+974
-970
File diff suppressed because it is too large
Load Diff
+976
-972
File diff suppressed because it is too large
Load Diff
@@ -7,8 +7,8 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: TACHYON\n"
|
"Project-Id-Version: TACHYON\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2026-09-17 18:55+0300\n"
|
"POT-Creation-Date: 2026-09-17 19:42+0300\n"
|
||||||
"PO-Revision-Date: 2026-09-17 18:55+0300\n"
|
"PO-Revision-Date: 2026-09-17 19:42+0300\n"
|
||||||
"Last-Translator: Dushnilin\n"
|
"Last-Translator: Dushnilin\n"
|
||||||
"Language-Team: none\n"
|
"Language-Team: none\n"
|
||||||
"Language: ru\n"
|
"Language: ru\n"
|
||||||
|
|||||||
@@ -47,6 +47,19 @@ interface IRenderSectionsProps {
|
|||||||
testingOutboundCodes?: Record<string, boolean>;
|
testingOutboundCodes?: Record<string, boolean>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const CONNECTION_ACTION_LABELS: Record<string, string> = {
|
||||||
|
awg: 'AmneziaWG',
|
||||||
|
warp: 'WARP',
|
||||||
|
anytls: 'AnyTLS',
|
||||||
|
snell: 'Snell',
|
||||||
|
mieru: 'Mieru',
|
||||||
|
sudoku: 'Sudoku',
|
||||||
|
masque: 'MASQUE',
|
||||||
|
openvpn: 'OpenVPN',
|
||||||
|
fptn: 'FPTN',
|
||||||
|
vpn: 'VPN',
|
||||||
|
};
|
||||||
|
|
||||||
function renderFailedState() {
|
function renderFailedState() {
|
||||||
return E(
|
return E(
|
||||||
'div',
|
'div',
|
||||||
@@ -529,89 +542,88 @@ function renderDefaultState({
|
|||||||
.join(' ');
|
.join(' ');
|
||||||
|
|
||||||
if (isConnectionNode) {
|
if (isConnectionNode) {
|
||||||
|
const statusText = latencyFetching
|
||||||
|
? _('Checking...')
|
||||||
|
: outbound.latency === -1 || outbound.latency < 0
|
||||||
|
? _('Not responding')
|
||||||
|
: outbound.runtimeAvailable
|
||||||
|
? _('Connected')
|
||||||
|
: _('Not connected');
|
||||||
|
|
||||||
|
const statusColor = latencyFetching
|
||||||
|
? 'var(--warn-color-medium, orange)'
|
||||||
|
: outbound.latency === -1 || outbound.latency < 0
|
||||||
|
? 'var(--error-color-medium, red)'
|
||||||
|
: outbound.runtimeAvailable
|
||||||
|
? 'var(--success-color-medium, green)'
|
||||||
|
: 'var(--error-color-medium, red)';
|
||||||
|
|
||||||
|
const latencyDisplay =
|
||||||
|
outbound.latency && outbound.latency > 0
|
||||||
|
? `${outbound.latency} ms`
|
||||||
|
: '';
|
||||||
|
|
||||||
return E(
|
return E(
|
||||||
'div',
|
'div',
|
||||||
{
|
{
|
||||||
class: className,
|
class: className,
|
||||||
style:
|
style:
|
||||||
'display: flex; align-items: center; justify-content: space-between; padding: 12px; min-width: 0; gap: 16px;',
|
'display: flex; flex-direction: column; justify-content: space-between; padding: 12px; min-width: 0;',
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
E(
|
E(
|
||||||
'div',
|
'div',
|
||||||
{
|
{
|
||||||
style:
|
class: 'tachyon_dashboard-page__outbound-grid__item__header',
|
||||||
'display: flex; align-items: center; gap: 12px; min-width: 0;',
|
style: 'margin-bottom: 8px;',
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
E(
|
|
||||||
'b',
|
|
||||||
{
|
|
||||||
style:
|
|
||||||
'overflow-wrap: anywhere; word-break: break-all; min-width: 0;',
|
|
||||||
},
|
|
||||||
renderFlagEmojis(outbound.displayName),
|
|
||||||
),
|
|
||||||
outbound.prefix
|
|
||||||
? E(
|
|
||||||
'span',
|
|
||||||
{
|
|
||||||
class:
|
|
||||||
'tachyon_dashboard-page__outbound-grid__item__prefix-badge',
|
|
||||||
},
|
|
||||||
renderFlagEmojis(outbound.prefix),
|
|
||||||
)
|
|
||||||
: '',
|
|
||||||
E(
|
|
||||||
'span',
|
|
||||||
{
|
|
||||||
style:
|
|
||||||
'opacity: 0.7; font-size: 13px; white-space: nowrap; flex-shrink: 0;',
|
|
||||||
},
|
|
||||||
[formatOutboundType(outbound.type, outbound.transport)].filter(
|
|
||||||
Boolean,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
E(
|
E(
|
||||||
'div',
|
'div',
|
||||||
{
|
{
|
||||||
class: getLatencyClass(),
|
class:
|
||||||
style: 'white-space: nowrap; flex-shrink: 0;',
|
'tachyon_dashboard-page__outbound-grid__item__title-wrapper',
|
||||||
},
|
},
|
||||||
connectionStatusText,
|
[
|
||||||
|
E(
|
||||||
|
'span',
|
||||||
|
{
|
||||||
|
style: `color: ${statusColor}; font-weight: 500; font-size: 13px;`,
|
||||||
|
},
|
||||||
|
statusText,
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
E(
|
E(
|
||||||
'button',
|
'div',
|
||||||
{
|
{
|
||||||
type: 'button',
|
class: 'tachyon_dashboard-page__outbound-grid__item__footer',
|
||||||
class: 'btn dashboard-sections-grid-item-test-latency',
|
style:
|
||||||
style: 'padding: 4px 12px; height: 30px; flex-shrink: 0;',
|
'display: flex; align-items: center; justify-content: space-between; gap: 8px;',
|
||||||
disabled: latencyFetching ? true : undefined,
|
|
||||||
click: (event: MouseEvent) => {
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
if (latencyFetching) return;
|
|
||||||
testLatency();
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
latencyFetching
|
[
|
||||||
? [
|
E(
|
||||||
renderLoaderCircleIcon24(),
|
'div',
|
||||||
E(
|
{
|
||||||
'span',
|
class: 'tachyon_dashboard-page__outbound-grid__item__type',
|
||||||
|
style:
|
||||||
|
'overflow: hidden; text-overflow: ellipsis; white-space: nowrap;',
|
||||||
|
},
|
||||||
|
renderFlagEmojis(outbound.displayName),
|
||||||
|
),
|
||||||
|
latencyDisplay
|
||||||
|
? E(
|
||||||
|
'div',
|
||||||
{
|
{
|
||||||
class: 'dashboard-sections-grid-item-test-latency__label',
|
class: getLatencyClass(),
|
||||||
|
style: 'white-space: nowrap; flex-shrink: 0;',
|
||||||
},
|
},
|
||||||
_('Checking...'),
|
latencyDisplay,
|
||||||
),
|
)
|
||||||
]
|
: '',
|
||||||
: E(
|
],
|
||||||
'span',
|
|
||||||
{ class: 'dashboard-sections-grid-item-test-latency__label' },
|
|
||||||
_('Check Connection'),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@@ -817,26 +829,6 @@ function renderDefaultState({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isConnectionNode) {
|
|
||||||
return E(
|
|
||||||
'div',
|
|
||||||
{
|
|
||||||
class: 'tachyon_dashboard-page__outbound-section',
|
|
||||||
style: 'border: none; padding: 0;',
|
|
||||||
},
|
|
||||||
[
|
|
||||||
E(
|
|
||||||
'div',
|
|
||||||
{
|
|
||||||
style:
|
|
||||||
'display: flex; flex-direction: column; gap: 8px; padding: 0;',
|
|
||||||
},
|
|
||||||
[...section.outbounds.map((outbound) => renderOutbound(outbound))],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const metadataNodes = (section.subscriptionMetadata || [])
|
const metadataNodes = (section.subscriptionMetadata || [])
|
||||||
.map((metadata) => renderSubscriptionMetadata(metadata))
|
.map((metadata) => renderSubscriptionMetadata(metadata))
|
||||||
.filter(Boolean) as HTMLElement[];
|
.filter(Boolean) as HTMLElement[];
|
||||||
@@ -907,6 +899,11 @@ function renderDefaultState({
|
|||||||
'openvpn',
|
'openvpn',
|
||||||
].includes(section.action || '');
|
].includes(section.action || '');
|
||||||
|
|
||||||
|
const connectionTitle =
|
||||||
|
(section.action &&
|
||||||
|
CONNECTION_ACTION_LABELS[section.action]) ||
|
||||||
|
selectedOutbound.displayName;
|
||||||
|
|
||||||
function getLatencyColor() {
|
function getLatencyColor() {
|
||||||
if (isConnectionNode) {
|
if (isConnectionNode) {
|
||||||
if (latencyFetching)
|
if (latencyFetching)
|
||||||
@@ -974,7 +971,10 @@ function renderDefaultState({
|
|||||||
E(
|
E(
|
||||||
'span',
|
'span',
|
||||||
{ style: 'opacity: 0.7;' },
|
{ style: 'opacity: 0.7;' },
|
||||||
selectedOutbound.displayName,
|
isConnectionNode
|
||||||
|
? connectionTitle
|
||||||
|
: selectedOutbound.prefix ||
|
||||||
|
selectedOutbound.displayName,
|
||||||
),
|
),
|
||||||
latencyText
|
latencyText
|
||||||
? E(
|
? E(
|
||||||
@@ -986,7 +986,32 @@ function renderDefaultState({
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
})()
|
})()
|
||||||
: '',
|
: isConnectionNode
|
||||||
|
? (() => {
|
||||||
|
const selectedOutbound = section.outbounds.find(
|
||||||
|
(o) => o.selected,
|
||||||
|
);
|
||||||
|
if (!selectedOutbound) return '';
|
||||||
|
const connectionTitle =
|
||||||
|
(section.action &&
|
||||||
|
CONNECTION_ACTION_LABELS[section.action]) ||
|
||||||
|
selectedOutbound.displayName;
|
||||||
|
if (
|
||||||
|
section.displayName.toLowerCase().trim() ===
|
||||||
|
connectionTitle.toLowerCase().trim()
|
||||||
|
) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return E(
|
||||||
|
'span',
|
||||||
|
{
|
||||||
|
style:
|
||||||
|
'font-size: 13px; font-weight: normal; margin-left: 8px; opacity: 0.7;',
|
||||||
|
},
|
||||||
|
connectionTitle,
|
||||||
|
);
|
||||||
|
})()
|
||||||
|
: '',
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
E(
|
E(
|
||||||
|
|||||||
@@ -527,4 +527,81 @@ describe('renderSections', () => {
|
|||||||
expect(prefixBadge).not.toBeNull();
|
expect(prefixBadge).not.toBeNull();
|
||||||
expect(prefixBadge?.textContent).toBe('Geodema Network');
|
expect(prefixBadge?.textContent).toBe('Geodema Network');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('renders connection node (AWG) with collapsible header and check connection action', () => {
|
||||||
|
const section: any = {
|
||||||
|
sectionName: 'AWG',
|
||||||
|
displayName: 'AMNEZIA',
|
||||||
|
action: 'awg',
|
||||||
|
outbounds: [
|
||||||
|
{
|
||||||
|
code: 'awg-out',
|
||||||
|
displayName: 'AmneziaWG 3.0',
|
||||||
|
latency: 155,
|
||||||
|
runtimeAvailable: true,
|
||||||
|
selected: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
// 1. Collapsed state
|
||||||
|
const collapsedEl = renderSections({
|
||||||
|
loading: false,
|
||||||
|
failed: false,
|
||||||
|
section,
|
||||||
|
isCollapsed: true,
|
||||||
|
onTestLatency: vi.fn(),
|
||||||
|
onChooseOutbound: vi.fn(),
|
||||||
|
onCopyOutbound: vi.fn(),
|
||||||
|
onShowUrlTestInfo: vi.fn(),
|
||||||
|
onShowPriorityInfo: vi.fn(),
|
||||||
|
onUpdateSubscription: vi.fn(),
|
||||||
|
latencyFetching: false,
|
||||||
|
subscriptionUpdating: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const titleSec = collapsedEl.querySelector(
|
||||||
|
'.tachyon_dashboard-page__outbound-section__title-section',
|
||||||
|
);
|
||||||
|
expect(titleSec).not.toBeNull();
|
||||||
|
expect(titleSec?.textContent).toContain('AMNEZIA');
|
||||||
|
expect(titleSec?.textContent).toContain('AmneziaWG');
|
||||||
|
expect(titleSec?.textContent).toContain('155ms');
|
||||||
|
expect(titleSec?.textContent).toContain('Check Connection');
|
||||||
|
expect(
|
||||||
|
collapsedEl.querySelector('.tachyon_dashboard-page__outbound-grid'),
|
||||||
|
).toBeNull();
|
||||||
|
|
||||||
|
// 2. Expanded state
|
||||||
|
const expandedEl = renderSections({
|
||||||
|
loading: false,
|
||||||
|
failed: false,
|
||||||
|
section,
|
||||||
|
isCollapsed: false,
|
||||||
|
onTestLatency: vi.fn(),
|
||||||
|
onChooseOutbound: vi.fn(),
|
||||||
|
onCopyOutbound: vi.fn(),
|
||||||
|
onShowUrlTestInfo: vi.fn(),
|
||||||
|
onShowPriorityInfo: vi.fn(),
|
||||||
|
onUpdateSubscription: vi.fn(),
|
||||||
|
latencyFetching: false,
|
||||||
|
subscriptionUpdating: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const expandedTitleSec = expandedEl.querySelector(
|
||||||
|
'.tachyon_dashboard-page__outbound-section__title-section',
|
||||||
|
);
|
||||||
|
expect(expandedTitleSec).not.toBeNull();
|
||||||
|
expect(expandedTitleSec?.textContent).toContain('AMNEZIA');
|
||||||
|
expect(expandedTitleSec?.textContent).toContain('AmneziaWG');
|
||||||
|
expect(expandedTitleSec?.textContent).toContain('Check Connection');
|
||||||
|
|
||||||
|
const grid = expandedEl.querySelector(
|
||||||
|
'.tachyon_dashboard-page__outbound-grid',
|
||||||
|
);
|
||||||
|
expect(grid).not.toBeNull();
|
||||||
|
expect(grid?.textContent).toContain('Connected');
|
||||||
|
expect(grid?.textContent).toContain('AmneziaWG 3.0');
|
||||||
|
expect(grid?.textContent).toContain('155 ms');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1862,6 +1862,18 @@ function formatServiceStatusMessage(msg) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// src/tachyon/tabs/dashboard/partials/renderSections.ts
|
// src/tachyon/tabs/dashboard/partials/renderSections.ts
|
||||||
|
var CONNECTION_ACTION_LABELS = {
|
||||||
|
awg: "AmneziaWG",
|
||||||
|
warp: "WARP",
|
||||||
|
anytls: "AnyTLS",
|
||||||
|
snell: "Snell",
|
||||||
|
mieru: "Mieru",
|
||||||
|
sudoku: "Sudoku",
|
||||||
|
masque: "MASQUE",
|
||||||
|
openvpn: "OpenVPN",
|
||||||
|
fptn: "FPTN",
|
||||||
|
vpn: "VPN"
|
||||||
|
};
|
||||||
function renderFailedState() {
|
function renderFailedState() {
|
||||||
return E(
|
return E(
|
||||||
"div",
|
"div",
|
||||||
@@ -2209,80 +2221,64 @@ function renderDefaultState({
|
|||||||
outboundSwitching ? "tachyon_dashboard-page__outbound-grid__item--switching" : ""
|
outboundSwitching ? "tachyon_dashboard-page__outbound-grid__item--switching" : ""
|
||||||
].filter(Boolean).join(" ");
|
].filter(Boolean).join(" ");
|
||||||
if (isConnectionNode) {
|
if (isConnectionNode) {
|
||||||
|
const statusText = latencyFetching ? _("Checking...") : outbound.latency === -1 || outbound.latency < 0 ? _("Not responding") : outbound.runtimeAvailable ? _("Connected") : _("Not connected");
|
||||||
|
const statusColor = latencyFetching ? "var(--warn-color-medium, orange)" : outbound.latency === -1 || outbound.latency < 0 ? "var(--error-color-medium, red)" : outbound.runtimeAvailable ? "var(--success-color-medium, green)" : "var(--error-color-medium, red)";
|
||||||
|
const latencyDisplay = outbound.latency && outbound.latency > 0 ? `${outbound.latency} ms` : "";
|
||||||
return E(
|
return E(
|
||||||
"div",
|
"div",
|
||||||
{
|
{
|
||||||
class: className,
|
class: className,
|
||||||
style: "display: flex; align-items: center; justify-content: space-between; padding: 12px; min-width: 0; gap: 16px;"
|
style: "display: flex; flex-direction: column; justify-content: space-between; padding: 12px; min-width: 0;"
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
E(
|
E(
|
||||||
"div",
|
"div",
|
||||||
{
|
{
|
||||||
style: "display: flex; align-items: center; gap: 12px; min-width: 0;"
|
class: "tachyon_dashboard-page__outbound-grid__item__header",
|
||||||
|
style: "margin-bottom: 8px;"
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
E(
|
E(
|
||||||
"b",
|
"div",
|
||||||
{
|
{
|
||||||
style: "overflow-wrap: anywhere; word-break: break-all; min-width: 0;"
|
class: "tachyon_dashboard-page__outbound-grid__item__title-wrapper"
|
||||||
|
},
|
||||||
|
[
|
||||||
|
E(
|
||||||
|
"span",
|
||||||
|
{
|
||||||
|
style: `color: ${statusColor}; font-weight: 500; font-size: 13px;`
|
||||||
|
},
|
||||||
|
statusText
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
]
|
||||||
|
),
|
||||||
|
E(
|
||||||
|
"div",
|
||||||
|
{
|
||||||
|
class: "tachyon_dashboard-page__outbound-grid__item__footer",
|
||||||
|
style: "display: flex; align-items: center; justify-content: space-between; gap: 8px;"
|
||||||
|
},
|
||||||
|
[
|
||||||
|
E(
|
||||||
|
"div",
|
||||||
|
{
|
||||||
|
class: "tachyon_dashboard-page__outbound-grid__item__type",
|
||||||
|
style: "overflow: hidden; text-overflow: ellipsis; white-space: nowrap;"
|
||||||
},
|
},
|
||||||
renderFlagEmojis(outbound.displayName)
|
renderFlagEmojis(outbound.displayName)
|
||||||
),
|
),
|
||||||
outbound.prefix ? E(
|
latencyDisplay ? E(
|
||||||
"span",
|
|
||||||
{
|
|
||||||
class: "tachyon_dashboard-page__outbound-grid__item__prefix-badge"
|
|
||||||
},
|
|
||||||
renderFlagEmojis(outbound.prefix)
|
|
||||||
) : "",
|
|
||||||
E(
|
|
||||||
"span",
|
|
||||||
{
|
|
||||||
style: "opacity: 0.7; font-size: 13px; white-space: nowrap; flex-shrink: 0;"
|
|
||||||
},
|
|
||||||
[formatOutboundType(outbound.type, outbound.transport)].filter(
|
|
||||||
Boolean
|
|
||||||
)
|
|
||||||
),
|
|
||||||
E(
|
|
||||||
"div",
|
"div",
|
||||||
{
|
{
|
||||||
class: getLatencyClass(),
|
class: getLatencyClass(),
|
||||||
style: "white-space: nowrap; flex-shrink: 0;"
|
style: "white-space: nowrap; flex-shrink: 0;"
|
||||||
},
|
},
|
||||||
connectionStatusText
|
latencyDisplay
|
||||||
)
|
) : ""
|
||||||
]
|
]
|
||||||
),
|
|
||||||
E(
|
|
||||||
"button",
|
|
||||||
{
|
|
||||||
type: "button",
|
|
||||||
class: "btn dashboard-sections-grid-item-test-latency",
|
|
||||||
style: "padding: 4px 12px; height: 30px; flex-shrink: 0;",
|
|
||||||
disabled: latencyFetching ? true : void 0,
|
|
||||||
click: (event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
if (latencyFetching) return;
|
|
||||||
testLatency();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
latencyFetching ? [
|
|
||||||
renderLoaderCircleIcon24(),
|
|
||||||
E(
|
|
||||||
"span",
|
|
||||||
{
|
|
||||||
class: "dashboard-sections-grid-item-test-latency__label"
|
|
||||||
},
|
|
||||||
_("Checking...")
|
|
||||||
)
|
|
||||||
] : E(
|
|
||||||
"span",
|
|
||||||
{ class: "dashboard-sections-grid-item-test-latency__label" },
|
|
||||||
_("Check Connection")
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
@@ -2435,24 +2431,6 @@ function renderDefaultState({
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (isConnectionNode) {
|
|
||||||
return E(
|
|
||||||
"div",
|
|
||||||
{
|
|
||||||
class: "tachyon_dashboard-page__outbound-section",
|
|
||||||
style: "border: none; padding: 0;"
|
|
||||||
},
|
|
||||||
[
|
|
||||||
E(
|
|
||||||
"div",
|
|
||||||
{
|
|
||||||
style: "display: flex; flex-direction: column; gap: 8px; padding: 0;"
|
|
||||||
},
|
|
||||||
[...section.outbounds.map((outbound) => renderOutbound(outbound))]
|
|
||||||
)
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const metadataNodes = (section.subscriptionMetadata || []).map((metadata) => renderSubscriptionMetadata(metadata)).filter(Boolean);
|
const metadataNodes = (section.subscriptionMetadata || []).map((metadata) => renderSubscriptionMetadata(metadata)).filter(Boolean);
|
||||||
const subscriptionUpdateAction = renderSubscriptionUpdateAction(
|
const subscriptionUpdateAction = renderSubscriptionUpdateAction(
|
||||||
section,
|
section,
|
||||||
@@ -2512,6 +2490,7 @@ function renderDefaultState({
|
|||||||
"masque",
|
"masque",
|
||||||
"openvpn"
|
"openvpn"
|
||||||
].includes(section.action || "");
|
].includes(section.action || "");
|
||||||
|
const connectionTitle = section.action && CONNECTION_ACTION_LABELS[section.action] || selectedOutbound.displayName;
|
||||||
function getLatencyColor() {
|
function getLatencyColor() {
|
||||||
if (isConnectionNode2) {
|
if (isConnectionNode2) {
|
||||||
if (latencyFetching)
|
if (latencyFetching)
|
||||||
@@ -2552,7 +2531,7 @@ function renderDefaultState({
|
|||||||
E(
|
E(
|
||||||
"span",
|
"span",
|
||||||
{ style: "opacity: 0.7;" },
|
{ style: "opacity: 0.7;" },
|
||||||
selectedOutbound.displayName
|
isConnectionNode2 ? connectionTitle : selectedOutbound.prefix || selectedOutbound.displayName
|
||||||
),
|
),
|
||||||
latencyText ? E(
|
latencyText ? E(
|
||||||
"span",
|
"span",
|
||||||
@@ -2561,6 +2540,22 @@ function renderDefaultState({
|
|||||||
) : ""
|
) : ""
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
})() : isConnectionNode ? (() => {
|
||||||
|
const selectedOutbound = section.outbounds.find(
|
||||||
|
(o) => o.selected
|
||||||
|
);
|
||||||
|
if (!selectedOutbound) return "";
|
||||||
|
const connectionTitle = section.action && CONNECTION_ACTION_LABELS[section.action] || selectedOutbound.displayName;
|
||||||
|
if (section.displayName.toLowerCase().trim() === connectionTitle.toLowerCase().trim()) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return E(
|
||||||
|
"span",
|
||||||
|
{
|
||||||
|
style: "font-size: 13px; font-weight: normal; margin-left: 8px; opacity: 0.7;"
|
||||||
|
},
|
||||||
|
connectionTitle
|
||||||
|
);
|
||||||
})() : ""
|
})() : ""
|
||||||
]
|
]
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ msgid ""
|
|||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: TACHYON\n"
|
"Project-Id-Version: TACHYON\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2026-09-17 18:55+0300\n"
|
"POT-Creation-Date: 2026-09-17 19:42+0300\n"
|
||||||
"PO-Revision-Date: 2026-09-17 18:55+0300\n"
|
"PO-Revision-Date: 2026-09-17 19:42+0300\n"
|
||||||
"Last-Translator: Dushnilin\n"
|
"Last-Translator: Dushnilin\n"
|
||||||
"Language-Team: none\n"
|
"Language-Team: none\n"
|
||||||
"Language: ru\n"
|
"Language: ru\n"
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -22,6 +22,7 @@ function config(ctx) {
|
|||||||
mark_mask: env_get("TACHYON_FPTN_MARK_MASK", "0x00ff0000"),
|
mark_mask: env_get("TACHYON_FPTN_MARK_MASK", "0x00ff0000"),
|
||||||
rule_priority: env_get("TACHYON_FPTN_RULE_PRIORITY", "102"),
|
rule_priority: env_get("TACHYON_FPTN_RULE_PRIORITY", "102"),
|
||||||
state_dir: state_dir,
|
state_dir: state_dir,
|
||||||
|
shims_dir: state_dir + "/bin",
|
||||||
pid_file: state_dir + "/fptn.pid",
|
pid_file: state_dir + "/fptn.pid",
|
||||||
log_file: state_dir + "/fptn.log",
|
log_file: state_dir + "/fptn.log",
|
||||||
package_name: "fptn",
|
package_name: "fptn",
|
||||||
|
|||||||
@@ -86,6 +86,87 @@ function package_version() {
|
|||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function install_shims() {
|
||||||
|
let shims_dir = cfg.shims_dir || (cfg.state_dir + "/bin");
|
||||||
|
command_status("mkdir -p " + shell_quote(shims_dir) + " 2>/dev/null");
|
||||||
|
|
||||||
|
let ip_shim = "#!/bin/sh\n" +
|
||||||
|
"REAL_IP=''\n" +
|
||||||
|
"for p in /sbin/ip /usr/sbin/ip /usr/bin/ip /bin/ip; do\n" +
|
||||||
|
" if [ -x \"$p\" ] && [ \"$p\" != \"$0\" ]; then\n" +
|
||||||
|
" REAL_IP=\"$p\"\n" +
|
||||||
|
" break\n" +
|
||||||
|
" fi\n" +
|
||||||
|
"done\n" +
|
||||||
|
"[ -z \"$REAL_IP\" ] && REAL_IP='ip'\n" +
|
||||||
|
"CMD_LINE=\"$*\"\n" +
|
||||||
|
"case \"$CMD_LINE\" in\n" +
|
||||||
|
" *route*default*|*default*route*)\n" +
|
||||||
|
" case \"$CMD_LINE\" in\n" +
|
||||||
|
" *replace*|*add*|*del*)\n" +
|
||||||
|
" exit 0\n" +
|
||||||
|
" ;;\n" +
|
||||||
|
" esac\n" +
|
||||||
|
" ;;\n" +
|
||||||
|
"esac\n" +
|
||||||
|
"exec \"$REAL_IP\" \"$@\"\n";
|
||||||
|
fs.writefile(shims_dir + "/ip", ip_shim);
|
||||||
|
command_status("chmod +x " + shell_quote(shims_dir + "/ip") + " 2>/dev/null");
|
||||||
|
|
||||||
|
let sed_shim = "#!/bin/sh\n" +
|
||||||
|
"REAL_SED=''\n" +
|
||||||
|
"for p in /bin/sed /usr/bin/sed /sbin/sed /usr/sbin/sed; do\n" +
|
||||||
|
" if [ -x \"$p\" ] && [ \"$p\" != \"$0\" ]; then\n" +
|
||||||
|
" REAL_SED=\"$p\"\n" +
|
||||||
|
" break\n" +
|
||||||
|
" fi\n" +
|
||||||
|
"done\n" +
|
||||||
|
"[ -z \"$REAL_SED\" ] && REAL_SED='sed'\n" +
|
||||||
|
"case \"$*\" in\n" +
|
||||||
|
" *resolv.conf*)\n" +
|
||||||
|
" exit 0\n" +
|
||||||
|
" ;;\n" +
|
||||||
|
"esac\n" +
|
||||||
|
"exec \"$REAL_SED\" \"$@\"\n";
|
||||||
|
fs.writefile(shims_dir + "/sed", sed_shim);
|
||||||
|
command_status("chmod +x " + shell_quote(shims_dir + "/sed") + " 2>/dev/null");
|
||||||
|
|
||||||
|
let dummy_names = ["iptables", "ip6tables", "resolvectl", "chattr", "systemctl"];
|
||||||
|
let dummy_shim = "#!/bin/sh\nexit 0\n";
|
||||||
|
for (let idx, name in dummy_names) {
|
||||||
|
fs.writefile(shims_dir + "/" + name, dummy_shim);
|
||||||
|
command_status("chmod +x " + shell_quote(shims_dir + "/" + name) + " 2>/dev/null");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function sanitize_system() {
|
||||||
|
command_status("chattr -i /etc/resolv.conf 2>/dev/null; true");
|
||||||
|
|
||||||
|
if (fs.stat("/etc/resolv.conf") == null) {
|
||||||
|
if (fs.stat("/tmp/resolv.conf") != null)
|
||||||
|
command_status("ln -sf /tmp/resolv.conf /etc/resolv.conf 2>/dev/null; true");
|
||||||
|
else if (fs.stat("/tmp/resolv.conf.d/resolv.conf.auto") != null)
|
||||||
|
command_status("ln -sf /tmp/resolv.conf.d/resolv.conf.auto /etc/resolv.conf 2>/dev/null; true");
|
||||||
|
}
|
||||||
|
|
||||||
|
command_status("sed -i '/nameserver 172\\.20\\./d' /etc/resolv.conf 2>/dev/null; true");
|
||||||
|
|
||||||
|
for (let i = 0; i < 4; i++) {
|
||||||
|
command_status("iptables -D OUTPUT -p udp --dport 53 -m comment --comment fptn -j DROP 2>/dev/null; true");
|
||||||
|
command_status("iptables -D OUTPUT -p tcp --dport 53 -m comment --comment fptn -j DROP 2>/dev/null; true");
|
||||||
|
command_status("iptables -D OUTPUT -p udp --dport 853 -m comment --comment fptn -j DROP 2>/dev/null; true");
|
||||||
|
command_status("iptables -D OUTPUT -p tcp --dport 853 -m comment --comment fptn -j DROP 2>/dev/null; true");
|
||||||
|
}
|
||||||
|
|
||||||
|
command_status("uci del_list dhcp.@dnsmasq[0].server='172.20.0.1' 2>/dev/null; uci commit dhcp 2>/dev/null; true");
|
||||||
|
|
||||||
|
let main_default = trim(command_output("ip -4 route show table main default 2>/dev/null | grep dev | grep " + shell_quote(cfg.tun_interface) + " || true"));
|
||||||
|
if (main_default != "") {
|
||||||
|
log_message("Sanitizing rogue default route dev " + cfg.tun_interface + " in table main", "warn");
|
||||||
|
command_status("ip -4 route del default dev " + shell_quote(cfg.tun_interface) + " table main 2>/dev/null; true");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function remove_kernel_routing() {
|
function remove_kernel_routing() {
|
||||||
let p = cfg.rule_priority || "102";
|
let p = cfg.rule_priority || "102";
|
||||||
let mark_spec = cfg.fwmark + "/" + cfg.mark_mask;
|
let mark_spec = cfg.fwmark + "/" + cfg.mark_mask;
|
||||||
@@ -99,6 +180,8 @@ function remove_kernel_routing() {
|
|||||||
command_status("ip -4 route del default dev " + shell_quote(cfg.tun_interface) + " table main 2>/dev/null; true");
|
command_status("ip -4 route del default dev " + shell_quote(cfg.tun_interface) + " table main 2>/dev/null; true");
|
||||||
if (command_status("ip link show " + shell_quote(cfg.tun_interface) + " >/dev/null 2>&1") == 0)
|
if (command_status("ip link show " + shell_quote(cfg.tun_interface) + " >/dev/null 2>&1") == 0)
|
||||||
command_status("ip link set dev " + shell_quote(cfg.tun_interface) + " down 2>/dev/null; true");
|
command_status("ip link set dev " + shell_quote(cfg.tun_interface) + " down 2>/dev/null; true");
|
||||||
|
|
||||||
|
sanitize_system();
|
||||||
}
|
}
|
||||||
|
|
||||||
function install_kernel_routing() {
|
function install_kernel_routing() {
|
||||||
@@ -119,12 +202,7 @@ function install_kernel_routing() {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure table main default route was not hijacked by fptn-client-cli
|
sanitize_system();
|
||||||
let main_default_dev = trim(command_output("ip -4 route show table main default 2>/dev/null | awk '{print $5; exit}'"));
|
|
||||||
if (main_default_dev == cfg.tun_interface) {
|
|
||||||
log_message("FPTN client hijacked table main default route; restoring WAN route", "warn");
|
|
||||||
command_status("ip -4 route del default dev " + shell_quote(cfg.tun_interface) + " table main 2>/dev/null; true");
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -161,6 +239,7 @@ function launch_fptn_process(section) {
|
|||||||
return false;
|
return false;
|
||||||
|
|
||||||
command_status("mkdir -p " + shell_quote(cfg.state_dir) + " 2>/dev/null");
|
command_status("mkdir -p " + shell_quote(cfg.state_dir) + " 2>/dev/null");
|
||||||
|
install_shims();
|
||||||
|
|
||||||
let cmd_args = [
|
let cmd_args = [
|
||||||
cfg.binary,
|
cfg.binary,
|
||||||
@@ -187,7 +266,9 @@ function launch_fptn_process(section) {
|
|||||||
push(cmd_args, preferred_server);
|
push(cmd_args, preferred_server);
|
||||||
}
|
}
|
||||||
|
|
||||||
let cmd_str = command_from_args(cmd_args) + " >> " + shell_quote(cfg.log_file) + " 2>&1 & echo $! > " + shell_quote(cfg.pid_file);
|
let shims_dir = cfg.shims_dir || (cfg.state_dir + "/bin");
|
||||||
|
let cmd_str = "PATH=" + shell_quote(shims_dir) + ":$PATH " +
|
||||||
|
command_from_args(cmd_args) + " >> " + shell_quote(cfg.log_file) + " 2>&1 & echo $! > " + shell_quote(cfg.pid_file);
|
||||||
log_message("Starting FPTN client on interface " + cfg.tun_interface, "info");
|
log_message("Starting FPTN client on interface " + cfg.tun_interface, "info");
|
||||||
system(cmd_str);
|
system(cmd_str);
|
||||||
return true;
|
return true;
|
||||||
@@ -246,8 +327,10 @@ function ensure_routing() {
|
|||||||
if (p != null && tun_up) {
|
if (p != null && tun_up) {
|
||||||
let route_installed = command_status("ip route show table " + cfg.route_table + " default dev " + shell_quote(cfg.tun_interface) + " 2>/dev/null | grep -q default") == 0;
|
let route_installed = command_status("ip route show table " + cfg.route_table + " default dev " + shell_quote(cfg.tun_interface) + " 2>/dev/null | grep -q default") == 0;
|
||||||
let rule_installed = command_status("ip rule show 2>/dev/null | grep -q " + shell_quote(cfg.route_table)) == 0;
|
let rule_installed = command_status("ip rule show 2>/dev/null | grep -q " + shell_quote(cfg.route_table)) == 0;
|
||||||
if (route_installed && rule_installed)
|
if (route_installed && rule_installed) {
|
||||||
|
sanitize_system();
|
||||||
return true;
|
return true;
|
||||||
|
}
|
||||||
return install_kernel_routing();
|
return install_kernel_routing();
|
||||||
}
|
}
|
||||||
return start_runtime();
|
return start_runtime();
|
||||||
@@ -379,7 +462,9 @@ function module_exports() {
|
|||||||
ensure_routing: ensure_routing,
|
ensure_routing: ensure_routing,
|
||||||
supervise_runtime: supervise_runtime,
|
supervise_runtime: supervise_runtime,
|
||||||
status_json: status_json,
|
status_json: status_json,
|
||||||
check_json: check_json
|
check_json: check_json,
|
||||||
|
install_shims: install_shims,
|
||||||
|
sanitize_system: sanitize_system
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -399,6 +484,14 @@ else if (mode == "ensure-routing")
|
|||||||
exit(ensure_routing() ? 0 : 1);
|
exit(ensure_routing() ? 0 : 1);
|
||||||
else if (mode == "supervise-runtime")
|
else if (mode == "supervise-runtime")
|
||||||
exit(supervise_runtime() ? 0 : 1);
|
exit(supervise_runtime() ? 0 : 1);
|
||||||
|
else if (mode == "install-shims") {
|
||||||
|
install_shims();
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
else if (mode == "sanitize-system") {
|
||||||
|
sanitize_system();
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
else if (mode == "status")
|
else if (mode == "status")
|
||||||
status_json();
|
status_json();
|
||||||
else if (mode == "check")
|
else if (mode == "check")
|
||||||
|
|||||||
@@ -154,5 +154,36 @@ TACHYON_LIB="$TACHYON_LIB" ucode -L "$TACHYON_LIB" "$ROOT_DIR/tachyon/files/usr/
|
|||||||
TG_MASK_OUT="$(ucode -L "$TACHYON_LIB" "$ROOT_DIR/tachyon/files/usr/lib/service/telegram.uc" mask-token "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9Q7Fx")"
|
TG_MASK_OUT="$(ucode -L "$TACHYON_LIB" "$ROOT_DIR/tachyon/files/usr/lib/service/telegram.uc" mask-token "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9Q7Fx")"
|
||||||
assert_eq "••••••••Q7Fx" "$TG_MASK_OUT" "telegram token masking"
|
assert_eq "••••••••Q7Fx" "$TG_MASK_OUT" "telegram token masking"
|
||||||
|
|
||||||
|
# 9. Test shims creation and isolation behavior
|
||||||
|
export TACHYON_FPTN_STATE_DIR="$WORK_DIR/fptn_state"
|
||||||
|
mkdir -p "$TACHYON_FPTN_STATE_DIR"
|
||||||
|
TACHYON_LIB_DIR="$TACHYON_LIB" ucode_run "$FPTN_RUNTIME_UC" install-shims
|
||||||
|
|
||||||
|
SHIMS_DIR="$TACHYON_FPTN_STATE_DIR/bin"
|
||||||
|
[ -x "$SHIMS_DIR/ip" ] || fail "ip shim not created or not executable"
|
||||||
|
[ -x "$SHIMS_DIR/sed" ] || fail "sed shim not created or not executable"
|
||||||
|
[ -x "$SHIMS_DIR/iptables" ] || fail "iptables shim not created or not executable"
|
||||||
|
[ -x "$SHIMS_DIR/chattr" ] || fail "chattr shim not created or not executable"
|
||||||
|
|
||||||
|
# Test ip shim blocks default route replace/del/add
|
||||||
|
IP_REPLACE_STATUS=0
|
||||||
|
"$SHIMS_DIR/ip" route replace default dev tun-fptn scope link || IP_REPLACE_STATUS=$?
|
||||||
|
assert_eq "0" "$IP_REPLACE_STATUS" "ip shim should return 0 for route replace default"
|
||||||
|
|
||||||
|
IP_DEL_STATUS=0
|
||||||
|
"$SHIMS_DIR/ip" route del default dev tun-fptn scope link || IP_DEL_STATUS=$?
|
||||||
|
assert_eq "0" "$IP_DEL_STATUS" "ip shim should return 0 for route del default"
|
||||||
|
|
||||||
|
# Test sed shim blocks modifying /etc/resolv.conf
|
||||||
|
SED_STATUS=0
|
||||||
|
"$SHIMS_DIR/sed" -i '1i nameserver 172.20.0.1' /etc/resolv.conf || SED_STATUS=$?
|
||||||
|
assert_eq "0" "$SED_STATUS" "sed shim should return 0 for resolv.conf"
|
||||||
|
|
||||||
|
# Test dummy shims exit 0
|
||||||
|
IPT_STATUS=0
|
||||||
|
"$SHIMS_DIR/iptables" -A OUTPUT -p udp --dport 53 -j DROP || IPT_STATUS=$?
|
||||||
|
assert_eq "0" "$IPT_STATUS" "iptables shim should return 0"
|
||||||
|
|
||||||
echo "fptn component tests passed"
|
echo "fptn component tests passed"
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user