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[];
|
||||
}
|
||||
|
||||
|
||||
@@ -10713,11 +10713,26 @@ async function runInboundsCheck() {
|
||||
});
|
||||
return;
|
||||
}
|
||||
const requiresPublicWan = data.requires_public_wan !== void 0 ? Boolean(data.requires_public_wan) : data.items.some(
|
||||
(item) => item.protocol !== "tailscale" && item.protocol !== "json_inbound"
|
||||
);
|
||||
let wanState = "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 = [
|
||||
{
|
||||
state: data.wan_public ? "success" : "warning",
|
||||
state: wanState,
|
||||
key: _("WAN public IP"),
|
||||
value: data.wan_ip || _("Not detected")
|
||||
value: wanValue
|
||||
}
|
||||
];
|
||||
data.items.forEach((item) => {
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -67,14 +67,25 @@ function is_public_dns_resolver(ip, name, asn) {
|
||||
* Prioritizes actual routing table default gateway device (e.g. pppoe-wan, eth1, br-wan),
|
||||
* avoiding IP-less physical interfaces from UCI network.wan.device under PPPoE/VLANs.
|
||||
*/
|
||||
function is_virtual_or_tunnel_iface(dev) {
|
||||
dev = trim(as_string(dev));
|
||||
if (dev == "" || dev == "lo")
|
||||
return true;
|
||||
if (match(dev, /^(tun|tap|tailscale|wg|docker|veth|br-|dummy|gre|sit|ifb)/))
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
function get_wan_interface() {
|
||||
// 1. Prefer default routing table lookup: the device carrying the default route
|
||||
// is guaranteed to be the active L3 interface.
|
||||
let route_res = command_capture("ip -4 route show default 2>/dev/null");
|
||||
if (route_res && route_res.status == 0 && route_res.output != "") {
|
||||
let m = match(route_res.output, /dev\s+([a-zA-Z0-9_\.\-]+)/);
|
||||
if (m && m[1])
|
||||
return trim(as_string(m[1]));
|
||||
for (let line in split(route_res.output, "\n")) {
|
||||
let m = match(line, /dev\s+([a-zA-Z0-9_\.\-]+)/);
|
||||
if (m && m[1] && !is_virtual_or_tunnel_iface(m[1]))
|
||||
return trim(as_string(m[1]));
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Fallback to UCI network inspection
|
||||
@@ -83,7 +94,7 @@ function get_wan_interface() {
|
||||
if (cursor) {
|
||||
cursor.load("network");
|
||||
let dev = cursor.get("network", "wan", "device") || cursor.get("network", "wan", "ifname");
|
||||
if (dev != null && dev != "")
|
||||
if (dev != null && dev != "" && !is_virtual_or_tunnel_iface(dev))
|
||||
return trim(as_string(dev));
|
||||
}
|
||||
} catch (e) {
|
||||
|
||||
@@ -199,11 +199,25 @@ function network_status_ip_addresses(data, key) {
|
||||
return result;
|
||||
}
|
||||
|
||||
function is_virtual_or_tunnel_iface(dev) {
|
||||
dev = trim(as_string(dev));
|
||||
if (dev == "" || dev == "lo")
|
||||
return true;
|
||||
if (match(dev, /^(tun|tap|tailscale|wg|docker|veth|br-|dummy|gre|sit|ifb)/))
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
function get_wan_ip_addresses() {
|
||||
let result = [];
|
||||
let seen = {};
|
||||
|
||||
for (let interface in [ "wan", "wwan" ]) {
|
||||
let ifaces = [ "wan", "wan6", "wwan", "wwan6" ];
|
||||
let custom_iface = trim(as_string(uci_core.get(CONFIG_NAME + ".settings.output_network_interface")));
|
||||
if (custom_iface != "" && !is_virtual_or_tunnel_iface(custom_iface))
|
||||
unshift(ifaces, custom_iface);
|
||||
|
||||
for (let interface in ifaces) {
|
||||
let data = command_output_from_args([
|
||||
"ubus", "-S", "call", "network.interface." + interface, "status"
|
||||
]);
|
||||
@@ -213,47 +227,56 @@ function get_wan_ip_addresses() {
|
||||
push_unique(result, seen, ip);
|
||||
}
|
||||
|
||||
let route = command_output_from_args([ "ip", "-4", "route", "show", "default" ]);
|
||||
let fields = words(route);
|
||||
let iface = "";
|
||||
for (let i = 0; i + 1 < length(fields); i++) {
|
||||
if (fields[i] == "dev") {
|
||||
iface = fields[i + 1];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (iface == "")
|
||||
return "";
|
||||
if (length(result) > 0)
|
||||
return join(" ", result);
|
||||
|
||||
let addr = command_output_from_args([ "ip", "-4", "addr", "show", "dev", iface ]);
|
||||
for (let line in split(addr, "\n")) {
|
||||
line = trim(as_string(line));
|
||||
let matched = match(line, /^inet[ \t]+([0-9.]+)\//);
|
||||
if (matched != null)
|
||||
push_unique(result, seen, matched[1]);
|
||||
let route = command_output_from_args([ "ip", "-4", "route", "show", "default" ]);
|
||||
for (let line in split(route, "\n")) {
|
||||
let m = match(line, /dev\s+([a-zA-Z0-9_\.\-]+)/);
|
||||
if (!m || !m[1])
|
||||
continue;
|
||||
let iface = m[1];
|
||||
if (is_virtual_or_tunnel_iface(iface))
|
||||
continue;
|
||||
|
||||
let addr = command_output_from_args([ "ip", "-4", "addr", "show", "dev", iface ]);
|
||||
for (let l in split(addr, "\n")) {
|
||||
l = trim(as_string(l));
|
||||
let matched = match(l, /^inet[ \t]+([0-9.]+)\//);
|
||||
if (matched != null)
|
||||
push_unique(result, seen, matched[1]);
|
||||
}
|
||||
if (length(result) > 0)
|
||||
break;
|
||||
}
|
||||
|
||||
return join(" ", result);
|
||||
}
|
||||
|
||||
function get_wan_interface() {
|
||||
let iface = trim(as_string(uci_core.get(CONFIG_NAME + ".settings.output_network_interface")));
|
||||
if (iface != "") return iface;
|
||||
if (iface != "" && !is_virtual_or_tunnel_iface(iface)) return iface;
|
||||
|
||||
iface = trim(as_string(uci_core.get("network.wan.device")));
|
||||
if (iface != "") return iface;
|
||||
if (iface != "" && !is_virtual_or_tunnel_iface(iface)) return iface;
|
||||
|
||||
iface = trim(as_string(uci_core.get("network.wan.ifname")));
|
||||
if (iface != "") return iface;
|
||||
if (iface != "" && !is_virtual_or_tunnel_iface(iface)) return iface;
|
||||
|
||||
let ubus_data = command_output_from_args([ "ubus", "-S", "call", "network.interface.wan", "status" ]);
|
||||
let wan_stat = parse_json_or_null(ubus_data);
|
||||
if (type(wan_stat) == "object") {
|
||||
let dev = wan_stat.l3_device || wan_stat.device;
|
||||
if (dev && !is_virtual_or_tunnel_iface(dev))
|
||||
return dev;
|
||||
}
|
||||
|
||||
let route = command_output_from_args([ "ip", "-4", "route", "show", "default" ]);
|
||||
let fields = words(route);
|
||||
for (let i = 0; i + 1 < length(fields); i++) {
|
||||
if (fields[i] == "dev") {
|
||||
iface = fields[i + 1];
|
||||
break;
|
||||
}
|
||||
for (let line in split(route, "\n")) {
|
||||
let m = match(line, /dev\s+([a-zA-Z0-9_\.\-]+)/);
|
||||
if (m && m[1] && !is_virtual_or_tunnel_iface(m[1]))
|
||||
return m[1];
|
||||
}
|
||||
if (iface != "") return iface;
|
||||
|
||||
return "eth0";
|
||||
}
|
||||
@@ -684,6 +707,7 @@ function check_inbounds() {
|
||||
}
|
||||
let items = [];
|
||||
let enabled_count = 0;
|
||||
let requires_public_wan = 0;
|
||||
|
||||
for (let section in uci_sections("server")) {
|
||||
if (!bool_option(section, "enabled", false))
|
||||
@@ -693,6 +717,8 @@ function check_inbounds() {
|
||||
let section_name = as_string(section[".name"] || "");
|
||||
let label = option(section, "label", section_name);
|
||||
let protocol = option(section, "protocol", "vless");
|
||||
if (protocol != "tailscale" && protocol != "json_inbound")
|
||||
requires_public_wan = 1;
|
||||
let listen = option(section, "listen", "0.0.0.0");
|
||||
let listen_port = option(section, "listen_port", "");
|
||||
let public_host = option(section, "public_host", "");
|
||||
@@ -762,6 +788,7 @@ function check_inbounds() {
|
||||
config_path: sing_box_config_path,
|
||||
wan_ip,
|
||||
wan_public,
|
||||
requires_public_wan,
|
||||
items
|
||||
});
|
||||
return 0;
|
||||
|
||||
@@ -1261,20 +1261,37 @@ function render_global_inbounds_check() {
|
||||
let enabled_count = number_value(value.enabled_count);
|
||||
let wan_ip = as_string(value.wan_ip || "");
|
||||
let wan_public = number_value(value.wan_public);
|
||||
let requires_public_wan = value.requires_public_wan != null
|
||||
? number_value(value.requires_public_wan)
|
||||
: null;
|
||||
|
||||
if (enabled_count == 0) {
|
||||
print_line("[OK] No enabled server inbounds");
|
||||
return;
|
||||
}
|
||||
|
||||
let items = type(value.items) == "array" ? value.items : [];
|
||||
if (requires_public_wan == null) {
|
||||
requires_public_wan = 0;
|
||||
for (let i = 0; i < enabled_count; i++) {
|
||||
let item = type(items[i]) == "object" ? items[i] : {};
|
||||
let proto = as_string(item.protocol || "");
|
||||
if (proto != "tailscale" && proto != "json_inbound") {
|
||||
requires_public_wan = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (wan_public == 1)
|
||||
print_line("[OK] WAN public IP: " + wan_ip);
|
||||
else if (!requires_public_wan)
|
||||
print_line("[OK] WAN IP: " + (wan_ip != "" ? wan_ip : "private") + " (Tailscale does not require public WAN)");
|
||||
else if (wan_ip != "")
|
||||
print_line("[WARN] WAN IP is not public: " + wan_ip);
|
||||
else
|
||||
print_line("[WARN] WAN IP was not detected");
|
||||
|
||||
let items = type(value.items) == "array" ? value.items : [];
|
||||
for (let i = 0; i < enabled_count; i++)
|
||||
render_inbound_item(type(items[i]) == "object" ? items[i] : {}, wan_ip);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
#!/usr/bin/env bash
|
||||
set -eo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
TACHYON_LIB="$ROOT_DIR/tachyon/files/usr/lib"
|
||||
DIAGNOSTICS_RUNTIME="$TACHYON_LIB/diagnostics/runtime.uc"
|
||||
STATUS_UC="$TACHYON_LIB/diagnostics/status.uc"
|
||||
LEAK_CHECK="$TACHYON_LIB/diagnostics/leak_check.uc"
|
||||
WORK_DIR="$(mktemp -d)"
|
||||
|
||||
cleanup() {
|
||||
rm -rf "$WORK_DIR"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
fail() {
|
||||
printf 'FAIL: %s\n' "$1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
# 1. Syntax check
|
||||
ucode -L "$TACHYON_LIB" -c "$DIAGNOSTICS_RUNTIME" || fail "runtime.uc syntax error"
|
||||
ucode -S -L "$TACHYON_LIB" -c "$DIAGNOSTICS_RUNTIME" || fail "runtime.uc strict syntax error"
|
||||
ucode -L "$TACHYON_LIB" -c "$STATUS_UC" || fail "status.uc syntax error"
|
||||
ucode -S -L "$TACHYON_LIB" -c "$STATUS_UC" || fail "status.uc strict syntax error"
|
||||
ucode -L "$TACHYON_LIB" -c "$LEAK_CHECK" || fail "leak_check.uc syntax error"
|
||||
ucode -S -L "$TACHYON_LIB" -c "$LEAK_CHECK" || fail "leak_check.uc strict syntax error"
|
||||
|
||||
# 2. Test check-inbounds runtime mode
|
||||
check_out="$(ucode -L "$TACHYON_LIB" "$DIAGNOSTICS_RUNTIME" check-inbounds 2>/dev/null || true)"
|
||||
[ -n "$check_out" ] || fail "check-inbounds should return JSON output"
|
||||
|
||||
JSON_VAL="$check_out" node - <<'NODE'
|
||||
const data = JSON.parse(process.env.JSON_VAL);
|
||||
if (typeof data.enabled_count !== 'number' || typeof data.requires_public_wan !== 'number') {
|
||||
console.error("Invalid check-inbounds response:", data);
|
||||
process.exit(1);
|
||||
}
|
||||
NODE
|
||||
|
||||
# 3. Test render_global_inbounds_check in status.uc for Tailscale
|
||||
cat >"$WORK_DIR/test_tailscale.json" <<'EOF'
|
||||
{
|
||||
"enabled_count": 1,
|
||||
"wan_ip": "192.168.1.60",
|
||||
"wan_public": 0,
|
||||
"requires_public_wan": 0,
|
||||
"items": [
|
||||
{
|
||||
"label": "Head",
|
||||
"protocol": "tailscale",
|
||||
"tag": "server-Head-in",
|
||||
"runtime_ok": 1,
|
||||
"routes_configured": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
EOF
|
||||
|
||||
render_out="$(ucode -L "$TACHYON_LIB" "$STATUS_UC" global-inbounds-check < "$WORK_DIR/test_tailscale.json")"
|
||||
if echo "$render_out" | grep -Fq "[WARN] WAN IP is not public"; then
|
||||
fail "Tailscale with private WAN IP should not produce [WARN] in render_global_inbounds_check: $render_out"
|
||||
fi
|
||||
if ! echo "$render_out" | grep -Fq "[OK] WAN IP: 192.168.1.60 (Tailscale does not require public WAN)"; then
|
||||
fail "Expected Tailscale exemption message in render_global_inbounds_check: $render_out"
|
||||
fi
|
||||
|
||||
# 4. Test render_global_inbounds_check for public inbound (VLESS) with private WAN
|
||||
cat >"$WORK_DIR/test_vless.json" <<'EOF'
|
||||
{
|
||||
"enabled_count": 1,
|
||||
"wan_ip": "192.168.1.60",
|
||||
"wan_public": 0,
|
||||
"requires_public_wan": 1,
|
||||
"items": [
|
||||
{
|
||||
"label": "Vless",
|
||||
"protocol": "vless",
|
||||
"tag": "server-vless-in",
|
||||
"runtime_ok": 1,
|
||||
"routes_configured": 1,
|
||||
"listening": 1,
|
||||
"firewall_required": 1,
|
||||
"firewall_open": 1,
|
||||
"port_conflict": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
EOF
|
||||
|
||||
render_vless_out="$(ucode -L "$TACHYON_LIB" "$STATUS_UC" global-inbounds-check < "$WORK_DIR/test_vless.json")"
|
||||
if ! echo "$render_vless_out" | grep -Fq "[WARN] WAN IP is not public"; then
|
||||
fail "VLESS with private WAN IP should produce [WARN] in render_global_inbounds_check: $render_vless_out"
|
||||
fi
|
||||
|
||||
printf 'inbounds check diagnostics tests passed\n'
|
||||
Reference in New Issue
Block a user