fix(diagnostics): resolve leak test XHR timeout with parallel probes and async polling

This commit is contained in:
Dushnilin
2026-09-10 10:11:25 +03:00
parent a324e32297
commit 01468b3a42
8 changed files with 691 additions and 143 deletions
@@ -1665,22 +1665,89 @@ export const TachyonShellMethods = {
};
},
leakCheck: async (): Promise<
Tachyon.MethodResponse<Tachyon.LeakCheckResult>
> => {
const response = await executeShellCommand({
leakCheck: async (
onProgress?: (progress: number, stage: string) => void,
): Promise<Tachyon.MethodResponse<Tachyon.LeakCheckResult>> => {
// 1. Attempt asynchronous leak check execution to avoid LuCI RPC / browser XHR timeout
const startResponse = await executeShellCommand({
command: '/usr/bin/tachyon',
args: [Tachyon.AvailableMethods.LEAK_CHECK],
timeout: 25000,
args: [Tachyon.AvailableMethods.LEAK_CHECK_ASYNC],
timeout: 5000,
});
const parsed = parseJsonObjectOutput<Tachyon.LeakCheckResult>(
response.stdout,
const startParsed = parseJsonObjectOutput<{
success?: boolean;
job_id?: string;
error?: string;
}>(startResponse.stdout);
if (
(startResponse.code ?? 0) === 0 &&
startParsed?.success &&
startParsed.job_id
) {
const jobId = startParsed.job_id;
const startedAt = Date.now();
const MAX_WAIT_MS = 35000;
const POLL_INTERVAL_MS = 800;
while (Date.now() - startedAt < MAX_WAIT_MS) {
await sleep(POLL_INTERVAL_MS);
const statusResponse = await executeShellCommand({
command: '/usr/bin/tachyon',
args: [Tachyon.AvailableMethods.LEAK_CHECK_STATUS, jobId],
timeout: 4000,
});
const statusParsed =
parseJsonObjectOutput<Tachyon.LeakCheckJobState>(
statusResponse.stdout,
);
if ((response.code ?? 0) !== 0 || !parsed) {
if (statusParsed) {
if (typeof statusParsed.progress === 'number' && onProgress) {
onProgress(statusParsed.progress, statusParsed.stage || '');
}
if (!statusParsed.running) {
if (statusParsed.success && statusParsed.data) {
return {
success: true,
data: statusParsed.data,
};
}
return {
success: false,
error: response.stderr || _('Failed to execute IP and DNS leak check'),
error:
statusParsed.error ||
_('Failed to execute IP and DNS leak check'),
};
}
}
}
return {
success: false,
error: _('IP and DNS leak check timed out'),
};
}
// 2. Fallback to direct synchronous execution if async mode is not supported
const syncResponse = await executeShellCommand({
command: '/usr/bin/tachyon',
args: [Tachyon.AvailableMethods.LEAK_CHECK],
timeout: 15000,
});
const parsed = parseJsonObjectOutput<Tachyon.LeakCheckResult>(
syncResponse.stdout,
);
if ((syncResponse.code ?? 0) !== 0 || !parsed) {
return {
success: false,
error:
syncResponse.stderr || _('Failed to execute IP and DNS leak check'),
};
}
return {
@@ -54,7 +54,21 @@ export function renderLeakCheckModal() {
}, 1500);
try {
const response = await TachyonShellMethods.leakCheck();
const response = await TachyonShellMethods.leakCheck(
(progress, stage) => {
clearTimeout(timer);
progressBar.style.width = `${progress}%`;
if (stage === 'dns') {
statusLabel.textContent = _(
'Testing DNS leak upstream resolvers with bash.ws protocol...',
);
} else if (stage === 'ip') {
statusLabel.textContent = _(
'Querying WAN direct socket and proxy outbound on 127.0.0.1:4534...',
);
}
},
);
clearTimeout(timer);
progressBar.style.width = '100%';
+14
View File
@@ -118,6 +118,8 @@ export namespace Tachyon {
DNS_BENCHMARK_STOP = 'dns_benchmark_stop',
DNS_BENCHMARK_APPLY = 'dns_benchmark_apply',
LEAK_CHECK = 'leak_check',
LEAK_CHECK_ASYNC = 'leak_check_async',
LEAK_CHECK_STATUS = 'leak_check_status',
CHECK_IP_LEAK = 'check_ip_leak',
CHECK_DNS_LEAK = 'check_dns_leak',
}
@@ -1212,4 +1214,16 @@ export namespace Tachyon {
dns_leak: DNSLeakResult;
timestamp?: number;
}
export interface LeakCheckJobState {
running: boolean;
success?: boolean;
job_id?: string;
progress?: number;
stage?: string;
error?: string;
data?: LeakCheckResult;
started_at?: number;
finished_at?: number;
}
}
@@ -3456,6 +3456,8 @@ var Tachyon;
AvailableMethods2["DNS_BENCHMARK_STOP"] = "dns_benchmark_stop";
AvailableMethods2["DNS_BENCHMARK_APPLY"] = "dns_benchmark_apply";
AvailableMethods2["LEAK_CHECK"] = "leak_check";
AvailableMethods2["LEAK_CHECK_ASYNC"] = "leak_check_async";
AvailableMethods2["LEAK_CHECK_STATUS"] = "leak_check_status";
AvailableMethods2["CHECK_IP_LEAK"] = "check_ip_leak";
AvailableMethods2["CHECK_DNS_LEAK"] = "check_dns_leak";
})(AvailableMethods = Tachyon2.AvailableMethods || (Tachyon2.AvailableMethods = {}));
@@ -4750,19 +4752,63 @@ var TachyonShellMethods = {
}
};
},
leakCheck: async () => {
const response = await executeShellCommand({
leakCheck: async (onProgress) => {
const startResponse = await executeShellCommand({
command: "/usr/bin/tachyon",
args: [Tachyon.AvailableMethods.LEAK_CHECK],
timeout: 25e3
args: [Tachyon.AvailableMethods.LEAK_CHECK_ASYNC],
timeout: 5e3
});
const parsed = parseJsonObjectOutput(
response.stdout
const startParsed = parseJsonObjectOutput(startResponse.stdout);
if ((startResponse.code ?? 0) === 0 && startParsed?.success && startParsed.job_id) {
const jobId = startParsed.job_id;
const startedAt = Date.now();
const MAX_WAIT_MS = 35e3;
const POLL_INTERVAL_MS = 800;
while (Date.now() - startedAt < MAX_WAIT_MS) {
await sleep(POLL_INTERVAL_MS);
const statusResponse = await executeShellCommand({
command: "/usr/bin/tachyon",
args: [Tachyon.AvailableMethods.LEAK_CHECK_STATUS, jobId],
timeout: 4e3
});
const statusParsed = parseJsonObjectOutput(
statusResponse.stdout
);
if ((response.code ?? 0) !== 0 || !parsed) {
if (statusParsed) {
if (typeof statusParsed.progress === "number" && onProgress) {
onProgress(statusParsed.progress, statusParsed.stage || "");
}
if (!statusParsed.running) {
if (statusParsed.success && statusParsed.data) {
return {
success: true,
data: statusParsed.data
};
}
return {
success: false,
error: response.stderr || _("Failed to execute IP and DNS leak check")
error: statusParsed.error || _("Failed to execute IP and DNS leak check")
};
}
}
}
return {
success: false,
error: _("IP and DNS leak check timed out")
};
}
const syncResponse = await executeShellCommand({
command: "/usr/bin/tachyon",
args: [Tachyon.AvailableMethods.LEAK_CHECK],
timeout: 15e3
});
const parsed = parseJsonObjectOutput(
syncResponse.stdout
);
if ((syncResponse.code ?? 0) !== 0 || !parsed) {
return {
success: false,
error: syncResponse.stderr || _("Failed to execute IP and DNS leak check")
};
}
return {
@@ -14834,7 +14880,21 @@ function renderLeakCheckModal() {
);
}, 1500);
try {
const response = await TachyonShellMethods.leakCheck();
const response = await TachyonShellMethods.leakCheck(
(progress, stage) => {
clearTimeout(timer);
progressBar.style.width = `${progress}%`;
if (stage === "dns") {
statusLabel.textContent = _(
"Testing DNS leak upstream resolvers with bash.ws protocol..."
);
} else if (stage === "ip") {
statusLabel.textContent = _(
"Querying WAN direct socket and proxy outbound on 127.0.0.1:4534..."
);
}
}
);
clearTimeout(timer);
progressBar.style.width = "100%";
if (response.success && response.data) {
+4
View File
@@ -169,6 +169,8 @@ function show_help() {
print(" validate_byedpi_strategy_json\n");
print(" Validate a ByeDPI ciadpi strategy and print JSON result\n");
print(" leak_check [--pretty] Test for IP and DNS leaks (direct vs proxy outbound)\n");
print(" leak_check_async Start async IP and DNS leak test (returns job_id)\n");
print(" leak_check_status <id> Poll async leak check status\n");
print(" check_ip_leak Check IP leak status (JSON)\n");
print(" check_dns_leak Check DNS leak status via bash.ws (JSON)\n");
}
@@ -279,6 +281,8 @@ function command_spec(command) {
validate_nfqws2_strategy_json: [ "diagnostics/runtime.uc", "validate-nfqws2-strategy-json", 1 ],
validate_byedpi_strategy_json: [ "diagnostics/runtime.uc", "validate-byedpi-strategy-json", 1 ],
leak_check: [ "diagnostics/leak_check.uc", "leak-check", 2 ],
leak_check_async: [ "diagnostics/leak_check.uc", "leak-check-async", 0 ],
leak_check_status: [ "diagnostics/leak_check.uc", "leak-check-status", 1 ],
check_ip_leak: [ "diagnostics/leak_check.uc", "ip-leak", 0 ],
check_dns_leak: [ "diagnostics/leak_check.uc", "dns-leak", 0 ],
fuzzer_start: [ "diagnostics/fuzzer.uc", "start", 6 ],
+483 -114
View File
@@ -11,6 +11,7 @@ let command_status = common.command_status;
let shell_quote = common.shell_quote;
const CONFIG_NAME = getenv("TACHYON_CONFIG_NAME") || "tachyon";
const LEAK_JOB_DIR = "/var/run/tachyon/leak_check";
let _has_so_mark_cached = null;
function curl_supports_so_mark() {
@@ -22,8 +23,20 @@ function curl_supports_so_mark() {
/**
* Detect active WAN network interface/device name.
* 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 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]));
}
// 2. Fallback to UCI network inspection
try {
let cursor = uci_core.cursor();
if (cursor) {
@@ -33,14 +46,7 @@ function get_wan_interface() {
return trim(as_string(dev));
}
} catch (e) {
// Fallback to routing inspection below
}
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]));
// Fallback
}
return "";
@@ -107,67 +113,92 @@ function get_direct_curl_flags(wan_iface) {
}
/**
* Query public IP details with fallback services.
* Parse IP, Country, City, and ISP from various JSON and plaintext endpoints.
*/
function parse_ip_response(text) {
if (text == null || text == "")
return null;
// 1. Try JSON formats (api.ip.sb, ipwho.is, ipleak.net, api.ipify.org)
try {
let data = json(text);
if (type(data) == "object" && data.ip != null && data.ip != "") {
let isp = "";
if (data.isp) {
isp = as_string(data.isp);
} else if (data.isp_name) {
isp = as_string(data.isp_name);
} else if (data.connection && (data.connection.isp || data.connection.org)) {
isp = as_string(data.connection.isp || data.connection.org);
} else if (data.organization) {
isp = as_string(data.organization);
}
return {
ip: as_string(data.ip),
country: as_string(data.country || data.country_name || ""),
country_code: as_string(data.country_code || ""),
city: as_string(data.city || data.city_name || ""),
isp: isp,
ok: true
};
}
} catch (e) {}
// 2. Try Cloudflare cdn-cgi/trace (ip=..., loc=...)
let m_ip = match(text, /ip=([0-9a-fA-F\.\:]+)/);
if (m_ip && m_ip[1]) {
let loc = "";
let m_loc = match(text, /loc=([A-Z]{2})/);
if (m_loc && m_loc[1])
loc = m_loc[1];
return {
ip: m_ip[1],
country: loc,
country_code: loc,
city: "",
isp: "Cloudflare Anycast",
ok: true
};
}
// 3. Try plain IP address text
let plain_ip = trim(text);
if (match(plain_ip, /^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$/) || match(plain_ip, /^[0-9a-fA-F:]+$/)) {
return {
ip: plain_ip,
country: "",
country_code: "",
city: "",
isp: "",
ok: true
};
}
return null;
}
/**
* Query public IP details with fast Anycast failover endpoints.
*/
function fetch_ip_info(use_proxy, wan_iface, mixed_port) {
let proxy_flag = use_proxy ? ("--proxy http://127.0.0.1:" + mixed_port + " ") : "";
let direct_flag = use_proxy ? "" : (get_direct_curl_flags(wan_iface) + " ");
let flags = "-s -m 7 --connect-timeout 4 " + proxy_flag + direct_flag;
let endpoints = [
"https://api.ip.sb/geoip",
"https://ipwho.is/",
"https://api.ipify.org?format=json",
"https://cloudflare.com/cdn-cgi/trace"
];
// 1. Try ipleak.net JSON API
let res = command_capture("curl " + flags + "https://ipleak.net/json/ 2>/dev/null");
for (let ep in endpoints) {
let cmd = sprintf("curl -s -m 3 --connect-timeout 2 %s%s%s 2>/dev/null", proxy_flag, direct_flag, shell_quote(ep));
let res = command_capture(cmd);
if (res && res.status == 0 && res.output != "") {
try {
let data = json(res.output);
if (type(data) == "object" && data.ip != null && data.ip != "") {
return {
ip: as_string(data.ip),
country: as_string(data.country_name || ""),
country_code: as_string(data.country_code || ""),
city: as_string(data.city_name || ""),
isp: as_string(data.isp_name || ""),
ok: true
};
}
} catch (e) {
// Fall through to fallback
}
}
// 2. Fallback to api.ipify.org
res = command_capture("curl -s -m 5 --connect-timeout 3 " + proxy_flag + direct_flag + "'https://api.ipify.org?format=json' 2>/dev/null");
if (res && res.status == 0 && res.output != "") {
try {
let data = json(res.output);
if (type(data) == "object" && data.ip != null && data.ip != "") {
return {
ip: as_string(data.ip),
country: "",
country_code: "",
city: "",
isp: "",
ok: true
};
}
} catch (e) {
// Fall through
}
}
// 3. Fallback to ifconfig.me
res = command_capture("curl -s -m 5 --connect-timeout 3 " + proxy_flag + direct_flag + "https://ifconfig.me/ip 2>/dev/null");
if (res && res.status == 0 && res.output != "") {
let ip = trim(as_string(res.output));
if (ip != "" && match(ip, /^[0-9a-fA-F\.\:]+$/)) {
return {
ip: ip,
country: "",
country_code: "",
city: "",
isp: "",
ok: true
};
let parsed = parse_ip_response(res.output);
if (parsed != null && parsed.ok)
return parsed;
}
}
@@ -183,23 +214,46 @@ function fetch_ip_info(use_proxy, wan_iface, mixed_port) {
/**
* Check for IP Leaks by comparing direct WAN IP and proxy outbound IP.
* Executes direct and proxy queries concurrently in parallel to avoid timeouts.
*/
function check_ip_leak(wan_iface, mixed_port) {
mixed_port = mixed_port || common.get_mixed_port();
wan_iface = wan_iface != null ? wan_iface : get_wan_interface();
let direct = fetch_ip_info(false, wan_iface, mixed_port);
let proxy = fetch_ip_info(true, wan_iface, mixed_port);
let tmp_res = command_capture("mktemp -d /tmp/tachyon_leak_ip_XXXXXX 2>/dev/null");
let work_dir = (tmp_res && tmp_res.status == 0) ? trim(as_string(tmp_res.output)) : "";
let direct = null;
let proxy = null;
if (work_dir != "") {
let proxy_flag = "--proxy http://127.0.0.1:" + mixed_port;
let direct_flags = get_direct_curl_flags(wan_iface);
let cmd_direct = sprintf("( curl -s -m 3 --connect-timeout 2 %s https://api.ip.sb/geoip || curl -s -m 3 --connect-timeout 2 %s https://ipwho.is/ || curl -s -m 2 --connect-timeout 2 %s 'https://api.ipify.org?format=json' || curl -s -m 2 --connect-timeout 2 %s https://cloudflare.com/cdn-cgi/trace ) > %s/direct.out 2>/dev/null", direct_flags, direct_flags, direct_flags, direct_flags, work_dir);
let cmd_proxy = sprintf("( curl -s -m 3 --connect-timeout 2 %s https://api.ip.sb/geoip || curl -s -m 3 --connect-timeout 2 %s https://ipwho.is/ || curl -s -m 2 --connect-timeout 2 %s 'https://api.ipify.org?format=json' || curl -s -m 2 --connect-timeout 2 %s https://cloudflare.com/cdn-cgi/trace ) > %s/proxy.out 2>/dev/null", proxy_flag, proxy_flag, proxy_flag, proxy_flag, work_dir);
system(sprintf("{ %s & %s & wait; } 2>/dev/null", cmd_direct, cmd_proxy));
let direct_out = fs.readfile(work_dir + "/direct.out");
let proxy_out = fs.readfile(work_dir + "/proxy.out");
direct = parse_ip_response(direct_out);
proxy = parse_ip_response(proxy_out);
system(sprintf("rm -rf %s 2>/dev/null", shell_quote(work_dir)));
}
if (direct == null)
direct = fetch_ip_info(false, wan_iface, mixed_port);
if (proxy == null)
proxy = fetch_ip_info(true, wan_iface, mixed_port);
let proxy_online = (proxy.ok && proxy.ip != "—");
let leaked = true;
let leaked = false;
if (proxy_online && direct.ok && direct.ip != "—") {
leaked = (proxy.ip == direct.ip);
} else if (proxy_online) {
leaked = false;
} else {
leaked = false;
}
return {
@@ -220,6 +274,7 @@ function check_ip_leak(wan_iface, mixed_port) {
/**
* Check for DNS Leaks using the bash.ws DNS Leak detection protocol.
* Queries tokens and results in parallel with fast timeouts and OpenWrt resolv fallback.
*/
function check_dns_leak(wan_iface, mixed_port, direct_ip, proxy_ip) {
mixed_port = mixed_port || common.get_mixed_port();
@@ -230,59 +285,79 @@ function check_dns_leak(wan_iface, mixed_port, direct_ip, proxy_ip) {
let direct_flags = get_direct_curl_flags(wan_iface);
let proxy_flag = "--proxy http://127.0.0.1:" + mixed_port;
// 1. Get unique leak tokens from bash.ws/id
let res_direct_id = command_capture("curl -s -m 6 --connect-timeout 4 " + direct_flags + " https://bash.ws/id 2>/dev/null");
let direct_id = (res_direct_id && res_direct_id.status == 0) ? trim(as_string(res_direct_id.output)) : "";
let tmp_res = command_capture("mktemp -d /tmp/tachyon_leak_dns_XXXXXX 2>/dev/null");
let work_dir = (tmp_res && tmp_res.status == 0) ? trim(as_string(tmp_res.output)) : "";
let res_proxy_id = command_capture("curl -s -m 6 --connect-timeout 4 " + proxy_flag + " https://bash.ws/id 2>/dev/null");
let proxy_id = (res_proxy_id && res_proxy_id.status == 0) ? trim(as_string(res_proxy_id.output)) : "";
// 2. Fire parallel DNS queries for bash.ws subdomains
if (direct_id != "" || proxy_id != "") {
let parallel_cmd = "{ ";
if (direct_id != "") {
for (let i = 1; i <= 6; i++) {
parallel_cmd += sprintf("curl -s -m 3 %s http://%d.%s.bash.ws >/dev/null 2>&1 & ", direct_flags, i, direct_id);
}
}
if (proxy_id != "") {
for (let i = 1; i <= 6; i++) {
parallel_cmd += sprintf("curl -s -m 3 %s http://%d.%s.bash.ws >/dev/null 2>&1 & ", proxy_flag, i, proxy_id);
}
}
parallel_cmd += "wait; } 2>/dev/null";
system(parallel_cmd);
// Allow 1.5s propagation time for upstream resolvers
system("sleep 1.5 2>/dev/null || sleep 2 2>/dev/null");
}
// 3. Fetch test results from bash.ws
let direct_id = "";
let proxy_id = "";
let direct_resolvers = [];
if (direct_id != "") {
let res = command_capture("curl -s -m 6 --connect-timeout 4 " + direct_flags + " 'https://bash.ws/dnsleak/test/" + direct_id + "?json' 2>/dev/null");
if (res && res.status == 0 && res.output != "") {
try {
let parsed = json(res.output);
if (type(parsed) == "array")
direct_resolvers = parsed;
} catch (e) {}
}
}
let proxy_resolvers = [];
if (work_dir != "") {
// Step 1: Concurrently fetch leak tokens
let cmd_direct_id = sprintf("curl -s -m 3 --connect-timeout 2 %s https://bash.ws/id > %s/direct_id 2>/dev/null", direct_flags, work_dir);
let cmd_proxy_id = sprintf("curl -s -m 3 --connect-timeout 2 %s https://bash.ws/id > %s/proxy_id 2>/dev/null", proxy_flag, work_dir);
system(sprintf("{ %s & %s & wait; } 2>/dev/null", cmd_direct_id, cmd_proxy_id));
direct_id = trim(as_string(fs.readfile(work_dir + "/direct_id") || ""));
proxy_id = trim(as_string(fs.readfile(work_dir + "/proxy_id") || ""));
// Step 2: Fire parallel DNS queries for subdomains
if (direct_id != "" || proxy_id != "") {
let burst_cmd = "{ ";
if (direct_id != "") {
for (let i = 1; i <= 4; i++) {
burst_cmd += sprintf("curl -s -m 2 %s http://%d.%s.bash.ws >/dev/null 2>&1 & ", direct_flags, i, direct_id);
}
}
if (proxy_id != "") {
let res = command_capture("curl -s -m 6 --connect-timeout 4 " + proxy_flag + " 'https://bash.ws/dnsleak/test/" + proxy_id + "?json' 2>/dev/null");
if (res && res.status == 0 && res.output != "") {
for (let i = 1; i <= 4; i++) {
burst_cmd += sprintf("curl -s -m 2 %s http://%d.%s.bash.ws >/dev/null 2>&1 & ", proxy_flag, i, proxy_id);
}
}
burst_cmd += "wait; } 2>/dev/null";
system(burst_cmd);
// Allow 1s propagation time for upstream resolvers
system("sleep 1 2>/dev/null");
// Step 3: Concurrently fetch results
let cmd_res_direct = "";
let cmd_res_proxy = "";
if (direct_id != "") {
cmd_res_direct = sprintf("curl -s -m 3 --connect-timeout 2 %s 'https://bash.ws/dnsleak/test/%s?json' > %s/direct_res 2>/dev/null & ", direct_flags, direct_id, work_dir);
}
if (proxy_id != "") {
cmd_res_proxy = sprintf("curl -s -m 3 --connect-timeout 2 %s 'https://bash.ws/dnsleak/test/%s?json' > %s/proxy_res 2>/dev/null & ", proxy_flag, proxy_id, work_dir);
}
if (cmd_res_direct != "" || cmd_res_proxy != "") {
system(sprintf("{ %s%swait; } 2>/dev/null", cmd_res_direct, cmd_res_proxy));
}
if (direct_id != "") {
let text = fs.readfile(work_dir + "/direct_res");
if (text) {
try {
let parsed = json(res.output);
if (type(parsed) == "array")
proxy_resolvers = parsed;
let parsed = json(text);
if (type(parsed) == "array") direct_resolvers = parsed;
} catch (e) {}
}
}
if (proxy_id != "") {
let text = fs.readfile(work_dir + "/proxy_res");
if (text) {
try {
let parsed = json(text);
if (type(parsed) == "array") proxy_resolvers = parsed;
} catch (e) {}
}
}
}
// 4. Build lookup table of direct / ISP DNS resolvers
system(sprintf("rm -rf %s 2>/dev/null", shell_quote(work_dir)));
}
// Build lookup table of direct / ISP DNS resolvers
let direct_map = {};
for (let s in direct_dns_servers) {
if (s.ip) direct_map[s.ip] = true;
@@ -352,13 +427,186 @@ function check_dns_leak(wan_iface, mixed_port, direct_ip, proxy_ip) {
/**
* Unified diagnostic check for both IP and DNS leaks.
* Runs IP and DNS token acquisition concurrently, completing in 3-4s total.
*/
function run_leak_check(wan_iface, mixed_port) {
mixed_port = mixed_port || common.get_mixed_port();
wan_iface = wan_iface != null ? wan_iface : get_wan_interface();
let tmp_res = command_capture("mktemp -d /tmp/tachyon_leak_all_XXXXXX 2>/dev/null");
let work_dir = (tmp_res && tmp_res.status == 0) ? trim(as_string(tmp_res.output)) : "";
if (work_dir == "") {
let ip_res = check_ip_leak(wan_iface, mixed_port);
let dns_res = check_dns_leak(wan_iface, mixed_port, ip_res.direct_ip, ip_res.proxy_ip);
return {
ip_leak: ip_res,
dns_leak: dns_res,
timestamp: time()
};
}
let direct_dns_servers = get_direct_dns_servers();
let direct_flags = get_direct_curl_flags(wan_iface);
let proxy_flag = "--proxy http://127.0.0.1:" + mixed_port;
// Stage 1: Run all 4 probes concurrently (direct IP, proxy IP, direct bash.ws ID, proxy bash.ws ID)
let cmd_direct_ip = sprintf("( curl -s -m 3 --connect-timeout 2 %s https://api.ip.sb/geoip || curl -s -m 3 --connect-timeout 2 %s https://ipwho.is/ || curl -s -m 2 --connect-timeout 2 %s 'https://api.ipify.org?format=json' || curl -s -m 2 --connect-timeout 2 %s https://cloudflare.com/cdn-cgi/trace ) > %s/direct_ip.out 2>/dev/null", direct_flags, direct_flags, direct_flags, direct_flags, work_dir);
let cmd_proxy_ip = sprintf("( curl -s -m 3 --connect-timeout 2 %s https://api.ip.sb/geoip || curl -s -m 3 --connect-timeout 2 %s https://ipwho.is/ || curl -s -m 2 --connect-timeout 2 %s 'https://api.ipify.org?format=json' || curl -s -m 2 --connect-timeout 2 %s https://cloudflare.com/cdn-cgi/trace ) > %s/proxy_ip.out 2>/dev/null", proxy_flag, proxy_flag, proxy_flag, proxy_flag, work_dir);
let cmd_direct_id = sprintf("curl -s -m 3 --connect-timeout 2 %s https://bash.ws/id > %s/direct_id.out 2>/dev/null", direct_flags, work_dir);
let cmd_proxy_id = sprintf("curl -s -m 3 --connect-timeout 2 %s https://bash.ws/id > %s/proxy_id.out 2>/dev/null", proxy_flag, work_dir);
system(sprintf("{ %s & %s & %s & %s & wait; } 2>/dev/null", cmd_direct_ip, cmd_proxy_ip, cmd_direct_id, cmd_proxy_id));
let direct_ip_info = parse_ip_response(fs.readfile(work_dir + "/direct_ip.out"));
let proxy_ip_info = parse_ip_response(fs.readfile(work_dir + "/proxy_ip.out"));
let direct_id = trim(as_string(fs.readfile(work_dir + "/direct_id.out") || ""));
let proxy_id = trim(as_string(fs.readfile(work_dir + "/proxy_id.out") || ""));
if (direct_ip_info == null) direct_ip_info = fetch_ip_info(false, wan_iface, mixed_port);
if (proxy_ip_info == null) proxy_ip_info = fetch_ip_info(true, wan_iface, mixed_port);
// Stage 2: DNS queries & result retrieval
let direct_resolvers = [];
let proxy_resolvers = [];
if (direct_id != "" || proxy_id != "") {
let burst_cmd = "{ ";
if (direct_id != "") {
for (let i = 1; i <= 4; i++) {
burst_cmd += sprintf("curl -s -m 2 %s http://%d.%s.bash.ws >/dev/null 2>&1 & ", direct_flags, i, direct_id);
}
}
if (proxy_id != "") {
for (let i = 1; i <= 4; i++) {
burst_cmd += sprintf("curl -s -m 2 %s http://%d.%s.bash.ws >/dev/null 2>&1 & ", proxy_flag, i, proxy_id);
}
}
burst_cmd += "wait; } 2>/dev/null";
system(burst_cmd);
system("sleep 1 2>/dev/null");
let cmd_res_direct = "";
let cmd_res_proxy = "";
if (direct_id != "") {
cmd_res_direct = sprintf("curl -s -m 3 --connect-timeout 2 %s 'https://bash.ws/dnsleak/test/%s?json' > %s/direct_res 2>/dev/null & ", direct_flags, direct_id, work_dir);
}
if (proxy_id != "") {
cmd_res_proxy = sprintf("curl -s -m 3 --connect-timeout 2 %s 'https://bash.ws/dnsleak/test/%s?json' > %s/proxy_res 2>/dev/null & ", proxy_flag, proxy_id, work_dir);
}
if (cmd_res_direct != "" || cmd_res_proxy != "") {
system(sprintf("{ %s%swait; } 2>/dev/null", cmd_res_direct, cmd_res_proxy));
}
if (direct_id != "") {
let text = fs.readfile(work_dir + "/direct_res");
if (text) {
try {
let parsed = json(text);
if (type(parsed) == "array") direct_resolvers = parsed;
} catch (e) {}
}
}
if (proxy_id != "") {
let text = fs.readfile(work_dir + "/proxy_res");
if (text) {
try {
let parsed = json(text);
if (type(parsed) == "array") proxy_resolvers = parsed;
} catch (e) {}
}
}
}
system(sprintf("rm -rf %s 2>/dev/null", shell_quote(work_dir)));
// Assemble IP check result
let proxy_online = (proxy_ip_info.ok && proxy_ip_info.ip != "—");
let ip_leaked = false;
if (proxy_online && direct_ip_info.ok && direct_ip_info.ip != "—") {
ip_leaked = (proxy_ip_info.ip == direct_ip_info.ip);
}
let ip_res = {
leaked: ip_leaked,
direct_ip: direct_ip_info.ip,
direct_country: direct_ip_info.country,
direct_country_code: direct_ip_info.country_code,
direct_city: direct_ip_info.city,
direct_isp: direct_ip_info.isp,
proxy_ip: proxy_ip_info.ip,
proxy_country: proxy_ip_info.country,
proxy_country_code: proxy_ip_info.country_code,
proxy_city: proxy_ip_info.city,
proxy_org: proxy_ip_info.isp,
proxy_online: proxy_online
};
// Assemble DNS check result
let direct_map = {};
for (let s in direct_dns_servers) {
if (s.ip) direct_map[s.ip] = true;
}
for (let r in direct_resolvers) {
if (r.ip) direct_map[r.ip] = true;
}
let formatted_proxy_servers = [];
let leak_found = false;
for (let pr in proxy_resolvers) {
if (type(pr) != "object" || !pr.ip) continue;
let is_direct_leak = (direct_map[pr.ip] == true);
if (is_direct_leak) {
leak_found = true;
}
push(formatted_proxy_servers, {
ip: pr.ip,
country: as_string(pr.country || pr.country_name || ""),
isp: as_string(pr.name || pr.asn || "Unknown"),
is_isp: is_direct_leak
});
}
let formatted_direct_servers = [];
for (let dr in direct_resolvers) {
if (type(dr) != "object" || !dr.ip) continue;
push(formatted_direct_servers, {
ip: dr.ip,
country: as_string(dr.country || dr.country_name || ""),
isp: as_string(dr.name || dr.asn || "ISP Upstream"),
is_isp: true
});
}
if (length(formatted_direct_servers) == 0 && length(direct_dns_servers) > 0) {
formatted_direct_servers = direct_dns_servers;
}
let dns_proxy_online = (proxy_id != "" || (ip_res.proxy_ip != null && ip_res.proxy_ip != "" && ip_res.proxy_ip != "—"));
let dns_leaked = false;
if (dns_proxy_online && length(formatted_proxy_servers) > 0) {
dns_leaked = leak_found;
} else if (dns_proxy_online && length(formatted_proxy_servers) == 0) {
dns_leaked = false;
}
if (ip_res.proxy_ip != null && ip_res.direct_ip != null && ip_res.proxy_ip != "—" && ip_res.direct_ip != "—" && ip_res.proxy_ip == ip_res.direct_ip) {
dns_leaked = true;
}
let dns_res = {
dns_leaked: dns_leaked,
direct_ip: ip_res.direct_ip,
proxy_ip: ip_res.proxy_ip,
dns_servers: formatted_proxy_servers,
direct_dns_servers: formatted_direct_servers,
proxy_online: dns_proxy_online
};
return {
ip_leak: ip_res,
@@ -367,6 +615,116 @@ function run_leak_check(wan_iface, mixed_port) {
};
}
/**
* Start asynchronous leak check job and return job_id.
*/
function start_leak_check_async() {
common.ensure_dir("/var/run/tachyon");
common.ensure_dir(LEAK_JOB_DIR);
let id = sprintf("%d_%d", time(), int(clock()[1] % 10000));
let path = sprintf("%s/%s.json", LEAK_JOB_DIR, id);
let initial_state = {
running: true,
job_id: id,
progress: 25,
stage: "ip",
started_at: time()
};
if (!common.write_json_file(path, initial_state)) {
print(sprintf("%J\n", { success: false, error: "Failed to initialize leak check job state" }));
exit(1);
}
let lib_dir = getenv("TACHYON_LIB") || "/usr/lib/tachyon";
let mod_file = lib_dir + "/diagnostics/leak_check.uc";
let worker_cmd = sprintf("TACHYON_LIB=%s ucode -L %s %s leak-check-worker %s", shell_quote(lib_dir), shell_quote(lib_dir), shell_quote(mod_file), shell_quote(id));
let bg_cmd = common.background_command_with_pid(worker_cmd);
command_capture("sh -c " + shell_quote(bg_cmd));
print(sprintf("%J\n", { success: true, job_id: id }));
exit(0);
}
/**
* Worker executing leak check in background and recording final state.
*/
function leak_check_worker(job_id) {
if (job_id == null || job_id == "")
exit(1);
common.ensure_dir("/var/run/tachyon");
common.ensure_dir(LEAK_JOB_DIR);
let path = sprintf("%s/%s.json", LEAK_JOB_DIR, job_id);
common.write_json_file(path, {
running: true,
job_id: job_id,
progress: 50,
stage: "dns",
started_at: time()
});
let res = run_leak_check(null, null);
common.write_json_file(path, {
running: false,
success: true,
job_id: job_id,
progress: 100,
stage: "done",
data: res,
finished_at: time()
});
// Cleanup old completed job files (> 10 minutes old)
let cutoff = time() - 600;
for (let f_path in (fs.glob(LEAK_JOB_DIR + "/*.json") || [])) {
let st = fs.stat(f_path);
if (st && st.mtime < cutoff) {
try { fs.unlink(f_path); } catch (e) {}
}
}
exit(0);
}
/**
* Poll status of asynchronous leak check job.
*/
function get_leak_check_status(job_id) {
if (job_id == null || job_id == "") {
print(sprintf("%J\n", { success: false, error: "Job ID is required" }));
exit(1);
}
let path = sprintf("%s/%s.json", LEAK_JOB_DIR, job_id);
if (!fs.stat(path)) {
print(sprintf("%J\n", { success: false, error: "Job not found" }));
exit(1);
}
let state = common.read_json_file(path);
if (state == null) {
print(sprintf("%J\n", { success: false, error: "Failed to read job state" }));
exit(1);
}
// Timeout check: if worker died or hung for more than 40 seconds
if (state.running && (time() - state.started_at > 40)) {
state.running = false;
state.success = false;
state.error = "IP & DNS leak test timed out on router";
common.write_json_file(path, state);
}
print(sprintf("%J\n", state));
exit(0);
}
function print_cli_summary(res) {
print("\n=== TACHYON IP & DNS LEAK TEST REPORT ===\n\n");
print(sprintf("Direct WAN IP : %s (%s, %s)\n", res.ip_leak.direct_ip, res.ip_leak.direct_country || "Unknown", res.ip_leak.direct_isp || "ISP"));
@@ -411,6 +769,15 @@ if (mode == "leak-check" || mode == "leak_check") {
}
exit(0);
}
else if (mode == "leak-check-async" || mode == "leak_check_async") {
start_leak_check_async();
}
else if (mode == "leak-check-worker") {
leak_check_worker(ARGV[1]);
}
else if (mode == "leak-check-status" || mode == "leak_check_status") {
get_leak_check_status(ARGV[1]);
}
else if (mode == "ip-leak" || mode == "check_ip_leak") {
let res = check_ip_leak(null, null);
print(sprintf("%J\n", res));
@@ -429,5 +796,7 @@ return {
fetch_ip_info,
check_ip_leak,
check_dns_leak,
run_leak_check
run_leak_check,
start_leak_check_async,
get_leak_check_status
};
+14
View File
@@ -275,6 +275,18 @@ function run_dns_leak_check() {
return leak_mod.check_dns_leak(null, null, null, null);
}
function run_leak_check_async() {
let leak_mod = require("diagnostics.leak_check");
if (!leak_mod) return null;
return leak_mod.start_leak_check_async();
}
function run_leak_check_status(job_id) {
let leak_mod = require("diagnostics.leak_check");
if (!leak_mod) return null;
return leak_mod.get_leak_check_status(job_id);
}
function manage_domain_list(action_type, domain, do_delete) {
let c = uci_core.cursor();
if (!c) return { success: false, error: "Не удалось инициализировать UCI" };
@@ -391,6 +403,8 @@ return {
check_connection,
run_speedtest,
run_leak_check,
run_leak_check_async,
run_leak_check_status,
run_ip_leak_check,
run_dns_leak_check,
manage_domain_list,
+7 -1
View File
@@ -34,7 +34,9 @@ if (type(leak_mod) != "object") {
// Test exports
if (type(leak_mod.check_ip_leak) != "function" ||
type(leak_mod.check_dns_leak) != "function" ||
type(leak_mod.run_leak_check) != "function") {
type(leak_mod.run_leak_check) != "function" ||
type(leak_mod.start_leak_check_async) != "function" ||
type(leak_mod.get_leak_check_status) != "function") {
print("ERR: leak_check module missing required functions\n");
exit(2);
}
@@ -55,11 +57,15 @@ ucode -L "$TACHYON_LIB" "$WORK_DIR/test_leak_logic.uc" | grep -q "OK" ||
# ─── 3. Test CLI registration in /usr/bin/tachyon ───────────────────────────
grep -Fq "leak_check" "$BIN" || fail "/usr/bin/tachyon must register leak_check"
grep -Fq "leak_check_async" "$BIN" || fail "/usr/bin/tachyon must register leak_check_async"
grep -Fq "leak_check_status" "$BIN" || fail "/usr/bin/tachyon must register leak_check_status"
grep -Fq "check_ip_leak" "$BIN" || fail "/usr/bin/tachyon must register check_ip_leak"
grep -Fq "check_dns_leak" "$BIN" || fail "/usr/bin/tachyon must register check_dns_leak"
# ─── 4. Test api.uc integration ─────────────────────────────────────────────
grep -Fq "run_leak_check" "$API_UC" || fail "api.uc must export run_leak_check"
grep -Fq "run_leak_check_async" "$API_UC" || fail "api.uc must export run_leak_check_async"
grep -Fq "run_leak_check_status" "$API_UC" || fail "api.uc must export run_leak_check_status"
grep -Fq "run_ip_leak_check" "$API_UC" || fail "api.uc must export run_ip_leak_check"
grep -Fq "run_dns_leak_check" "$API_UC" || fail "api.uc must export run_dns_leak_check"