fix(diagnostics): prevent tunnel IP pollution in WAN addresses and exempt Tailscale inbounds from public WAN warning
This commit is contained in:
@@ -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) {
|
||||
|
||||
+713
-697
File diff suppressed because it is too large
Load Diff
+709
-699
File diff suppressed because it is too large
Load Diff
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -599,6 +599,7 @@ export namespace Tachyon {
|
||||
config_path: string;
|
||||
wan_ip: string;
|
||||
wan_public: 0 | 1;
|
||||
requires_public_wan?: 0 | 1;
|
||||
items: InboundCheckItem[];
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user