fix(fuzzer): ensure direct outbound routing and robust zapret2 strategy testing

This commit is contained in:
Dushnilin
2026-08-30 10:21:52 +03:00
parent 123f5e6ccf
commit b3e0a73b2d
3 changed files with 162 additions and 26 deletions
@@ -1281,6 +1281,11 @@ export function renderStrategyFuzzerModal(ruleNames: string[] = []) {
subProbesHtml = `<div style="font-size: 10px; opacity: 0.75; margin-top: 2px;">${passedSub}/${totalSub} endpoints OK</div>`;
}
let errorHtml = '';
if (!item.success && item.error) {
errorHtml = `<div style="font-size: 10px; color: #dc3545; opacity: 0.85; margin-top: 2px; max-width: 140px; word-break: break-word;" title="${item.error}">${item.error}</div>`;
}
let badgeHtml = '';
if (item.badge) {
badgeHtml = `<span style="font-size: 10px; margin-left: 6px; font-weight: bold; color: ${isBest ? '#28a745' : '#17a2b8'};">${item.badge}</span>`;
@@ -1320,7 +1325,7 @@ export function renderStrategyFuzzerModal(ruleNames: string[] = []) {
E(
'td',
{ style: 'padding: 8px 10px;' },
E('div', { innerHTML: statusBadge + subProbesHtml }),
E('div', { innerHTML: statusBadge + subProbesHtml + errorHtml }),
),
E(
'td',
@@ -13037,6 +13037,10 @@ function renderStrategyFuzzerModal(ruleNames = []) {
const totalSub = item.sub_probes.length;
subProbesHtml = `<div style="font-size: 10px; opacity: 0.75; margin-top: 2px;">${passedSub}/${totalSub} endpoints OK</div>`;
}
let errorHtml = "";
if (!item.success && item.error) {
errorHtml = `<div style="font-size: 10px; color: #dc3545; opacity: 0.85; margin-top: 2px; max-width: 140px; word-break: break-word;" title="${item.error}">${item.error}</div>`;
}
let badgeHtml = "";
if (item.badge) {
badgeHtml = `<span style="font-size: 10px; margin-left: 6px; font-weight: bold; color: ${isBest ? "#28a745" : "#17a2b8"};">${item.badge}</span>`;
@@ -13071,7 +13075,7 @@ function renderStrategyFuzzerModal(ruleNames = []) {
E(
"td",
{ style: "padding: 8px 10px;" },
E("div", { innerHTML: statusBadge + subProbesHtml })
E("div", { innerHTML: statusBadge + subProbesHtml + errorHtml })
),
E(
"td",
+151 -24
View File
@@ -61,6 +61,70 @@ function get_byedpi_bin() {
]);
}
function get_zapret2_lua_flags(args_str) {
if (index(args_str, "--lua-init") >= 0)
return "";
let candidate_dirs = [
getenv("ZAPRET2_PROVIDER_LUA_DIR"),
"/opt/zapret2/lua",
"/usr/share/zapret2/lua",
"/etc/zapret2/lua",
"/opt/zapret/lua",
"/usr/share/zapret/lua"
];
let flags = "";
for (let d in candidate_dirs) {
if (!d || fs.stat(d) == null)
continue;
let lib_lua = d + "/zapret-lib.lua";
let antidpi_lua = d + "/zapret-antidpi.lua";
let auto_lua = d + "/zapret-auto.lua";
if (fs.stat(lib_lua) != null) flags += sprintf("--lua-init=@%s ", lib_lua);
if (fs.stat(antidpi_lua) != null) flags += sprintf("--lua-init=@%s ", antidpi_lua);
if (fs.stat(auto_lua) != null) flags += sprintf("--lua-init=@%s ", auto_lua);
if (flags != "")
break;
}
return flags;
}
let _fuzzer_curl_dns_flags = null;
function get_fuzzer_curl_dns_flags() {
if (_fuzzer_curl_dns_flags !== null)
return _fuzzer_curl_dns_flags;
if (system("curl --doh-url https://1.1.1.1/dns-query -V >/dev/null 2>&1") == 0) {
_fuzzer_curl_dns_flags = "--doh-url https://1.1.1.1/dns-query ";
return _fuzzer_curl_dns_flags;
}
if (system("curl --dns-servers 8.8.8.8 -V >/dev/null 2>&1") == 0) {
_fuzzer_curl_dns_flags = "--dns-servers 8.8.8.8,1.1.1.1 ";
return _fuzzer_curl_dns_flags;
}
_fuzzer_curl_dns_flags = "";
return _fuzzer_curl_dns_flags;
}
function setup_fuzzer_direct_nftables(qnum, is_udp) {
system("nft add table inet tachyon_fuzzer 2>/dev/null");
system("nft 'add chain inet tachyon_fuzzer output { type filter hook output priority -200 ; policy accept; }' 2>/dev/null");
system(sprintf("nft add rule inet tachyon_fuzzer output meta mark %s counter return 2>/dev/null", FUZZER_FWMARK));
if (is_udp) {
system(sprintf("nft 'add rule inet tachyon_fuzzer output meta l4proto { tcp, udp } th dport { 80, 443, 50000-65535 } counter queue num %d bypass' 2>/dev/null", qnum));
} else {
system(sprintf("nft 'add rule inet tachyon_fuzzer output meta l4proto tcp tcp dport { 80, 443 } counter queue num %d bypass' 2>/dev/null", qnum));
}
// Route hook with priority -155 (before TachyonTable's -150) marks test traffic with 0x00200000 (direct outbound mark)
// This guarantees that TachyonTable's mangle_output immediately returns and test traffic goes DIRECT to WAN without Sing-box TProxy
system("nft 'add chain inet tachyon_fuzzer bypass_singbox { type route hook output priority -155 ; policy accept; }' 2>/dev/null");
system(sprintf("nft add rule inet tachyon_fuzzer bypass_singbox meta mark %s counter return 2>/dev/null", FUZZER_FWMARK));
system("nft 'add rule inet tachyon_fuzzer bypass_singbox meta l4proto tcp tcp dport { 80, 443 } meta mark set meta mark | 0x00200000 counter' 2>/dev/null");
if (is_udp) {
system("nft 'add rule inet tachyon_fuzzer bypass_singbox meta l4proto udp udp dport { 80, 443, 50000-65535 } meta mark set meta mark | 0x00200000 counter' 2>/dev/null");
}
}
function validate_strategy_args(engine, args_val) {
args_val = trim(as_string(args_val));
if (args_val == "") return false;
@@ -145,11 +209,11 @@ function reset_patterns_config() {
// Target Suites & Definitions
const TARGET_SUITES = {
youtube_suite: {
name: "YouTube Full Suite (Web + Static CDN + 4K Stream)",
name: "YouTube Full Suite (Web + Static CDN + Stream)",
urls: [
{ name: "Web Interface", url: "https://www.youtube.com", weight: 30 },
{ name: "Static Assets (i.ytimg)", url: "https://i.ytimg.com/generate_204", weight: 20 },
{ name: "GoogleVideo Stream Chunk", url: "https://rr1---sn-4g5ednss.googlevideo.com/generate_204", weight: 50 }
{ name: "Web Interface", url: "https://www.youtube.com", weight: 40 },
{ name: "Static Assets (i.ytimg)", url: "https://i.ytimg.com/generate_204", weight: 30 },
{ name: "GoogleVideo Stream CDN", url: "https://redirector.googlevideo.com/generate_204", weight: 30 }
]
},
discord_suite: {
@@ -164,7 +228,7 @@ const TARGET_SUITES = {
name: "Instagram / Meta Suite (Web + Static CDN)",
urls: [
{ name: "Web Interface", url: "https://www.instagram.com", weight: 50 },
{ name: "CDN Static Assets", url: "https://static.cdninstagram.com/rsrc.php/v3/y4/r/dukNmT7A9dd.webp", weight: 50 }
{ name: "CDN Static Assets", url: "https://static.cdninstagram.com/", weight: 50 }
]
},
telegram_suite: {
@@ -1028,6 +1092,7 @@ function cleanup_temp_daemons() {
kill_pid_file(STATE_DIR + "/fuzzer_zapret.pid");
kill_pid_file(STATE_DIR + "/fuzzer_zapret2.pid");
system("nft delete table inet tachyon_fuzzer 2>/dev/null");
try { fs.unlink(STATE_DIR + "/fuzzer_daemon_err.log"); } catch (e) {}
}
function parse_curl_output(output, result) {
@@ -1086,6 +1151,7 @@ function parse_curl_output(output, result) {
function detect_dpi_type(target_key, custom_url) {
let urls_list = resolve_target_urls_list(target_key, custom_url);
let target_url = urls_list[0] ? urls_list[0].url : "https://www.google.com";
let dns_flags = get_fuzzer_curl_dns_flags();
let result = {
type: "unknown",
@@ -1095,9 +1161,14 @@ function detect_dpi_type(target_key, custom_url) {
probe_metrics: { http_code: 0, handshake_ms: 0, ttfb_ms: 0, speed_kbps: 0, error: "" }
};
// Direct probe without any bypass
// Direct probe with bypass of Sing-box TProxy
system("nft add table inet tachyon_fuzzer 2>/dev/null");
system("nft 'add chain inet tachyon_fuzzer bypass_singbox { type route hook output priority -155 ; policy accept; }' 2>/dev/null");
system("nft 'add rule inet tachyon_fuzzer bypass_singbox meta l4proto tcp tcp dport { 80, 443 } meta mark set meta mark | 0x00200000 counter' 2>/dev/null");
let curl_cmd = sprintf(
"curl -so /dev/null -w '%%{http_code}\\t%%{time_appconnect}\\t%%{time_starttransfer}\\t%%{speed_download}' -L --connect-timeout 3 --max-time 5 %s 2>&1",
"curl %s-so /dev/null -w '%%{http_code}\\t%%{time_appconnect}\\t%%{time_starttransfer}\\t%%{speed_download}' -L --connect-timeout 4 --max-time 6 %s 2>&1",
dns_flags,
shell_quote(target_url)
);
let pipe = fs.popen(curl_cmd, "r");
@@ -1105,6 +1176,8 @@ function detect_dpi_type(target_key, custom_url) {
if (pipe) pipe.close();
output = trim(output);
cleanup_temp_daemons();
let metrics = {};
parse_curl_output(output, metrics);
result.probe_metrics = {
@@ -1304,9 +1377,37 @@ function run_probe(engine, args_str, target_key, custom_url) {
}
let pid_path = STATE_DIR + "/fuzzer_byedpi.pid";
let spawn_cmd = sprintf("cd /tmp && %s -i 127.0.0.1 -p %d %s", bin, BYEDPI_PORT, args_str);
let stderr_log = STATE_DIR + "/fuzzer_daemon_err.log";
let spawn_cmd = sprintf("cd /tmp && %s -i 127.0.0.1 -p %d %s 2>%s", bin, BYEDPI_PORT, args_str, shell_quote(stderr_log));
system(common.background_command_with_pid(spawn_cmd, ">/dev/null", ">" + shell_quote(pid_path)));
system("sleep 0.15");
system("sleep 0.25");
let pid_str = fs.readfile(pid_path);
let pid_running = false;
if (pid_str) {
let pid = trim(as_string(pid_str));
if (pid != "" && match(pid, /^[0-9]+$/) != null && system(sprintf("kill -0 %s >/dev/null 2>&1", pid)) == 0) {
pid_running = true;
}
}
if (!pid_running) {
let err_content = fs.readfile(stderr_log);
let err_msg = err_content ? trim(as_string(err_content)) : "";
if (err_msg != "") {
let first_line = split(err_msg, "\n")[0];
result.error = sprintf("Daemon failed to start: %s", first_line);
} else {
result.error = "ByeDPI daemon failed to start (invalid arguments)";
}
cleanup_temp_daemons();
return result;
}
// Ensure ciadpi direct outbound connections bypass Sing-Box TProxy
system("nft add table inet tachyon_fuzzer 2>/dev/null");
system("nft 'add chain inet tachyon_fuzzer bypass_singbox { type route hook output priority -155 ; policy accept; }' 2>/dev/null");
system("nft 'add rule inet tachyon_fuzzer bypass_singbox meta l4proto tcp tcp dport { 80, 443 } meta mark set meta mark | 0x00200000 counter' 2>/dev/null");
let passed_count = 0;
let sum_handshake = 0;
@@ -1316,7 +1417,7 @@ function run_probe(engine, args_str, target_key, custom_url) {
for (let target_item in urls_list) {
let curl_cmd = sprintf(
"curl -x socks5h://127.0.0.1:%d -so /dev/null -w '%%{http_code}\\t%%{time_appconnect}\\t%%{time_starttransfer}\\t%%{speed_download}' -L --connect-timeout 2 --max-time 3 %s 2>/dev/null",
"curl -x socks5h://127.0.0.1:%d -so /dev/null -w '%%{http_code}\\t%%{time_appconnect}\\t%%{time_starttransfer}\\t%%{speed_download}' -L --connect-timeout 4 --max-time 6 %s 2>/dev/null",
BYEDPI_PORT,
shell_quote(target_item.url)
);
@@ -1337,6 +1438,7 @@ function run_probe(engine, args_str, target_key, custom_url) {
last_http = single_res.http_code;
} else {
if (last_http == 0) last_http = single_res.http_code;
if (single_res.error && result.error == "") result.error = single_res.error;
break;
}
}
@@ -1350,11 +1452,14 @@ function run_probe(engine, args_str, target_key, custom_url) {
result.ttfb_ms = int(sum_ttfb / double(total_urls));
result.speed_kbps = max_speed;
result.score = 100 + max(0, 1000 - result.ttfb_ms) + int(result.speed_kbps / 10.0);
result.error = "";
} else {
result.success = false;
result.http_code = last_http;
result.score = 0;
result.error = sprintf("Failed %d of %d endpoints", total_urls - passed_count, total_urls);
if (result.error == "") {
result.error = sprintf("Failed %d of %d endpoints", total_urls - passed_count, total_urls);
}
}
return result;
@@ -1365,6 +1470,7 @@ function run_probe(engine, args_str, target_key, custom_url) {
let bin = is_z2 ? get_zapret2_bin() : get_zapret_bin();
let qnum = is_z2 ? NFQUEUE_QNUM_ZAPRET2 : NFQUEUE_QNUM_ZAPRET;
let pid_path = is_z2 ? (STATE_DIR + "/fuzzer_zapret2.pid") : (STATE_DIR + "/fuzzer_zapret.pid");
let stderr_log = STATE_DIR + "/fuzzer_daemon_err.log";
if (!bin) {
result.error = (is_z2 ? "Zapret v2" : "Zapret v1") + " binary not found";
@@ -1372,8 +1478,8 @@ function run_probe(engine, args_str, target_key, custom_url) {
}
let lua_init_flags = "";
if (is_z2 && fs.stat("/opt/zapret2/lua/zapret-lib.lua") != null && index(args_str, "--lua-init") < 0) {
lua_init_flags = "--lua-init=@/opt/zapret2/lua/zapret-lib.lua --lua-init=@/opt/zapret2/lua/zapret-antidpi.lua --lua-init=@/opt/zapret2/lua/zapret-auto.lua ";
if (is_z2) {
lua_init_flags = get_zapret2_lua_flags(args_str);
}
let filter_prefix = "";
@@ -1384,28 +1490,45 @@ function run_probe(engine, args_str, target_key, custom_url) {
filter_prefix += "--filter-udp=443 --payload=quic_initial ";
}
let spawn_cmd = sprintf("cd /tmp && %s --qnum=%d --fwmark=%s %s%s%s --pidfile=%s --daemon", bin, qnum, FUZZER_FWMARK, lua_init_flags, filter_prefix, args_str, pid_path);
let spawn_cmd = sprintf("cd /tmp && %s --qnum=%d --fwmark=%s %s%s%s --pidfile=%s --daemon 2>%s", bin, qnum, FUZZER_FWMARK, lua_init_flags, filter_prefix, args_str, pid_path, shell_quote(stderr_log));
system(common.background_command(spawn_cmd));
system("sleep 0.15");
system("sleep 0.25");
system("nft add table inet tachyon_fuzzer 2>/dev/null");
system("nft 'add chain inet tachyon_fuzzer output { type filter hook output priority -200 ; }' 2>/dev/null");
system(sprintf("nft add rule inet tachyon_fuzzer output meta mark %s counter return 2>/dev/null", FUZZER_FWMARK));
if (is_udp) {
system(sprintf("nft 'add rule inet tachyon_fuzzer output meta l4proto { tcp, udp } th dport { 80, 443, 50000-65535 } counter queue num %d bypass' 2>/dev/null", qnum));
} else {
system(sprintf("nft 'add rule inet tachyon_fuzzer output meta l4proto tcp tcp dport { 80, 443 } counter queue num %d bypass' 2>/dev/null", qnum));
let pid_str = fs.readfile(pid_path);
let pid_running = false;
if (pid_str) {
let pid = trim(as_string(pid_str));
if (pid != "" && match(pid, /^[0-9]+$/) != null && system(sprintf("kill -0 %s >/dev/null 2>&1", pid)) == 0) {
pid_running = true;
}
}
if (!pid_running) {
let err_content = fs.readfile(stderr_log);
let err_msg = err_content ? trim(as_string(err_content)) : "";
if (err_msg != "") {
let first_line = split(err_msg, "\n")[0];
result.error = sprintf("Daemon failed to start: %s", first_line);
} else {
result.error = sprintf("Daemon %s failed to start (invalid arguments or missing Lua library)", is_z2 ? "nfqws2" : "nfqws");
}
cleanup_temp_daemons();
return result;
}
setup_fuzzer_direct_nftables(qnum, is_udp);
let passed_count = 0;
let sum_handshake = 0;
let sum_ttfb = 0;
let max_speed = 0;
let last_http = 0;
let dns_flags = get_fuzzer_curl_dns_flags();
for (let target_item in urls_list) {
let curl_cmd = sprintf(
"curl -so /dev/null -w '%%{http_code}\\t%%{time_appconnect}\\t%%{time_starttransfer}\\t%%{speed_download}' -L --connect-timeout 2 --max-time 3 %s 2>/dev/null",
"curl %s-so /dev/null -w '%%{http_code}\\t%%{time_appconnect}\\t%%{time_starttransfer}\\t%%{speed_download}' -L --connect-timeout 4 --max-time 6 %s 2>/dev/null",
dns_flags,
shell_quote(target_item.url)
);
let pipe = fs.popen(curl_cmd, "r");
@@ -1425,6 +1548,7 @@ function run_probe(engine, args_str, target_key, custom_url) {
last_http = single_res.http_code;
} else {
if (last_http == 0) last_http = single_res.http_code;
if (single_res.error && result.error == "") result.error = single_res.error;
break;
}
}
@@ -1438,11 +1562,14 @@ function run_probe(engine, args_str, target_key, custom_url) {
result.ttfb_ms = int(sum_ttfb / double(total_urls));
result.speed_kbps = max_speed;
result.score = 100 + max(0, 1000 - result.ttfb_ms) + int(result.speed_kbps / 10.0);
result.error = "";
} else {
result.success = false;
result.http_code = last_http;
result.score = 0;
result.error = sprintf("Failed %d of %d endpoints", total_urls - passed_count, total_urls);
if (result.error == "") {
result.error = sprintf("Failed %d of %d endpoints", total_urls - passed_count, total_urls);
}
}
return result;