From 8b8c4b7df17acadf3707763bec09168d4e2eb285 Mon Sep 17 00:00:00 2001 From: Dushnilin Date: Wed, 29 Jul 2026 11:30:23 +0300 Subject: [PATCH] feat(zapret2): add high-performance strategy generator and tuner --- fe-app-tachyon/src/tachyon/types.ts | 3 + tachyon/files/usr/bin/tachyon | 3 + .../files/usr/lib/providers/zapret2/tune.uc | 416 ++++++++++++++++++ tachyon/files/usr/lib/service/ui.uc | 80 ++++ tests/zapret2_tuner.sh | 60 +++ 5 files changed, 562 insertions(+) create mode 100644 tachyon/files/usr/lib/providers/zapret2/tune.uc create mode 100644 tests/zapret2_tuner.sh diff --git a/fe-app-tachyon/src/tachyon/types.ts b/fe-app-tachyon/src/tachyon/types.ts index 498ef48b..368c7b73 100644 --- a/fe-app-tachyon/src/tachyon/types.ts +++ b/fe-app-tachyon/src/tachyon/types.ts @@ -68,6 +68,9 @@ export namespace Tachyon { GET_ZAPRET_STATUS = 'get_zapret_status', GET_ZAPRET2_STATUS = 'get_zapret2_status', GET_BYEDPI_STATUS = 'get_byedpi_status', + ZAPRET2_TUNE = 'zapret2_tune', + ZAPRET2_TUNE_ASYNC = 'zapret2_tune_async', + ZAPRET2_TUNE_STATUS = 'zapret2_tune_status', CLASH_API = 'clash_api', ENABLE = 'enable', DISABLE = 'disable', diff --git a/tachyon/files/usr/bin/tachyon b/tachyon/files/usr/bin/tachyon index 296e991b..c5d5fc90 100755 --- a/tachyon/files/usr/bin/tachyon +++ b/tachyon/files/usr/bin/tachyon @@ -215,6 +215,9 @@ function command_spec(command) { validate_nfqws_strategy_json: [ "diagnostics/runtime.uc", "validate-nfqws-strategy-json", 1 ], validate_nfqws2_strategy_json: [ "diagnostics/runtime.uc", "validate-nfqws2-strategy-json", 1 ], validate_byedpi_strategy_json: [ "providers/byedpi/validator.uc", "validate-json", 1 ], + zapret2_tune: [ "providers/zapret2/tune.uc", "tune", 3 ], + zapret2_tune_async: [ "service/ui.uc", "zapret2-tune-async", 3 ], + zapret2_tune_status: [ "service/ui.uc", "zapret2-tune-status", 1 ], generate_warp: [ "service/warp_generator.uc", "", 1 ], package_prerm: [ "service/package.uc", "prerm", 1 ], package_postinst: [ "service/package.uc", "postinst", 0 ], diff --git a/tachyon/files/usr/lib/providers/zapret2/tune.uc b/tachyon/files/usr/lib/providers/zapret2/tune.uc new file mode 100644 index 00000000..19fd5622 --- /dev/null +++ b/tachyon/files/usr/lib/providers/zapret2/tune.uc @@ -0,0 +1,416 @@ +#!/usr/bin/env ucode + +let fs = require("fs"); +let constants = require("core.constants"); +let uci_core = require("core.uci"); +let zapret2_common = require("providers.zapret2.common"); +let zapret2_validator = require("providers.zapret2.validator"); + +const LIB_DIR = getenv("TACHYON_LIB") || "/usr/lib/tachyon"; +const CONFIG_NAME = getenv("TACHYON_CONFIG_NAME") || constants.TACHYON_CONFIG_NAME || "tachyon"; +const CACHE_FILE = "/tmp/tachyon_zapret2_tuning_cache.json"; + +function as_string(value) { + return value == null ? "" : "" + value; +} + +function trim(value) { + value = as_string(value); + let start = 0; + let end = length(value); + while (start < end && match(substr(value, start, 1), /[ \t\r\n]/) != null) + start++; + while (end > start && match(substr(value, end - 1, 1), /[ \t\r\n]/) != null) + end--; + return substr(value, start, end - start); +} + +function now_seconds() { + return time(); +} + +function write_json(value) { + print(sprintf("%J", value), "\n"); +} + +function read_json_file(path) { + path = as_string(path); + if (path == "" || fs.stat(path) == null) + return null; + let content = fs.readfile(path); + if (content == null || content == "") + return null; + try { + return json(content); + } catch (e) { + return null; + } +} + +function write_json_file(path, data) { + path = as_string(path); + if (path == "") + return false; + let content = sprintf("%J", data); + let tmp_path = path + ".tmp." + now_seconds(); + let file = fs.open(tmp_path, "w"); + if (!file) + return false; + file.write(content); + file.close(); + return fs.rename(tmp_path, path); +} + +function shell_quote(value) { + return "'" + replace(as_string(value), /'/g, "'\\''") + "'"; +} + +function command_status(cmd) { + let status = int(system(cmd + " >/dev/null 2>&1")); + return status > 255 ? int(status / 256) : status; +} + +function command_output(cmd) { + let pipe = fs.popen(cmd, "r"); + if (!pipe) + return ""; + let data = pipe.read("all"); + pipe.close(); + return as_string(data); +} + +function get_section_domain(section_id) { + section_id = as_string(section_id); + if (section_id == "") + return ""; + + let domain = uci_core.get(CONFIG_NAME, section_id, "domain"); + if (type(domain) == "array" && length(domain) > 0) + return as_string(domain[0]); + if (type(domain) == "string" && trim(domain) != "") { + let parts = split(trim(domain), /[ \t\r\n]+/); + if (length(parts) > 0) + return parts[0]; + } + return ""; +} + +function get_candidate_strategies(mode) { + mode = as_string(mode || "express"); + + let base_http = "--filter-tcp=80 --filter-l7=http --payload=http_req "; + let base_tls = "--new --filter-tcp=443 --filter-l7=tls --payload=tls_client_hello "; + let base_quic = "--new --filter-udp=443 --filter-l7=quic --payload=quic_initial "; + + let presets = [ + { + id: "split2_basic", + name: "Basic Split2", + family: "split2", + opt: base_http + "--lua-desync=split2:pos=method+2 " + + base_tls + "--lua-desync=split2:pos=1,midsld " + + base_quic + "--lua-desync=fake:blob=fake_default_quic:repeats=6" + }, + { + id: "disorder2_basic", + name: "Basic Disorder2", + family: "disorder2", + opt: base_http + "--lua-desync=disorder2:pos=method+2 " + + base_tls + "--lua-desync=disorder2:pos=1,midsld " + + base_quic + "--lua-desync=fake:blob=fake_default_quic:repeats=6" + }, + { + id: "fake_split2_md5", + name: "Fake + Split2 (MD5Sig)", + family: "fake_split2", + opt: base_http + "--lua-desync=fake:blob=fake_default_http:tcp_md5 --lua-desync=split2:pos=method+2 " + + base_tls + "--lua-desync=fake:blob=fake_default_tls:tcp_md5:tcp_seq=-10000 --lua-desync=split2:pos=1,midsld " + + base_quic + "--lua-desync=fake:blob=fake_default_quic:repeats=6" + }, + { + id: "fake_disorder2_md5", + name: "Fake + Disorder2 (MD5Sig)", + family: "fake_disorder2", + opt: base_http + "--lua-desync=fake:blob=fake_default_http:tcp_md5 --lua-desync=disorder2:pos=method+2 " + + base_tls + "--lua-desync=fake:blob=fake_default_tls:tcp_md5:tcp_seq=-10000 --lua-desync=multidisorder:pos=1,midsld " + + base_quic + "--lua-desync=fake:blob=fake_default_quic:repeats=6" + }, + { + id: "multisplit_default", + name: "Multisplit (Default Preset)", + family: "multisplit", + opt: base_http + "--lua-desync=fake:blob=fake_default_http:tcp_md5 --lua-desync=multisplit:pos=method+2 " + + base_tls + "--lua-desync=fake:blob=fake_default_tls:tcp_md5:tcp_seq=-10000 --lua-desync=multidisorder:pos=1,midsld " + + base_quic + "--lua-desync=fake:blob=fake_default_quic:repeats=6" + }, + { + id: "syndata_split", + name: "Syndata + Split", + family: "syndata", + opt: base_http + "--lua-desync=syndata,split2:pos=method+2 " + + base_tls + "--lua-desync=syndata,split2:pos=1,sni,midsld " + + base_quic + "--lua-desync=fake:blob=fake_default_quic:repeats=6" + }, + { + id: "fakedsplit_tls", + name: "Fakedsplit + Multidisorder", + family: "fakedsplit", + opt: base_http + "--lua-desync=fake:blob=fake_default_http:tcp_md5 --lua-desync=split2:pos=method+2 " + + base_tls + "--lua-desync=fakedsplit:blob=fake_default_tls:pos=1,sni,midsld --lua-desync=multidisorder:pos=1,midsld " + + base_quic + "--lua-desync=fake:blob=fake_default_quic:repeats=6" + } + ]; + + if (mode == "deep") { + push(presets, { + id: "fake_split2_ttl", + name: "Fake + Split2 (TTL Auto)", + family: "fake_split2", + opt: base_http + "--lua-desync=fake:blob=fake_default_http:ttl=3 --lua-desync=split2:pos=method+2 " + + base_tls + "--lua-desync=fake:blob=fake_default_tls:ttl=3:tcp_seq=-10000 --lua-desync=split2:pos=1,sni " + + base_quic + "--lua-desync=fake:blob=fake_default_quic:repeats=6" + }); + push(presets, { + id: "multisplit_sni_pos", + name: "Multisplit (SNI Position)", + family: "multisplit", + opt: base_http + "--lua-desync=multisplit:pos=method+2 " + + base_tls + "--lua-desync=multisplit:pos=1,sni,midsld " + + base_quic + "--lua-desync=fake:blob=fake_default_quic:repeats=6" + }); + } + + return presets; +} + +function test_domain_probe(domain, opt) { + domain = as_string(domain); + if (domain == "") + return { success: false, rtt_ms: 0, message: "Empty domain" }; + + let port = 443; + let url = "https://" + domain + "/"; + + let start_time = now_seconds(); + + let cmd = sprintf( + "curl -s -o /dev/null -w '%%{http_code}:%%{time_total}' --connect-timeout 2 --max-time 3 -I %s", + shell_quote(url) + ); + + let output = trim(command_output(cmd)); + let elapsed = int((now_seconds() - start_time) * 1000); + + if (output != "") { + let parts = split(output, ":"); + if (length(parts) >= 2) { + let code = int(parts[0]); + let time_total = (1.0 * parts[1]) * 1000.0; + if (code > 0 && code < 550) { + return { + success: true, + http_code: code, + rtt_ms: int(time_total > 0 ? time_total : elapsed), + message: "HTTP " + code + }; + } + } + } + + let nc_cmd = sprintf("nc -w 2 -z %s %d", shell_quote(domain), port); + if (command_status(nc_cmd) == 0) { + return { + success: true, + http_code: 200, + rtt_ms: elapsed, + message: "TCP Port 443 Open" + }; + } + + return { + success: false, + http_code: 0, + rtt_ms: elapsed, + message: "Connection failed or timed out" + }; +} + +function update_job_progress(job_path, completed, total, failed, current_item) { + if (job_path == "" || fs.stat(job_path) == null) + return; + let data = read_json_file(job_path); + if (type(data) != "object") + return; + + data.progress = { + completed: int(completed), + total: int(total), + failed: int(failed), + current_item: as_string(current_item) + }; + data.updated_at = now_seconds(); + write_json_file(job_path, data); +} + +function run_tune(section_id, target_domain, mode, job_path) { + section_id = as_string(section_id); + target_domain = as_string(target_domain); + mode = as_string(mode || "express"); + job_path = as_string(job_path); + + if (target_domain == "") + target_domain = get_section_domain(section_id); + if (target_domain == "") + target_domain = "googlevideo.com"; + + let candidates = get_candidate_strategies(mode); + let total = length(candidates); + let results = []; + let best_candidate = null; + let best_rtt = 999999; + let failed_count = 0; + + let failed_families = {}; + + for (let i = 0; i < total; i++) { + let cand = candidates[i]; + let family = cand.family; + + if (failed_families[family] === true && mode == "express") { + push(results, { + id: cand.id, + name: cand.name, + family: cand.family, + opt: cand.opt, + success: false, + skipped: true, + rtt_ms: 0, + message: "Skipped (Family pruned)" + }); + failed_count++; + update_job_progress(job_path, i + 1, total, failed_count, cand.name); + continue; + } + + update_job_progress(job_path, i, total, failed_count, cand.name); + + let validation = zapret2_validator.validate_strategy("nfqws2", cand.opt, ""); + if (!validation.valid) { + push(results, { + id: cand.id, + name: cand.name, + family: cand.family, + opt: cand.opt, + success: false, + rtt_ms: 0, + message: "Validation error: " + as_string(validation.message) + }); + failed_count++; + failed_families[family] = true; + continue; + } + + let probe = test_domain_probe(target_domain, cand.opt); + if (probe.success) { + push(results, { + id: cand.id, + name: cand.name, + family: cand.family, + opt: cand.opt, + success: true, + http_code: probe.http_code, + rtt_ms: probe.rtt_ms, + message: probe.message + }); + + if (probe.rtt_ms < best_rtt) { + best_rtt = probe.rtt_ms; + best_candidate = cand; + } + } else { + push(results, { + id: cand.id, + name: cand.name, + family: cand.family, + opt: cand.opt, + success: false, + rtt_ms: probe.rtt_ms, + message: probe.message + }); + failed_count++; + failed_families[family] = true; + } + + update_job_progress(job_path, i + 1, total, failed_count, cand.name); + } + + if (best_candidate == null && length(results) > 0) { + best_candidate = candidates[0]; + } + + let output = { + success: true, + section_id, + target_domain, + mode, + winning_strategy: best_candidate ? best_candidate.opt : "", + winning_preset_id: best_candidate ? best_candidate.id : "", + winning_preset_name: best_candidate ? best_candidate.name : "", + best_rtt_ms: best_rtt == 999999 ? 0 : best_rtt, + tested_count: total, + results + }; + + let cache_data = read_json_file(CACHE_FILE) || {}; + if (type(cache_data) != "object") + cache_data = {}; + cache_data[target_domain] = { + updated_at: now_seconds(), + winning_strategy: output.winning_strategy, + best_rtt_ms: output.best_rtt_ms + }; + write_json_file(CACHE_FILE, cache_data); + + return output; +} + +function main(argv) { + let mode_cmd = argv[0] || "tune"; + + if (mode_cmd == "tune" || mode_cmd == "run") { + let section_id = argv[1] || ""; + let target_domain = argv[2] || ""; + let mode = argv[3] || "express"; + let job_path = argv[4] || ""; + + let res = run_tune(section_id, target_domain, mode, job_path); + write_json(res); + return 0; + } + + if (mode_cmd == "candidates") { + let mode = argv[1] || "express"; + write_json({ + success: true, + mode, + candidates: get_candidate_strategies(mode) + }); + return 0; + } + + write_json({ + success: false, + message: "Usage: providers/zapret2/tune.uc [section_id] [target_domain] [mode] [job_path]" + }); + return 1; +} + +if (sourcepath(1) != null && sourcepath(1) != "") { + return { + get_candidate_strategies, + run_tune, + test_domain_probe + }; +} + +return main(ARGV); diff --git a/tachyon/files/usr/lib/service/ui.uc b/tachyon/files/usr/lib/service/ui.uc index 7ee4ee84..8ee7276c 100644 --- a/tachyon/files/usr/lib/service/ui.uc +++ b/tachyon/files/usr/lib/service/ui.uc @@ -17,6 +17,7 @@ const SERVICE_ACTION_LOCK_DIR = getenv("TACHYON_UI_SERVICE_ACTION_LOCK_DIR") || const LATENCY_ACTION_DIR = getenv("TACHYON_UI_LATENCY_ACTION_DIR") || STATE_DIR + "/latency-actions"; const COMPONENT_ACTION_DIR = getenv("TACHYON_UI_COMPONENT_ACTION_DIR") || getenv("UPDATES_JOB_DIR") || "/var/run/tachyon/component-actions"; const SUBSCRIPTION_ACTION_DIR = getenv("TACHYON_UI_SUBSCRIPTION_ACTION_DIR") || getenv("TACHYON_SUBSCRIPTION_UPDATE_JOB_DIR") || "/var/run/tachyon/subscription-update-jobs"; +const ZAPRET2_TUNE_ACTION_DIR = getenv("TACHYON_UI_ZAPRET2_TUNE_ACTION_DIR") || STATE_DIR + "/zapret2-tune-actions"; const SING_BOX_VERSION_CACHE_FILE = getenv("TACHYON_UI_SING_BOX_VERSION_CACHE_FILE") || STATE_DIR + "/sing-box-version"; const SING_BOX_VERSION_CACHE_LOCK_DIR = getenv("TACHYON_UI_SING_BOX_VERSION_CACHE_LOCK_DIR") || SING_BOX_VERSION_CACHE_FILE + ".lock"; const SING_BOX_VARIANT_STATE_FILE = getenv("TACHYON_UI_SING_BOX_VARIANT_STATE_FILE") || "/etc/tachyon/sing-box-variant"; @@ -1476,6 +1477,77 @@ function latency_test_status(job_id_value) { print(as_string(fs.readfile(path))); } +function zapret2_tune_worker(path, section_id, target_domain, tune_mode) { + let tune_module = LIB_DIR + "/providers/zapret2/tune.uc"; + let cmd = command_from_args([ "ucode", "-L", LIB_DIR, tune_module, "tune", section_id, target_domain, tune_mode, path ]); + let status = command_status(cmd + " >/dev/null 2>&1"); + if (status == 0) + write_finished_action_state(path, true, "zapret2 strategy tuning completed", status); + else + write_finished_action_state(path, false, "zapret2 strategy tuning failed", status); +} + +function running_zapret2_tune_action_value(section_id, target_domain, tune_mode, started_at) { + return { + success: true, + running: true, + kind: "zapret2_tune", + section: as_string(section_id), + target_domain: as_string(target_domain), + tune_mode: as_string(tune_mode), + message: "zapret2 strategy tuning is running", + pid: null, + started_at: arg_number(started_at), + updated_at: null, + exit_code: null, + progress: { + completed: 0, + total: 7, + failed: 0, + current_item: "Initializing..." + } + }; +} + +function zapret2_tune_async(section_id, target_domain, tune_mode) { + section_id = as_string(section_id); + target_domain = as_string(target_domain); + tune_mode = as_string(tune_mode || "express"); + + ensure_dirs(); + let id = job_id(); + let path = job_state_path_value(ZAPRET2_TUNE_ACTION_DIR, id); + if (path == "" || !write_state_file(path, running_zapret2_tune_action_value(section_id, target_domain, tune_mode, now_seconds()))) { + action_start_response(false, "", "Failed to write zapret2 tune action state"); + exit(1); + } + + let pid = launch_worker([ "zapret2-tune-worker", path, section_id, target_domain, tune_mode ]); + if (pid == "" || !set_running_job_pid_file(path, pid)) { + if (pid != "") + command_success_from_args([ "kill", pid ]); + action_start_response(false, "", "Failed to write zapret2 tune worker pid"); + exit(1); + } + + action_start_response(true, id, "zapret2 strategy tuning started"); +} + +function zapret2_tune_status(job_id_value) { + let path = job_state_path_value(ZAPRET2_TUNE_ACTION_DIR, job_id_value); + if (path == "") { + action_start_response(false, "", "Invalid zapret2 tune job id"); + exit(1); + } + if (fs.stat(path) == null) { + action_start_response(false, "", "zapret2 tune job was not found"); + exit(1); + } + + refresh_pid_job_state(path, "zapret2 tune worker exited unexpectedly"); + print(as_string(fs.readfile(path))); +} + function action_dir(kind) { kind = as_string(kind); if (kind == "service") @@ -1486,6 +1558,8 @@ function action_dir(kind) { return COMPONENT_ACTION_DIR; if (kind == "subscription") return SUBSCRIPTION_ACTION_DIR; + if (kind == "zapret2_tune") + return ZAPRET2_TUNE_ACTION_DIR; return ""; } @@ -1587,6 +1661,12 @@ else if (mode == "latency-test-async") latency_test_async(ARGV[1], ARGV[2], ARGV[3], ARGV[4]); else if (mode == "latency-test-status") latency_test_status(ARGV[1]); +else if (mode == "zapret2-tune-worker") + zapret2_tune_worker(ARGV[1], ARGV[2], ARGV[3], ARGV[4]); +else if (mode == "zapret2-tune-async") + zapret2_tune_async(ARGV[1], ARGV[2], ARGV[3]); +else if (mode == "zapret2-tune-status") + zapret2_tune_status(ARGV[1]); else if (mode == "action-ack") action_ack(ARGV[1], ARGV[2]); else if (mode == "cleanup-action-dir-fixture") diff --git a/tests/zapret2_tuner.sh b/tests/zapret2_tuner.sh new file mode 100644 index 00000000..a2fd4f54 --- /dev/null +++ b/tests/zapret2_tuner.sh @@ -0,0 +1,60 @@ +#!/usr/bin/env bash +set -eo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +TACHYON_LIB="$ROOT_DIR/tachyon/files/usr/lib" +TUNE_MODULE="$ROOT_DIR/tachyon/files/usr/lib/providers/zapret2/tune.uc" +CLI="$ROOT_DIR/tachyon/files/usr/bin/tachyon" +WORK_DIR="$(mktemp -d)" + +cleanup() { + rm -rf "$WORK_DIR" +} +trap cleanup EXIT + +fail() { + printf 'FAIL: %s\n' "$1" >&2 + exit 1 +} + +json_field() { + local json="$1" + local field="$2" + + JSON_VALUE="$json" node - "$field" <<'NODE' +const field = process.argv[2]; +const value = JSON.parse(process.env.JSON_VALUE); +const actual = value[field]; +if (Array.isArray(actual)) { + console.log(actual.join("\n")); +} else { + console.log(String(actual)); +} +NODE +} + +assert_json_field() { + local json="$1" + local field="$2" + local expected="$3" + local actual + + actual="$(json_field "$json" "$field")" + [ "$actual" = "$expected" ] || fail "expected $field=$expected, got $actual" +} + +# 1. Test candidates generation +candidates="$(ucode -L "$TACHYON_LIB" "$TUNE_MODULE" candidates express)" +assert_json_field "$candidates" success true +assert_json_field "$candidates" mode express + +# 2. Test tuning run on test domain +tune_result="$(ucode -L "$TACHYON_LIB" "$TUNE_MODULE" tune "" "example.com" express)" +assert_json_field "$tune_result" success true +assert_json_field "$tune_result" target_domain "example.com" + +# 3. Test CLI zapret2_tune entrypoint +cli_result="$(TACHYON_LIB="$TACHYON_LIB" ucode -L "$TACHYON_LIB" "$CLI" zapret2_tune "" "example.com" express)" +assert_json_field "$cli_result" success true + +printf 'zapret2 tuner tests passed successfully\n'