fix(diagnostics): prevent tunnel IP pollution in WAN addresses and exempt Tailscale inbounds from public WAN warning

This commit is contained in:
Dushnilin
2026-09-16 10:07:09 +03:00
parent 4d6892e733
commit 3834b783cc
14 changed files with 2528 additions and 2136 deletions
+2
View File
@@ -1376,6 +1376,8 @@ const dictionary = {
"Optional: preferred FPTN server address": "Опционально: предпочитаемый адрес сервера FPTN",
"stability (3x)": "стабильность (3x)",
"Stage 1: Exploration Scan": "Этап 1: Разведочное сканирование",
"Tailscale does not require public WAN": "Tailscale не требует публичного WAN",
"Public IP not required for this configuration": "Публичный IP не требуется для этой конфигурации",
};
function escapePoString(str) {
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+8 -2
View File
@@ -7,8 +7,8 @@ msgid ""
msgstr ""
"Project-Id-Version: TACHYON\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-09-16 09:44+0300\n"
"PO-Revision-Date: 2026-09-16 09:44+0300\n"
"POT-Creation-Date: 2026-09-16 10:05+0300\n"
"PO-Revision-Date: 2026-09-16 10:05+0300\n"
"Last-Translator: Dushnilin\n"
"Language-Team: none\n"
"Language: ru\n"
@@ -3917,6 +3917,9 @@ msgstr "Публичный хост"
msgid "Public IP Address Isolation"
msgstr "Изоляция публичного IP-адреса"
msgid "Public IP not required for this configuration"
msgstr "Публичный IP не требуется для этой конфигурации"
msgid "QoS Priority Engine (DSCP)"
msgstr "Движок приоритета QoS (DSCP)"
@@ -5081,6 +5084,9 @@ msgstr "Ключ авторизации Tailscale"
msgid "Tailscale control URL"
msgstr "Control URL Tailscale"
msgid "Tailscale does not require public WAN"
msgstr "Tailscale не требует публичного WAN"
msgid "Tailscale endpoint"
msgstr "Tailscale endpoint"
@@ -192,11 +192,35 @@ export async function runInboundsCheck() {
return;
}
const requiresPublicWan =
data.requires_public_wan !== undefined
? Boolean(data.requires_public_wan)
: data.items.some(
(item) =>
item.protocol !== 'tailscale' && item.protocol !== 'json_inbound',
);
let wanState: IDiagnosticsChecksItem['state'] = 'warning';
let wanValue = data.wan_ip || _('Not detected');
if (data.wan_public) {
wanState = 'success';
} else if (!requiresPublicWan) {
wanState = 'success';
const isOnlyTailscale = data.items.every(
(item) => item.protocol === 'tailscale',
);
const note = isOnlyTailscale
? _('Tailscale does not require public WAN')
: _('Public IP not required for this configuration');
wanValue = data.wan_ip ? `${data.wan_ip} (${note})` : note;
}
const items: IDiagnosticsChecksItem[] = [
{
state: data.wan_public ? 'success' : 'warning',
state: wanState,
key: _('WAN public IP'),
value: data.wan_ip || _('Not detected'),
value: wanValue,
},
];
@@ -0,0 +1,151 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
const mocks = vi.hoisted(() => ({
checkInbounds: vi.fn(),
updateCheckStore: vi.fn(),
}));
vi.mock('../../../../methods', () => ({
TachyonShellMethods: {
checkInbounds: mocks.checkInbounds,
},
}));
vi.mock('../updateCheckStore', () => ({
updateCheckStore: mocks.updateCheckStore,
}));
import { runInboundsCheck } from '../runInboundsCheck';
describe('runInboundsCheck', () => {
beforeEach(() => {
mocks.checkInbounds.mockReset();
mocks.updateCheckStore.mockReset();
});
it('marks check as success for Tailscale inbound behind private WAN IP', async () => {
mocks.checkInbounds.mockResolvedValue({
success: true,
data: {
enabled_count: 1,
config_path: '/etc/sing-box/config.json',
wan_ip: '192.168.1.60',
wan_public: 0,
requires_public_wan: 0,
items: [
{
section: 'Head',
label: 'Head',
protocol: 'tailscale',
routing_mode: 'rules',
tag: 'server-Head-in',
listen: '0.0.0.0',
listen_port: 0,
public_host: '',
public_host_ips: '',
expected_type: 'tailscale',
required_proto: 'tcp',
runtime_exists: 1,
runtime_type: 'tailscale',
runtime_listen: '',
runtime_port: 0,
runtime_ok: 1,
listening: -1,
firewall_required: 0,
firewall_open: -1,
port_conflict: 0,
port_conflict_owners: '',
routes_configured: 1,
public_host_resolved: -1,
public_host_public: -1,
public_host_matches_wan: -1,
},
],
},
});
await runInboundsCheck();
const calls = mocks.updateCheckStore.mock.calls;
const lastCall = calls[calls.length - 1]?.[0];
expect(lastCall).toBeDefined();
expect(lastCall.state).toBe('success');
expect(lastCall.items[0].state).toBe('success');
expect(lastCall.items[0].value).toContain('192.168.1.60');
expect(lastCall.items[0].value).toContain(
'Tailscale does not require public WAN',
);
});
it('marks check as warning when public inbound has private WAN IP', async () => {
mocks.checkInbounds.mockResolvedValue({
success: true,
data: {
enabled_count: 1,
config_path: '/etc/sing-box/config.json',
wan_ip: '192.168.1.60',
wan_public: 0,
requires_public_wan: 1,
items: [
{
section: 'vless_srv',
label: 'Vless',
protocol: 'vless',
routing_mode: 'rules',
tag: 'server-vless-in',
listen: '0.0.0.0',
listen_port: 443,
public_host: '',
public_host_ips: '',
expected_type: 'vless',
required_proto: 'tcp',
runtime_exists: 1,
runtime_type: 'vless',
runtime_listen: '0.0.0.0',
runtime_port: 443,
runtime_ok: 1,
listening: 1,
firewall_required: 1,
firewall_open: 1,
port_conflict: 0,
port_conflict_owners: '',
routes_configured: 1,
public_host_resolved: -1,
public_host_public: -1,
public_host_matches_wan: -1,
},
],
},
});
await runInboundsCheck();
const calls = mocks.updateCheckStore.mock.calls;
const lastCall = calls[calls.length - 1]?.[0];
expect(lastCall).toBeDefined();
expect(lastCall.state).toBe('warning');
expect(lastCall.items[0].state).toBe('warning');
expect(lastCall.items[0].value).toBe('192.168.1.60');
});
it('skips check when no inbounds are enabled', async () => {
mocks.checkInbounds.mockResolvedValue({
success: true,
data: {
enabled_count: 0,
config_path: '/etc/sing-box/config.json',
wan_ip: '192.168.1.60',
wan_public: 0,
requires_public_wan: 0,
items: [],
},
});
await runInboundsCheck();
const calls = mocks.updateCheckStore.mock.calls;
const lastCall = calls[calls.length - 1]?.[0];
expect(lastCall).toBeDefined();
expect(lastCall.state).toBe('skipped');
});
});
+1
View File
@@ -599,6 +599,7 @@ export namespace Tachyon {
config_path: string;
wan_ip: string;
wan_public: 0 | 1;
requires_public_wan?: 0 | 1;
items: InboundCheckItem[];
}