1517 lines
56 KiB
Ucode
1517 lines
56 KiB
Ucode
#!/usr/bin/env ucode
|
|
|
|
let fs = require("fs");
|
|
let common = require("core.common");
|
|
let uci_core = require("core.uci");
|
|
let rag = require("diagnostics.rag");
|
|
|
|
let as_string = common.as_string;
|
|
let read_json_file = common.read_json_file;
|
|
let write_json_file = common.write_json_file;
|
|
let command_from_args = common.command_from_args;
|
|
let command_output = common.command_output;
|
|
let command_status = common.command_status;
|
|
let command_success = common.command_success;
|
|
let command_success_from_args = common.command_success_from_args;
|
|
let shell_quote = common.shell_quote;
|
|
let object_or_empty = common.object_or_empty;
|
|
let array_or_empty = common.array_or_empty;
|
|
|
|
const CONFIG_NAME = getenv("TACHYON_CONFIG_NAME") || "tachyon";
|
|
const STATE_DIR = getenv("TACHYON_FUZZER_STATE_DIR") || "/var/run/tachyon";
|
|
const STATE_FILE = STATE_DIR + "/fuzzer-state.json";
|
|
const PID_FILE = STATE_DIR + "/fuzzer-worker.pid";
|
|
const BYEDPI_PORT = 11089;
|
|
const NFQUEUE_QNUM_ZAPRET = 298;
|
|
const NFQUEUE_QNUM_ZAPRET2 = 299;
|
|
const FUZZER_FWMARK = "0x40000000";
|
|
|
|
function resolve_binary(paths) {
|
|
for (let p in paths) {
|
|
if (p && fs.stat(p) != null)
|
|
return p;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function get_zapret2_bin() {
|
|
return resolve_binary([
|
|
getenv("ZAPRET2_NFQWS2_BIN"),
|
|
"/opt/zapret2/nfq2/nfqws2",
|
|
"/opt/zapret2/nfqws2",
|
|
"/usr/bin/nfqws2"
|
|
]);
|
|
}
|
|
|
|
function get_zapret_bin() {
|
|
return resolve_binary([
|
|
getenv("ZAPRET_NFQWS_BIN"),
|
|
"/opt/zapret/nfq/nfqws",
|
|
"/opt/zapret/nfqws",
|
|
"/usr/bin/nfqws"
|
|
]);
|
|
}
|
|
|
|
function get_byedpi_bin() {
|
|
return resolve_binary([
|
|
getenv("BYEDPI_BIN"),
|
|
"/opt/byedpi/ciadpi",
|
|
"/usr/bin/ciadpi"
|
|
]);
|
|
}
|
|
|
|
function validate_strategy_args(engine, args_val) {
|
|
args_val = trim(as_string(args_val));
|
|
if (args_val == "") return false;
|
|
engine = lc(as_string(engine));
|
|
try {
|
|
if (engine == "zapret2") {
|
|
let val = require("providers.zapret2.validator");
|
|
let res = val.validate_strategy("nfqws2", args_val, "");
|
|
return res ? res.valid == true : true;
|
|
} else if (engine == "zapret") {
|
|
let val = require("providers.zapret.validator");
|
|
let res = val.validate_strategy("nfqws", args_val, "");
|
|
return res ? res.valid == true : true;
|
|
} else if (engine == "byedpi") {
|
|
let val = require("providers.byedpi.validator");
|
|
let res = val.validate_strategy(args_val, "");
|
|
return res ? res.valid == true : true;
|
|
}
|
|
} catch (e) {
|
|
return true;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
const PATTERNS_FILE = "/etc/tachyon/fuzzer_patterns.json";
|
|
|
|
const DEFAULT_PATTERNS = {
|
|
zapret2: {
|
|
splits: [ "1", "2", "3", "midsld", "sniext+4", "1,midsld", "1,sniext+2" ],
|
|
foolings: [ "badseq", "md5sig", "badack", "datanoack", "fakeddrop" ],
|
|
ttls: [ 2, 3, 4, 5, 6, 8 ],
|
|
seqovls: [ "1", "2" ],
|
|
wsizes: [ "1" ],
|
|
payloads: [ "tls_client_hello", "http_req", "quic_initial" ]
|
|
},
|
|
zapret: {
|
|
splits: [ "1", "2", "midsld", "sniext+4", "1,midsld" ],
|
|
foolings: [ "badseq", "md5sig", "badack", "datanoack" ],
|
|
ttls: [ 2, 3, 4, 6, 8 ],
|
|
split_modes: [ "split2", "disorder2", "fake,split2", "fake,disorder2" ]
|
|
},
|
|
byedpi: {
|
|
splits: [ "1", "2", "1+sniext", "midsld" ],
|
|
disorders: [ "1", "2" ],
|
|
ttls: [ 2, 3, 4, 6, 8 ],
|
|
oobs: [ "1", "2" ],
|
|
autos: [ "t,r,a,s", "r,s", "t,a" ],
|
|
tlsrecs: [ "1+sniext" ],
|
|
ipfrags: [ "24" ]
|
|
},
|
|
custom_strategies: []
|
|
};
|
|
|
|
function get_patterns_config() {
|
|
let custom = read_json_file(PATTERNS_FILE);
|
|
if (custom && type(custom) == "object") {
|
|
return {
|
|
zapret2: custom.zapret2 || DEFAULT_PATTERNS.zapret2,
|
|
zapret: custom.zapret || DEFAULT_PATTERNS.zapret,
|
|
byedpi: custom.byedpi || DEFAULT_PATTERNS.byedpi,
|
|
custom_strategies: custom.custom_strategies || []
|
|
};
|
|
}
|
|
return DEFAULT_PATTERNS;
|
|
}
|
|
|
|
function save_patterns_config(cfg_obj) {
|
|
if (!cfg_obj || type(cfg_obj) != "object") {
|
|
print(sprintf("%J\n", { success: false, error: "Invalid patterns configuration object" }));
|
|
return;
|
|
}
|
|
common.ensure_dir("/etc/tachyon");
|
|
write_json_file(PATTERNS_FILE, cfg_obj);
|
|
print(sprintf("%J\n", { success: true, message: "Patterns configuration saved successfully" }));
|
|
}
|
|
|
|
function reset_patterns_config() {
|
|
try { fs.unlink(PATTERNS_FILE); } catch(e) {}
|
|
print(sprintf("%J\n", { success: true, message: "Patterns configuration reset to factory defaults", patterns: DEFAULT_PATTERNS }));
|
|
}
|
|
|
|
// Target Suites & Definitions
|
|
const TARGET_SUITES = {
|
|
youtube_suite: {
|
|
name: "YouTube Full Suite (Web + Static CDN + 4K 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 }
|
|
]
|
|
},
|
|
discord_suite: {
|
|
name: "Discord Full Suite (API + WSS Gateway + CDN)",
|
|
urls: [
|
|
{ name: "API Gateway", url: "https://discord.com/api/v9/gateway", weight: 40 },
|
|
{ name: "Media Attachments CDN", url: "https://media.discordapp.net/", weight: 30 },
|
|
{ name: "Global Assets CDN", url: "https://cdn.discordapp.com/generate_204", weight: 30 }
|
|
]
|
|
},
|
|
instagram_suite: {
|
|
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 }
|
|
]
|
|
},
|
|
telegram_suite: {
|
|
name: "Telegram Suite (Web + API)",
|
|
urls: [
|
|
{ name: "Web App", url: "https://web.telegram.org", weight: 50 },
|
|
{ name: "Bot API", url: "https://api.telegram.org", weight: 50 }
|
|
]
|
|
},
|
|
rutracker_suite: {
|
|
name: "RuTracker Suite (HTTP / HTTPS)",
|
|
urls: [
|
|
{ name: "Main Portal", url: "https://rutracker.org", weight: 60 },
|
|
{ name: "CDN Static Logo", url: "https://static.rutracker.cc/logo/logo-3.png", weight: 40 }
|
|
]
|
|
}
|
|
};
|
|
|
|
const TARGET_URLS = {
|
|
youtube_suite: "https://www.youtube.com",
|
|
youtube: "https://www.youtube.com",
|
|
youtube_web: "https://www.youtube.com",
|
|
discord_suite: "https://discord.com/api/v9/gateway",
|
|
discord: "https://discord.com/api/v9/gateway",
|
|
instagram_suite: "https://www.instagram.com",
|
|
instagram: "https://www.instagram.com",
|
|
rutracker_suite: "https://rutracker.org",
|
|
rutracker: "https://rutracker.org",
|
|
telegram_suite: "https://web.telegram.org",
|
|
telegram: "https://web.telegram.org",
|
|
quic_http3: "https://www.google.com"
|
|
};
|
|
|
|
// Strategy Matrices (Expanded Production Suite: 38 strategies across engines)
|
|
const STRATEGIES_ZAPRET2 = [
|
|
{
|
|
id: "z2_fake_ttl4_badseq",
|
|
name: "Fake (TTL=4, BadSeq) + Multisplit",
|
|
engine: "zapret2",
|
|
args: "--lua-desync=fake:ttl=4:fooling=badseq --lua-desync=multisplit:pos=1,midsld",
|
|
description: "Low-TTL fake ClientHello with badseq fooling and multisplit segmentation."
|
|
},
|
|
{
|
|
id: "z2_fake_ttl5_md5sig",
|
|
name: "Fake (TTL=5, MD5Sig) + Multisplit",
|
|
engine: "zapret2",
|
|
args: "--lua-desync=fake:ttl=5:fooling=md5sig --lua-desync=multisplit:pos=1,midsld",
|
|
description: "MD5Sig TCP option drops packet at DPI while reaching end server."
|
|
},
|
|
{
|
|
id: "z2_fake_ttl4_badack",
|
|
name: "Fake (TTL=4, BadACK) + Multisplit",
|
|
engine: "zapret2",
|
|
args: "--lua-desync=fake:ttl=4:fooling=badack --lua-desync=multisplit:pos=1,sniext+2",
|
|
description: "BadACK fooling invalidates packet in DPI state tracking."
|
|
},
|
|
{
|
|
id: "z2_fake_ttl3_md5sig",
|
|
name: "Fake (TTL=3, MD5Sig) + Multisplit",
|
|
engine: "zapret2",
|
|
args: "--lua-desync=fake:ttl=3:fooling=md5sig --lua-desync=multisplit:pos=1,midsld",
|
|
description: "Aggressive low-TTL MD5Sig injection for close TSPU hops."
|
|
},
|
|
{
|
|
id: "z2_yt_multisplit_midsld",
|
|
name: "YouTube 4K Multisplit + MidSLD",
|
|
engine: "zapret2",
|
|
args: "--lua-desync=multisplit:pos=1,midsld:seqovl=1:fooling=badseq",
|
|
description: "Optimized for GoogleVideo 4K chunk streams and TSPU TLS desync."
|
|
},
|
|
{
|
|
id: "z2_yt_multisplit_sniext",
|
|
name: "SNI Extension Split + Badseq",
|
|
engine: "zapret2",
|
|
args: "--lua-desync=multisplit:pos=1,sniext+4:seqovl=1:fooling=badseq",
|
|
description: "Splits deep into SNI extensions to fool next-gen DPI signatures."
|
|
},
|
|
{
|
|
id: "z2_fake_badseq_mid",
|
|
name: "Fake Packet (TTL=8) + MidSLD Split",
|
|
engine: "zapret2",
|
|
args: "--lua-desync=fake:ttl=8:fooling=badseq --lua-desync=multisplit:pos=midsld",
|
|
description: "Injects fake ClientHello before segmented payload."
|
|
},
|
|
{
|
|
id: "z2_wsize_multisplit",
|
|
name: "Window Size Clamp (wsize=1)",
|
|
engine: "zapret2",
|
|
args: "--lua-desync=multisplit:pos=1,midsld:wsize=1:fooling=badseq",
|
|
description: "Forces single-byte TCP window segments to evade reassembly."
|
|
},
|
|
{
|
|
id: "z2_wsize_seqovl_combo",
|
|
name: "Window Clamp (wsize=1) + SeqOvl",
|
|
engine: "zapret2",
|
|
args: "--lua-desync=multisplit:pos=1,midsld:wsize=1:seqovl=1:fooling=badseq",
|
|
description: "Combines 1-byte window clamp with sequence overlap."
|
|
},
|
|
{
|
|
id: "z2_aggressive_combo",
|
|
name: "Aggressive Triple-Split + SeqOvl 2",
|
|
engine: "zapret2",
|
|
args: "--lua-desync=multisplit:pos=1,midsld,sniext+2:seqovl=2:fooling=badseq",
|
|
description: "High-entropy triple fragmentation for heavily filtered regions."
|
|
},
|
|
{
|
|
id: "z2_split2_pos1",
|
|
name: "Classic Split2 (pos=1)",
|
|
engine: "zapret2",
|
|
args: "--lua-desync=split2:pos=1:fooling=badseq",
|
|
description: "Standard 2-fragment desync for compatibility."
|
|
},
|
|
{
|
|
id: "z2_disorder2_pos1",
|
|
name: "Classic Disorder2 (pos=1)",
|
|
engine: "zapret2",
|
|
args: "--lua-desync=disorder2:pos=1:fooling=badseq",
|
|
description: "Sends out-of-order segment with badseq fooling."
|
|
},
|
|
{
|
|
id: "z2_fake_datanoack",
|
|
name: "Fake (TTL=8, DataNoAck) + Split2",
|
|
engine: "zapret2",
|
|
args: "--lua-desync=fake:ttl=8:fooling=datanoack --lua-desync=split2:pos=1",
|
|
description: "DataNoAck fooling confuses stateful DPI without triggering ACK RST."
|
|
},
|
|
{
|
|
id: "z2_multiprofile_discord",
|
|
name: "Discord Multi-Profile (TCP + UDP)",
|
|
engine: "zapret2",
|
|
args: "--filter-tcp=443 --lua-desync=multisplit:pos=1,midsld:seqovl=1 --new --filter-udp=50000-65535 --lua-desync=fake:ttl=8",
|
|
description: "Combined profile: multisplit for HTTPS/API and fake UDP for Discord Voice."
|
|
},
|
|
{
|
|
id: "z2_discord_udp",
|
|
name: "Discord Voice UDP Desync",
|
|
engine: "zapret2",
|
|
args: "--filter-udp=50000-65535 --lua-desync=fake:ttl=8",
|
|
description: "UDP fake packet desync for Discord RTC and Voice channels."
|
|
},
|
|
{
|
|
id: "z2_quic_http3_udp",
|
|
name: "QUIC / HTTP3 UDP Fake Desync",
|
|
engine: "zapret2",
|
|
args: "--filter-udp=443 --lua-desync=fake:ttl=8",
|
|
description: "UDP fake desync for QUIC/HTTP3 protocol bypass."
|
|
}
|
|
];
|
|
|
|
const STRATEGIES_ZAPRET = [
|
|
{
|
|
id: "z1_split2_pos2",
|
|
name: "Standard Split2 (pos=2)",
|
|
engine: "zapret",
|
|
args: "--dpi-desync=split2 --dpi-desync-split-pos=2",
|
|
description: "Base HTTP/TLS split inside SNI header."
|
|
},
|
|
{
|
|
id: "z1_split2_pos1",
|
|
name: "Standard Split2 (pos=1)",
|
|
engine: "zapret",
|
|
args: "--dpi-desync=split2 --dpi-desync-split-pos=1",
|
|
description: "1-byte TLS ClientHello split."
|
|
},
|
|
{
|
|
id: "z1_disorder2_badseq",
|
|
name: "Disorder2 + BadSeq (pos=1)",
|
|
engine: "zapret",
|
|
args: "--dpi-desync=disorder2 --dpi-desync-split-pos=1 --dpi-desync-fooling=badseq",
|
|
description: "Sends out-of-order packets with invalid TCP sequence fooling."
|
|
},
|
|
{
|
|
id: "z1_fake_split2_ttl8",
|
|
name: "Fake SNI + Split2 (TTL=8)",
|
|
engine: "zapret",
|
|
args: "--dpi-desync=fake,split2 --dpi-desync-split-pos=1 --dpi-desync-ttl=8 --dpi-desync-fooling=badseq",
|
|
description: "Sends TTL=8 fake packet followed by segmented ClientHello."
|
|
},
|
|
{
|
|
id: "z1_fake_split2_ttl6",
|
|
name: "Fake SNI + Split2 (TTL=6)",
|
|
engine: "zapret",
|
|
args: "--dpi-desync=fake,split2 --dpi-desync-split-pos=1 --dpi-desync-ttl=6 --dpi-desync-fooling=badseq",
|
|
description: "Sends TTL=6 fake packet for closer TSPU hops."
|
|
},
|
|
{
|
|
id: "z1_fake_split2_ttl4",
|
|
name: "Fake SNI + Split2 (TTL=4)",
|
|
engine: "zapret",
|
|
args: "--dpi-desync=fake,split2 --dpi-desync-split-pos=1 --dpi-desync-ttl=4 --dpi-desync-fooling=badseq",
|
|
description: "Low TTL=4 fake packet for nearest TSPU filters."
|
|
},
|
|
{
|
|
id: "z1_fake_disorder2_ttl8",
|
|
name: "Fake SNI + Disorder2 (TTL=8)",
|
|
engine: "zapret",
|
|
args: "--dpi-desync=fake,disorder2 --dpi-desync-split-pos=1 --dpi-desync-ttl=8 --dpi-desync-fooling=badseq",
|
|
description: "Sends fake packet and disorders real segments."
|
|
},
|
|
{
|
|
id: "z1_seqovl_split2_1",
|
|
name: "Sequence Overlap (SeqOvl=1)",
|
|
engine: "zapret",
|
|
args: "--dpi-desync=split2 --dpi-desync-split-seqovl=1 --dpi-desync-fooling=badseq",
|
|
description: "1-byte overlapping TCP payload to confuse stateful DPI."
|
|
},
|
|
{
|
|
id: "z1_seqovl_split2_2",
|
|
name: "Sequence Overlap (SeqOvl=2)",
|
|
engine: "zapret",
|
|
args: "--dpi-desync=split2 --dpi-desync-split-seqovl=2 --dpi-desync-fooling=badseq",
|
|
description: "2-byte overlapping TCP payload for aggressive DPI desync."
|
|
},
|
|
{
|
|
id: "z1_md5sig_disorder",
|
|
name: "MD5Sig Fooling + Disorder (TTL=6)",
|
|
engine: "zapret",
|
|
args: "--dpi-desync=fake,disorder2 --dpi-desync-fooling=md5sig --dpi-desync-ttl=6",
|
|
description: "Injects TCP MD5 signature option to trigger DPI packet drop."
|
|
},
|
|
{
|
|
id: "z1_badack_disorder",
|
|
name: "BadACK Fooling + Disorder (TTL=8)",
|
|
engine: "zapret",
|
|
args: "--dpi-desync=fake,disorder2 --dpi-desync-fooling=badack --dpi-desync-ttl=8",
|
|
description: "Injects BadACK sequence to break TCP state tracking."
|
|
},
|
|
{
|
|
id: "z1_fake_midsld_split",
|
|
name: "Fake + MidSLD Split (TTL=8)",
|
|
engine: "zapret",
|
|
args: "--dpi-desync=fake,split2 --dpi-desync-split-pos=midsld --dpi-desync-ttl=8 --dpi-desync-fooling=badseq",
|
|
description: "Splits in middle of domain name with fake injection."
|
|
}
|
|
];
|
|
|
|
const STRATEGIES_BYEDPI = [
|
|
{
|
|
id: "bd_auto_tr_d2",
|
|
name: "ByeDPI Auto (t,r,a,s) + Disorder",
|
|
engine: "byedpi",
|
|
args: "-o 2 --auto=t,r,a,s -d 2",
|
|
description: "Adaptive ByeDPI auto-mode with disorder and OOB."
|
|
},
|
|
{
|
|
id: "bd_auto_tr_s1",
|
|
name: "ByeDPI Auto (t,r,a,s) + Split",
|
|
engine: "byedpi",
|
|
args: "-o 1 --auto=t,r,a,s -s 1",
|
|
description: "Adaptive ByeDPI auto-mode with 1-byte split."
|
|
},
|
|
{
|
|
id: "bd_disorder_fake_ttl8",
|
|
name: "Disorder + Fake (TTL=8)",
|
|
engine: "byedpi",
|
|
args: "--split 1 --disorder 1 --fake -1 --ttl 8",
|
|
description: "1-byte split with reverse disorder and fake handshake packet."
|
|
},
|
|
{
|
|
id: "bd_disorder_fake_ttl6",
|
|
name: "Disorder + Fake (TTL=6)",
|
|
engine: "byedpi",
|
|
args: "--split 1 --disorder 1 --fake -1 --ttl 6",
|
|
description: "1-byte split with fake TTL=6 for intermediate hops."
|
|
},
|
|
{
|
|
id: "bd_disorder_fake_ttl4",
|
|
name: "Disorder + Fake (TTL=4)",
|
|
engine: "byedpi",
|
|
args: "--split 1 --disorder 1 --fake -1 --ttl 4",
|
|
description: "1-byte split with low fake TTL=4."
|
|
},
|
|
{
|
|
id: "bd_midsld_fake_frag_t6",
|
|
name: "SNI Extension + Fake (TTL=6)",
|
|
engine: "byedpi",
|
|
args: "-s 1+sniext -f -1 -t 6",
|
|
description: "SNI extension split with low-TTL fake payload."
|
|
},
|
|
{
|
|
id: "bd_midsld_fake_frag_t8",
|
|
name: "SNI Extension + Fake (TTL=8)",
|
|
engine: "byedpi",
|
|
args: "-s 1+sniext -f -1 -t 8",
|
|
description: "SNI extension split with fake TTL=8."
|
|
},
|
|
{
|
|
id: "bd_tls_sni_split2",
|
|
name: "TLS SNI Split + Disorder (pos=2)",
|
|
engine: "byedpi",
|
|
args: "--split 2 --disorder 2",
|
|
description: "Direct TLS SNI offset split with out-of-order delivery."
|
|
},
|
|
{
|
|
id: "bd_tls_sni_split1",
|
|
name: "TLS SNI Split + Disorder (pos=1)",
|
|
engine: "byedpi",
|
|
args: "--split 1 --disorder 1",
|
|
description: "1-byte TLS ClientHello split with disorder."
|
|
},
|
|
{
|
|
id: "bd_tlsrec_sniext",
|
|
name: "TLS Record Split (1+sniext)",
|
|
engine: "byedpi",
|
|
args: "--tlsrec 1+sniext --split 1",
|
|
description: "Fragments TLS Record header before SNI extension."
|
|
},
|
|
{
|
|
id: "bd_ip_frag_24",
|
|
name: "IP Fragmentation (24 bytes)",
|
|
engine: "byedpi",
|
|
args: "--ip-frag 24 --split 1",
|
|
description: "Network layer IP fragmentation on 24-byte boundary."
|
|
},
|
|
{
|
|
id: "bd_fake_sniext_disorder",
|
|
name: "Aggressive Fake (TTL=8) + SNIExt",
|
|
engine: "byedpi",
|
|
args: "--fake -1 --ttl 8 --split 1+sniext --disorder 1",
|
|
description: "Fake handshake with SNI extension split and disorder."
|
|
}
|
|
];
|
|
|
|
function generate_combinatorial_zapret2() {
|
|
let cfg = get_patterns_config();
|
|
let p = cfg.zapret2 || DEFAULT_PATTERNS.zapret2;
|
|
let list = [];
|
|
let seen = {};
|
|
|
|
let add = function(name, args, desc) {
|
|
args = trim(as_string(args));
|
|
if (args == "" || seen[args]) return;
|
|
if (!validate_strategy_args("zapret2", args)) return;
|
|
seen[args] = true;
|
|
push(list, {
|
|
id: sprintf("z2_comb_%d", length(list) + 1),
|
|
name: name,
|
|
engine: "zapret2",
|
|
args: args,
|
|
description: desc
|
|
});
|
|
};
|
|
|
|
for (let s in STRATEGIES_ZAPRET2) add(s.name, s.args, s.description);
|
|
|
|
if (cfg.custom_strategies && length(cfg.custom_strategies) > 0) {
|
|
for (let cs in cfg.custom_strategies) {
|
|
if (cs && cs.engine == "zapret2" && cs.args) {
|
|
add(cs.name || "Custom Zapret v2", cs.args, cs.description || "User custom strategy");
|
|
}
|
|
}
|
|
}
|
|
|
|
let splits = p.splits || [ "1", "2", "3", "midsld", "sniext+4", "1,midsld", "1,sniext+2" ];
|
|
let foolings = p.foolings || [ "badseq", "md5sig", "badack", "datanoack", "fakeddrop" ];
|
|
let ttls = p.ttls || [ 2, 3, 4, 5, 6, 8 ];
|
|
let seqovls = p.seqovls || [ "1", "2" ];
|
|
let wsizes = p.wsizes || [ "1" ];
|
|
|
|
for (let pos in splits) {
|
|
for (let fooling in foolings) {
|
|
add(sprintf("Multisplit (pos=%s, %s)", pos, fooling),
|
|
sprintf("--lua-desync=multisplit:pos=%s:fooling=%s", pos, fooling),
|
|
"Multisplit position and fooling method");
|
|
for (let sq in seqovls) {
|
|
add(sprintf("Multisplit + SeqOvl %s (pos=%s, %s)", sq, pos, fooling),
|
|
sprintf("--lua-desync=multisplit:pos=%s:seqovl=%s:fooling=%s", pos, sq, fooling),
|
|
"Multisplit with sequence overlap");
|
|
}
|
|
for (let w in wsizes) {
|
|
add(sprintf("Multisplit + Window %s (pos=%s, %s)", w, pos, fooling),
|
|
sprintf("--lua-desync=multisplit:pos=%s:wsize=%s:fooling=%s", pos, w, fooling),
|
|
"Multisplit with TCP window size clamping");
|
|
}
|
|
}
|
|
}
|
|
|
|
for (let ttl in ttls) {
|
|
for (let fooling in foolings) {
|
|
for (let pos in splits) {
|
|
add(sprintf("Fake (TTL=%d, %s) + Multisplit (pos=%s)", ttl, fooling, pos),
|
|
sprintf("--lua-desync=fake:ttl=%d:fooling=%s --lua-desync=multisplit:pos=%s", ttl, fooling, pos),
|
|
"Low-TTL fake injection followed by multisplit payload");
|
|
add(sprintf("Fake (TTL=%d, %s) + Split2 (pos=%s)", ttl, fooling, pos),
|
|
sprintf("--lua-desync=fake:ttl=%d:fooling=%s --lua-desync=split2:pos=%s", ttl, fooling, pos),
|
|
"Low-TTL fake injection followed by 2-part split");
|
|
}
|
|
}
|
|
}
|
|
|
|
return list;
|
|
}
|
|
|
|
function generate_combinatorial_zapret() {
|
|
let cfg = get_patterns_config();
|
|
let p = cfg.zapret || DEFAULT_PATTERNS.zapret;
|
|
let list = [];
|
|
let seen = {};
|
|
|
|
let add = function(name, args, desc) {
|
|
args = trim(as_string(args));
|
|
if (args == "" || seen[args]) return;
|
|
if (!validate_strategy_args("zapret", args)) return;
|
|
seen[args] = true;
|
|
push(list, {
|
|
id: sprintf("z1_gen_%d", length(list) + 1),
|
|
name: name,
|
|
engine: "zapret",
|
|
args: args,
|
|
description: desc
|
|
});
|
|
};
|
|
|
|
for (let s in STRATEGIES_ZAPRET) add(s.name, s.args, s.description);
|
|
|
|
if (cfg.custom_strategies && length(cfg.custom_strategies) > 0) {
|
|
for (let cs in cfg.custom_strategies) {
|
|
if (cs && cs.engine == "zapret" && cs.args) {
|
|
add(cs.name || "Custom Zapret v1", cs.args, cs.description || "User custom strategy");
|
|
}
|
|
}
|
|
}
|
|
|
|
let modes = p.split_modes || [ "split2", "disorder2", "fake,split2", "fake,disorder2" ];
|
|
let positions = p.splits || [ "1", "2", "midsld" ];
|
|
let foolings = p.foolings || [ "badseq", "md5sig", "badack", "datanoack" ];
|
|
let ttls = p.ttls || [ 2, 3, 4, 6, 8 ];
|
|
|
|
for (let mode in modes) {
|
|
for (let pos in positions) {
|
|
for (let fooling in foolings) {
|
|
if (index(mode, "fake") >= 0) {
|
|
for (let ttl in ttls) {
|
|
add(sprintf("%s (pos=%s, TTL=%d, %s)", mode, pos, ttl, fooling),
|
|
sprintf("--dpi-desync=%s --dpi-desync-split-pos=%s --dpi-desync-ttl=%d --dpi-desync-fooling=%s", mode, pos, ttl, fooling),
|
|
"Fake desync with split pos and fooling");
|
|
}
|
|
} else {
|
|
add(sprintf("%s (pos=%s, %s)", mode, pos, fooling),
|
|
sprintf("--dpi-desync=%s --dpi-desync-split-pos=%s --dpi-desync-fooling=%s", mode, pos, fooling),
|
|
"Desync with split pos and fooling");
|
|
add(sprintf("%s + SeqOvl (pos=%s, %s)", mode, pos, fooling),
|
|
sprintf("--dpi-desync=%s --dpi-desync-split-pos=%s --dpi-desync-split-seqovl=1 --dpi-desync-fooling=%s", mode, pos, fooling),
|
|
"Desync with sequence overlap");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return list;
|
|
}
|
|
|
|
function generate_combinatorial_byedpi() {
|
|
let cfg = get_patterns_config();
|
|
let p = cfg.byedpi || DEFAULT_PATTERNS.byedpi;
|
|
let list = [];
|
|
let seen = {};
|
|
|
|
let add = function(name, args, desc) {
|
|
args = trim(as_string(args));
|
|
if (args == "" || seen[args]) return;
|
|
if (!validate_strategy_args("byedpi", args)) return;
|
|
seen[args] = true;
|
|
push(list, {
|
|
id: sprintf("bd_gen_%d", length(list) + 1),
|
|
name: name,
|
|
engine: "byedpi",
|
|
args: args,
|
|
description: desc
|
|
});
|
|
};
|
|
|
|
for (let s in STRATEGIES_BYEDPI) add(s.name, s.args, s.description);
|
|
|
|
if (cfg.custom_strategies && length(cfg.custom_strategies) > 0) {
|
|
for (let cs in cfg.custom_strategies) {
|
|
if (cs && cs.engine == "byedpi" && cs.args) {
|
|
add(cs.name || "Custom ByeDPI", cs.args, cs.description || "User custom strategy");
|
|
}
|
|
}
|
|
}
|
|
|
|
let splits = p.splits || [ "1", "2", "1+sniext", "midsld" ];
|
|
let disorders = p.disorders || [ "1", "2" ];
|
|
let ttls = p.ttls || [ 2, 3, 4, 6, 8 ];
|
|
let oobs = p.oobs || [ "1", "2" ];
|
|
let autos = p.autos || [ "t,r,a,s", "r,s", "t,a" ];
|
|
let tlsrecs = p.tlsrecs || [ "1+sniext" ];
|
|
let ipfrags = p.ipfrags || [ "24" ];
|
|
|
|
for (let a in autos) {
|
|
for (let o in oobs) {
|
|
for (let d in disorders) {
|
|
add(sprintf("Auto (%s) + OOB=%s + Disorder=%s", a, o, d),
|
|
sprintf("-o %s --auto=%s -d %s", o, a, d),
|
|
"Adaptive auto mode with OOB and disorder");
|
|
}
|
|
for (let s in splits) {
|
|
add(sprintf("Auto (%s) + OOB=%s + Split=%s", a, o, s),
|
|
sprintf("-o %s --auto=%s -s %s", o, a, s),
|
|
"Adaptive auto mode with OOB and split");
|
|
}
|
|
}
|
|
}
|
|
|
|
for (let ttl in ttls) {
|
|
for (let s in splits) {
|
|
for (let d in disorders) {
|
|
add(sprintf("Split=%s + Disorder=%s + Fake (TTL=%d)", s, d, ttl),
|
|
sprintf("--split %s --disorder %s --fake -1 --ttl %d", s, d, ttl),
|
|
"Fake injection with split and disorder");
|
|
}
|
|
}
|
|
}
|
|
|
|
for (let s in splits) {
|
|
for (let tr in tlsrecs) {
|
|
add(sprintf("TLS-Rec (%s) + Split=%s", tr, s),
|
|
sprintf("--tlsrec %s --split %s", tr, s),
|
|
"TLS record boundary fragmentation");
|
|
}
|
|
for (let ipf in ipfrags) {
|
|
add(sprintf("IP-Frag (%s) + Split=%s", ipf, s),
|
|
sprintf("--ip-frag %s --split %s", ipf, s),
|
|
"IP packet fragmentation");
|
|
}
|
|
}
|
|
|
|
return list;
|
|
}
|
|
|
|
function get_strategies_for_engine(engine, mode) {
|
|
engine = lc(as_string(engine));
|
|
mode = lc(trim(as_string(mode || "presets")));
|
|
let cfg = get_patterns_config();
|
|
|
|
if (mode == "custom" || mode == "user") {
|
|
let custom_list = [];
|
|
for (let cs in cfg.custom_strategies) {
|
|
if (cs && (engine == "all" || cs.engine == engine) && cs.args) {
|
|
push(custom_list, {
|
|
id: cs.id || sprintf("custom_%d", length(custom_list) + 1),
|
|
name: cs.name || "Custom Strategy",
|
|
engine: cs.engine || engine,
|
|
args: cs.args,
|
|
description: cs.description || ""
|
|
});
|
|
}
|
|
}
|
|
return custom_list;
|
|
}
|
|
|
|
if (mode == "combinatorial" || mode == "deep_fuzz" || mode == "deep") {
|
|
if (engine == "zapret2") return generate_combinatorial_zapret2();
|
|
if (engine == "zapret") return generate_combinatorial_zapret();
|
|
if (engine == "byedpi") return generate_combinatorial_byedpi();
|
|
if (engine == "all") {
|
|
let combined = [];
|
|
for (let s in generate_combinatorial_zapret2()) push(combined, s);
|
|
for (let s in generate_combinatorial_zapret()) push(combined, s);
|
|
for (let s in generate_combinatorial_byedpi()) push(combined, s);
|
|
return combined;
|
|
}
|
|
}
|
|
|
|
let base = [];
|
|
if (engine == "zapret2") base = STRATEGIES_ZAPRET2;
|
|
else if (engine == "zapret") base = STRATEGIES_ZAPRET;
|
|
else if (engine == "byedpi") base = STRATEGIES_BYEDPI;
|
|
else if (engine == "all") {
|
|
for (let s in STRATEGIES_ZAPRET2) push(base, s);
|
|
for (let s in STRATEGIES_ZAPRET) push(base, s);
|
|
for (let s in STRATEGIES_BYEDPI) push(base, s);
|
|
}
|
|
|
|
let result = [];
|
|
for (let s in base) push(result, s);
|
|
for (let cs in cfg.custom_strategies) {
|
|
if (cs && (engine == "all" || cs.engine == engine) && cs.args) {
|
|
push(result, {
|
|
id: cs.id || sprintf("custom_%d", length(result) + 1),
|
|
name: cs.name || "Custom Strategy",
|
|
engine: cs.engine || engine,
|
|
args: cs.args,
|
|
description: cs.description || "User custom strategy"
|
|
});
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
function resolve_target_url(target_key, custom_url) {
|
|
if (custom_url && custom_url != "")
|
|
return custom_url;
|
|
return TARGET_URLS[target_key] || TARGET_URLS.youtube;
|
|
}
|
|
|
|
function resolve_target_urls_list(target_key, custom_url) {
|
|
if (custom_url && custom_url != "") {
|
|
return [ { name: "Custom Target", url: custom_url, weight: 100 } ];
|
|
}
|
|
target_key = as_string(target_key || "youtube_suite");
|
|
let suite = TARGET_SUITES[target_key];
|
|
if (suite && suite.urls && length(suite.urls) > 0) {
|
|
return suite.urls;
|
|
}
|
|
let single = TARGET_URLS[target_key] || TARGET_URLS.youtube;
|
|
return [ { name: target_key, url: single, weight: 100 } ];
|
|
}
|
|
|
|
function ensure_state_dir() {
|
|
common.ensure_dir(STATE_DIR);
|
|
}
|
|
|
|
function save_fuzzer_state(state) {
|
|
ensure_state_dir();
|
|
common.write_json_file(STATE_FILE, state);
|
|
}
|
|
|
|
function safe_json_parse(str) {
|
|
if (!str || str == "") return null;
|
|
let obj = null;
|
|
try {
|
|
obj = json(str);
|
|
} catch (e) {
|
|
obj = null;
|
|
}
|
|
return obj;
|
|
}
|
|
|
|
function query_llm(provider, api_key, custom_url, prompt_text, model_override) {
|
|
provider = lc(trim(as_string(provider || "openai")));
|
|
model_override = trim(as_string(model_override || ""));
|
|
|
|
if (provider == "anthropic" || provider == "claude") {
|
|
let api_url = "https://api.anthropic.com/v1/messages";
|
|
let model = model_override != "" ? model_override : "claude-3-5-haiku-20241022";
|
|
let body = {
|
|
model,
|
|
max_tokens: 1000,
|
|
messages: [{ role: "user", content: prompt_text }]
|
|
};
|
|
let cmd = sprintf(
|
|
"curl -s -m 35 --connect-timeout 10 -X POST -H 'x-api-key: %s' -H 'anthropic-version: 2023-06-01' -H 'content-type: application/json' -d %s %s 2>/dev/null",
|
|
shell_quote(api_key),
|
|
shell_quote(sprintf("%J", body)),
|
|
shell_quote(api_url)
|
|
);
|
|
let pipe = fs.popen(cmd, "r");
|
|
let output = pipe ? pipe.read("all") : "";
|
|
if (pipe) pipe.close();
|
|
let parsed = safe_json_parse(output);
|
|
if (parsed && parsed.content && type(parsed.content) == "array" && length(parsed.content) > 0) {
|
|
return parsed.content[0].text;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
let base_url = "https://api.openai.com/v1";
|
|
let default_model = "gpt-4o-mini";
|
|
if (provider == "deepseek") {
|
|
base_url = "https://api.deepseek.com/v1";
|
|
default_model = "deepseek-chat";
|
|
} else if (provider == "openrouter") {
|
|
base_url = "https://openrouter.ai/api/v1";
|
|
default_model = "deepseek/deepseek-chat";
|
|
} else if (provider == "ollama") {
|
|
base_url = custom_url && custom_url != "" ? custom_url : "http://127.0.0.1:11434/v1";
|
|
default_model = "llama3.2";
|
|
} else if (provider == "lmstudio" || provider == "custom") {
|
|
base_url = custom_url && custom_url != "" ? custom_url : "http://127.0.0.1:1234/v1";
|
|
default_model = "local-model";
|
|
}
|
|
|
|
base_url = replace(base_url, /\/+$/, "");
|
|
let api_url = base_url + "/chat/completions";
|
|
let model = model_override != "" ? model_override : default_model;
|
|
let body = {
|
|
model,
|
|
messages: [
|
|
{ role: "system", content: "You are a network censorship and DPI bypass expert. Always return responses formatted strictly as requested." },
|
|
{ role: "user", content: prompt_text }
|
|
],
|
|
temperature: 0.3
|
|
};
|
|
|
|
let auth_header = api_key != "" ? sprintf("-H 'Authorization: Bearer %s'", api_key) : "";
|
|
let cmd = sprintf(
|
|
"curl -s -m 35 --connect-timeout 10 -X POST %s -H 'Content-Type: application/json' -d %s %s 2>/dev/null",
|
|
auth_header,
|
|
shell_quote(sprintf("%J", body)),
|
|
shell_quote(api_url)
|
|
);
|
|
let pipe = fs.popen(cmd, "r");
|
|
let output = pipe ? pipe.read("all") : "";
|
|
if (pipe) pipe.close();
|
|
let parsed = safe_json_parse(output);
|
|
if (parsed && parsed.choices && type(parsed.choices) == "array" && length(parsed.choices) > 0) {
|
|
let msg = parsed.choices[0].message;
|
|
if (msg && msg.content) {
|
|
return msg.content;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function parse_llm_json(raw_text) {
|
|
raw_text = trim(as_string(raw_text));
|
|
if (raw_text == "") return null;
|
|
let direct = safe_json_parse(raw_text);
|
|
if (direct && type(direct) == "object") return direct;
|
|
|
|
let m = match(raw_text, /```json\s*([\s\S]*?)\s*```/);
|
|
if (m && m[1]) {
|
|
let parsed = safe_json_parse(m[1]);
|
|
if (parsed && type(parsed) == "object") return parsed;
|
|
}
|
|
|
|
m = match(raw_text, /\{[\s\S]*\}/);
|
|
if (m && m[0]) {
|
|
let parsed = safe_json_parse(m[0]);
|
|
if (parsed && type(parsed) == "object") return parsed;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function synthesize_ai_strategies(engine, target, custom_url, user_prompt) {
|
|
let current = get_fuzzer_state();
|
|
if (current.running) {
|
|
print(sprintf("%J\n", { success: false, error: "Fuzzer is currently running a benchmark" }));
|
|
return;
|
|
}
|
|
|
|
engine = lc(as_string(engine || "zapret2"));
|
|
target = trim(as_string(target || "youtube_suite"));
|
|
user_prompt = trim(as_string(user_prompt || ""));
|
|
let target_url = resolve_target_url(target, custom_url);
|
|
|
|
let baseline = run_probe(engine, "", target, custom_url);
|
|
|
|
let query_text = sprintf("%s %s %s", engine, target, user_prompt);
|
|
let rag_docs = rag.retrieve(query_text, 4);
|
|
|
|
let uci = uci_core.cursor();
|
|
let ai_sec = uci.get_all(CONFIG_NAME, "ai") || {};
|
|
let provider = ai_sec.provider || "openai";
|
|
let api_key = ai_sec.api_key || "";
|
|
let ai_custom_url = ai_sec.custom_url || "";
|
|
let model_override = ai_sec.model || "";
|
|
|
|
let prompt = sprintf(
|
|
"You are an expert DPI Bypass Engineer specializing in OpenWrt, Zapret, Zapret2 (nfqws2), and ByeDPI (ciadpi).\n" +
|
|
"We need to bypass censorship / TSPU blocking for target service '%s' (%s) using engine '%s'.\n\n" +
|
|
"LIVE PROBE DIAGNOSTICS:\n" +
|
|
"- Direct HTTP Code: %d\n" +
|
|
"- Connect Time: %d ms\n" +
|
|
"- TTFB: %d ms\n" +
|
|
"- Probe Error: %s\n" +
|
|
"- User Notes / ISP Context: %s\n\n" +
|
|
"TECHNICAL KNOWLEDGE BASE FRAGMENTS:\n%s\n\n" +
|
|
"TASK:\n" +
|
|
"1. Analyze why this target is blocked or throttled.\n" +
|
|
"2. Formulate 3 to 5 highly effective, syntactically valid DPI desync strategies for '%s'.\n" +
|
|
"3. Output MUST be strictly valid JSON matching this schema:\n" +
|
|
"{\n" +
|
|
' "analysis": "Brief 1-2 sentence diagnosis of the blocking pattern",\n' +
|
|
' "strategies": [\n' +
|
|
' {\n' +
|
|
' "id": "ai_strat_1",\n' +
|
|
' "name": "Human-readable descriptive strategy name",\n' +
|
|
' "args": "Exact command-line arguments string for the engine",\n' +
|
|
' "description": "Why this combination should bypass the block"\n' +
|
|
' }\n' +
|
|
' ]\n' +
|
|
"}\n\n" +
|
|
"RULES FOR STRATEGY ARGS:\n" +
|
|
"- For zapret2: use valid options like '--lua-desync=multisplit:pos=1,midsld:seqovl=1:fooling=badseq' or '--lua-desync=fake:ttl=4:fooling=badseq --lua-desync=multisplit:pos=1,midsld'. DO NOT include binary name.\n" +
|
|
"- For zapret: use valid options like '--dpi-desync=fake,split2 --dpi-desync-split-pos=1,midsld --dpi-desync-fooling=badseq --dpi-desync-ttl=4'. DO NOT include binary name.\n" +
|
|
"- For byedpi: use valid options like '-s 1 -d 1 --auto=t,r,s -o 1'. DO NOT include binary name.\n\n" +
|
|
"JSON OUTPUT:",
|
|
target, target_url, engine,
|
|
baseline.http_code, baseline.handshake_ms, baseline.ttfb_ms,
|
|
baseline.error != "" ? baseline.error : "none",
|
|
user_prompt != "" ? user_prompt : "None provided",
|
|
rag_docs,
|
|
engine
|
|
);
|
|
|
|
let raw_reply = query_llm(provider, api_key, ai_custom_url, prompt, model_override);
|
|
if (!raw_reply) {
|
|
print(sprintf("%J\n", {
|
|
success: false,
|
|
error: "Failed to receive response from AI provider. Check API key and network connectivity."
|
|
}));
|
|
return;
|
|
}
|
|
|
|
let parsed_json = parse_llm_json(raw_reply);
|
|
if (!parsed_json || !parsed_json.strategies || type(parsed_json.strategies) != "array" || length(parsed_json.strategies) == 0) {
|
|
print(sprintf("%J\n", {
|
|
success: false,
|
|
error: "AI returned non-JSON or invalid format",
|
|
raw_response: raw_reply
|
|
}));
|
|
return;
|
|
}
|
|
|
|
let valid_strategies = [];
|
|
for (let i = 0; i < length(parsed_json.strategies); i++) {
|
|
let st = parsed_json.strategies[i];
|
|
if (st && st.args && validate_strategy_args(engine, st.args)) {
|
|
push(valid_strategies, {
|
|
id: st.id || sprintf("ai_strat_%d", i + 1),
|
|
name: st.name || sprintf("AI Strategy %d", i + 1),
|
|
engine,
|
|
args: trim(st.args),
|
|
description: st.description || ""
|
|
});
|
|
}
|
|
}
|
|
|
|
if (length(valid_strategies) == 0) {
|
|
print(sprintf("%J\n", {
|
|
success: false,
|
|
error: "All AI strategies failed syntax validation for engine " + engine,
|
|
raw_strategies: parsed_json.strategies
|
|
}));
|
|
return;
|
|
}
|
|
|
|
let custom_file = STATE_DIR + "/fuzzer_ai_strategies.json";
|
|
ensure_state_dir();
|
|
common.write_json_file(custom_file, valid_strategies);
|
|
|
|
print(sprintf("%J\n", {
|
|
success: true,
|
|
engine,
|
|
target,
|
|
target_url,
|
|
analysis: parsed_json.analysis || "AI strategy synthesis complete",
|
|
strategies: valid_strategies,
|
|
custom_file
|
|
}));
|
|
}
|
|
|
|
function get_fuzzer_state() {
|
|
let state = common.read_json_file(STATE_FILE);
|
|
if (!state || type(state) != "object") {
|
|
return {
|
|
running: false,
|
|
job_id: null,
|
|
engine: "zapret2",
|
|
target: "youtube_suite",
|
|
progress_pct: 0,
|
|
current_index: 0,
|
|
total_strategies: 0,
|
|
current_strategy: null,
|
|
results: [],
|
|
best_strategy: null,
|
|
error: null,
|
|
started_at: 0,
|
|
finished_at: 0
|
|
};
|
|
}
|
|
return state;
|
|
}
|
|
|
|
function kill_pid_file(path) {
|
|
let pid_str = fs.readfile(path);
|
|
if (pid_str) {
|
|
let pid = trim(as_string(pid_str));
|
|
if (pid != "") {
|
|
system(sprintf("kill %s >/dev/null 2>&1 || kill -9 %s >/dev/null 2>&1", pid, pid));
|
|
}
|
|
try { fs.unlink(path); } catch (e) {}
|
|
}
|
|
}
|
|
|
|
function cleanup_temp_daemons() {
|
|
kill_pid_file(STATE_DIR + "/fuzzer_byedpi.pid");
|
|
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");
|
|
}
|
|
|
|
function parse_curl_output(output, result) {
|
|
result = result || {};
|
|
output = trim(as_string(output));
|
|
if (output == "") {
|
|
result.success = false;
|
|
result.http_code = 0;
|
|
result.handshake_ms = 0;
|
|
result.ttfb_ms = 0;
|
|
result.speed_kbps = 0;
|
|
result.score = 0;
|
|
result.error = "Probe timeout or connection refused";
|
|
return result;
|
|
}
|
|
|
|
let parts = split(output, "\t");
|
|
if (length(parts) < 4) {
|
|
result.success = false;
|
|
result.http_code = 0;
|
|
result.score = 0;
|
|
result.error = "Malformed probe metrics output";
|
|
return result;
|
|
}
|
|
|
|
let http_code = int(parts[0]);
|
|
let appconnect = double(parts[1]);
|
|
let starttransfer = double(parts[2]);
|
|
let speed_bytes = double(parts[3]);
|
|
|
|
result.http_code = http_code;
|
|
result.handshake_ms = int(appconnect * 1000.0);
|
|
result.ttfb_ms = int(starttransfer * 1000.0);
|
|
result.speed_kbps = int(speed_bytes / 1024.0);
|
|
|
|
if (http_code >= 200 && http_code < 400) {
|
|
result.success = true;
|
|
let base_score = 100;
|
|
let latency_score = max(0, 1000 - result.ttfb_ms);
|
|
let speed_score = int(result.speed_kbps / 10.0);
|
|
result.score = base_score + latency_score + speed_score;
|
|
result.error = "";
|
|
} else {
|
|
result.success = false;
|
|
result.score = 0;
|
|
result.error = sprintf("HTTP Status %d", http_code);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
function run_probe(engine, args_str, target_key, custom_url) {
|
|
cleanup_temp_daemons();
|
|
|
|
let urls_list = resolve_target_urls_list(target_key, custom_url);
|
|
let total_urls = length(urls_list);
|
|
|
|
let result = {
|
|
success: false,
|
|
http_code: 0,
|
|
handshake_ms: 0,
|
|
ttfb_ms: 0,
|
|
speed_kbps: 0,
|
|
score: 0,
|
|
error: "",
|
|
sub_probes: []
|
|
};
|
|
|
|
engine = lc(as_string(engine));
|
|
let is_udp = index(args_str, "--filter-udp") >= 0 || index(args_str, "--dpi-desync-any-protocol") >= 0 || target_key == "quic_http3";
|
|
|
|
if (engine == "byedpi") {
|
|
let bin = get_byedpi_bin();
|
|
if (!bin) {
|
|
result.error = "ByeDPI binary not found";
|
|
return result;
|
|
}
|
|
|
|
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);
|
|
system(common.background_command_with_pid(spawn_cmd, ">/dev/null", ">" + shell_quote(pid_path)));
|
|
system("sleep 0.15");
|
|
|
|
let passed_count = 0;
|
|
let sum_handshake = 0;
|
|
let sum_ttfb = 0;
|
|
let max_speed = 0;
|
|
let last_http = 0;
|
|
|
|
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 3 --max-time 4 %s 2>/dev/null",
|
|
BYEDPI_PORT,
|
|
shell_quote(target_item.url)
|
|
);
|
|
let pipe = fs.popen(curl_cmd, "r");
|
|
let output = pipe ? pipe.read("all") : "";
|
|
if (pipe) pipe.close();
|
|
|
|
let single_res = parse_curl_output(output, {});
|
|
single_res.target_name = target_item.name;
|
|
single_res.url = target_item.url;
|
|
push(result.sub_probes, single_res);
|
|
|
|
if (single_res.success) {
|
|
passed_count++;
|
|
sum_handshake += single_res.handshake_ms;
|
|
sum_ttfb += single_res.ttfb_ms;
|
|
if (single_res.speed_kbps > max_speed) max_speed = single_res.speed_kbps;
|
|
last_http = single_res.http_code;
|
|
} else if (last_http == 0) {
|
|
last_http = single_res.http_code;
|
|
}
|
|
}
|
|
|
|
cleanup_temp_daemons();
|
|
|
|
if (passed_count == total_urls) {
|
|
result.success = true;
|
|
result.http_code = last_http > 0 ? last_http : 200;
|
|
result.handshake_ms = int(sum_handshake / double(total_urls));
|
|
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);
|
|
} 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);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
if (engine == "zapret" || engine == "zapret2") {
|
|
let is_z2 = engine == "zapret2";
|
|
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");
|
|
|
|
if (!bin) {
|
|
result.error = (is_z2 ? "Zapret v2" : "Zapret v1") + " binary not found";
|
|
return result;
|
|
}
|
|
|
|
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 ";
|
|
}
|
|
|
|
let filter_prefix = "";
|
|
if (is_z2 && index(args_str, "--filter-tcp") < 0 && index(args_str, "--filter-l7") < 0) {
|
|
filter_prefix = "--filter-tcp=443 --filter-l7=tls --payload=tls_client_hello ";
|
|
}
|
|
if (is_z2 && is_udp && index(args_str, "--filter-udp") < 0) {
|
|
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);
|
|
system(common.background_command(spawn_cmd));
|
|
system("sleep 0.15");
|
|
|
|
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 passed_count = 0;
|
|
let sum_handshake = 0;
|
|
let sum_ttfb = 0;
|
|
let max_speed = 0;
|
|
let last_http = 0;
|
|
|
|
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 3 --max-time 4 %s 2>/dev/null",
|
|
shell_quote(target_item.url)
|
|
);
|
|
let pipe = fs.popen(curl_cmd, "r");
|
|
let output = pipe ? pipe.read("all") : "";
|
|
if (pipe) pipe.close();
|
|
|
|
let single_res = parse_curl_output(output, {});
|
|
single_res.target_name = target_item.name;
|
|
single_res.url = target_item.url;
|
|
push(result.sub_probes, single_res);
|
|
|
|
if (single_res.success) {
|
|
passed_count++;
|
|
sum_handshake += single_res.handshake_ms;
|
|
sum_ttfb += single_res.ttfb_ms;
|
|
if (single_res.speed_kbps > max_speed) max_speed = single_res.speed_kbps;
|
|
last_http = single_res.http_code;
|
|
} else if (last_http == 0) {
|
|
last_http = single_res.http_code;
|
|
}
|
|
}
|
|
|
|
cleanup_temp_daemons();
|
|
|
|
if (passed_count == total_urls) {
|
|
result.success = true;
|
|
result.http_code = last_http > 0 ? last_http : 200;
|
|
result.handshake_ms = int(sum_handshake / double(total_urls));
|
|
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);
|
|
} 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);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
result.error = "Unknown engine: " + engine;
|
|
return result;
|
|
}
|
|
|
|
function run_fuzzer_worker(engine, target, custom_url, rule_section, custom_file, mode) {
|
|
let target_url = resolve_target_url(target, custom_url);
|
|
let strategies = null;
|
|
if (custom_file && custom_file != "" && fs.stat(custom_file) != null) {
|
|
strategies = common.read_json_file(custom_file);
|
|
}
|
|
if (!strategies || type(strategies) != "array" || length(strategies) == 0) {
|
|
strategies = get_strategies_for_engine(engine, mode);
|
|
}
|
|
let total = length(strategies);
|
|
|
|
let state = {
|
|
running: true,
|
|
job_id: sprintf("fuzz_%d", clock()[0]),
|
|
engine,
|
|
target,
|
|
target_url,
|
|
mode: as_string(mode || "presets"),
|
|
rule_section: as_string(rule_section),
|
|
custom_file: as_string(custom_file || ""),
|
|
progress_pct: 0,
|
|
current_index: 0,
|
|
total_strategies: total,
|
|
current_strategy: null,
|
|
results: [],
|
|
best_strategy: null,
|
|
error: null,
|
|
started_at: clock()[0],
|
|
finished_at: 0
|
|
};
|
|
|
|
save_fuzzer_state(state);
|
|
|
|
try {
|
|
let highest_score = -1;
|
|
let best = null;
|
|
|
|
for (let i = 0; i < total; i++) {
|
|
let strat = strategies[i];
|
|
state.current_index = i + 1;
|
|
state.current_strategy = strat;
|
|
state.progress_pct = int(((i) / double(total)) * 100.0);
|
|
save_fuzzer_state(state);
|
|
|
|
let probe = run_probe(strat.engine || engine, strat.args, target, custom_url);
|
|
|
|
let item_result = {
|
|
id: strat.id || sprintf("strat_%d", i + 1),
|
|
name: strat.name || sprintf("Strategy %d", i + 1),
|
|
engine: strat.engine || engine,
|
|
args: strat.args,
|
|
description: strat.description || "",
|
|
rationale: strat.rationale || "",
|
|
success: probe.success,
|
|
http_code: probe.http_code,
|
|
handshake_ms: probe.handshake_ms,
|
|
ttfb_ms: probe.ttfb_ms,
|
|
speed_kbps: probe.speed_kbps,
|
|
score: probe.score,
|
|
error: probe.error,
|
|
sub_probes: probe.sub_probes || [],
|
|
badge: ""
|
|
};
|
|
|
|
if (item_result.score > highest_score && item_result.success) {
|
|
highest_score = item_result.score;
|
|
best = item_result;
|
|
}
|
|
|
|
push(state.results, item_result);
|
|
state.progress_pct = int(((i + 1) / double(total)) * 100.0);
|
|
save_fuzzer_state(state);
|
|
}
|
|
|
|
// Assign badges
|
|
if (best) {
|
|
best.badge = "🏆 Best Match";
|
|
state.best_strategy = best;
|
|
}
|
|
|
|
// Mark fastest and most stable
|
|
let min_ttfb = 999999;
|
|
let fastest = null;
|
|
for (let r in state.results) {
|
|
if (r.success && r.ttfb_ms > 0 && r.ttfb_ms < min_ttfb) {
|
|
min_ttfb = r.ttfb_ms;
|
|
fastest = r;
|
|
}
|
|
}
|
|
if (fastest && fastest.id != (best ? best.id : "")) {
|
|
fastest.badge = "⚡ Ultra Fast";
|
|
}
|
|
|
|
state.running = false;
|
|
state.current_strategy = null;
|
|
state.finished_at = clock()[0];
|
|
save_fuzzer_state(state);
|
|
} catch (err) {
|
|
state.running = false;
|
|
state.error = as_string(err);
|
|
state.finished_at = clock()[0];
|
|
save_fuzzer_state(state);
|
|
}
|
|
|
|
cleanup_temp_daemons();
|
|
}
|
|
|
|
function start_fuzzer(engine, target, custom_url, rule_section, custom_file, mode) {
|
|
let current = get_fuzzer_state();
|
|
if (current.running) {
|
|
print(sprintf("%J\n", { success: false, error: "Fuzzer is already running", job_id: current.job_id }));
|
|
return;
|
|
}
|
|
|
|
ensure_state_dir();
|
|
|
|
let job_id = sprintf("fuzz_%d", clock()[0]);
|
|
let cmd = sprintf(
|
|
"ucode -L /usr/lib/tachyon /usr/lib/tachyon/diagnostics/fuzzer.uc worker %s %s %s %s %s %s",
|
|
shell_quote(engine || "zapret2"),
|
|
shell_quote(target || "youtube_suite"),
|
|
shell_quote(custom_url || ""),
|
|
shell_quote(rule_section || ""),
|
|
shell_quote(custom_file || ""),
|
|
shell_quote(mode || "presets")
|
|
);
|
|
|
|
system(common.background_command_with_pid(cmd, ">/dev/null", ">" + shell_quote(PID_FILE)));
|
|
|
|
print(sprintf("%J\n", { success: true, job_id, engine: engine || "zapret2", target: target || "youtube_suite", mode: mode || "presets" }));
|
|
}
|
|
|
|
function stop_fuzzer() {
|
|
kill_pid_file(PID_FILE);
|
|
cleanup_temp_daemons();
|
|
|
|
let state = get_fuzzer_state();
|
|
state.running = false;
|
|
state.error = "Stopped by user";
|
|
state.finished_at = clock()[0];
|
|
save_fuzzer_state(state);
|
|
|
|
print(sprintf("%J\n", { success: true, message: "Fuzzer stopped" }));
|
|
}
|
|
|
|
function get_available_engines() {
|
|
return {
|
|
zapret2: get_zapret2_bin() != null,
|
|
zapret: get_zapret_bin() != null,
|
|
byedpi: get_byedpi_bin() != null
|
|
};
|
|
}
|
|
|
|
function apply_strategy(engine, args_val, target_rule) {
|
|
engine = lc(as_string(engine));
|
|
args_val = trim(as_string(args_val));
|
|
target_rule = trim(as_string(target_rule));
|
|
|
|
if (args_val == "") {
|
|
print(sprintf("%J\n", { success: false, error: "Empty strategy arguments" }));
|
|
return;
|
|
}
|
|
|
|
let uci = uci_core.cursor();
|
|
let applied = false;
|
|
|
|
if (target_rule != "" && target_rule != "global") {
|
|
uci.set(CONFIG_NAME, target_rule, "action", engine);
|
|
if (engine == "zapret2")
|
|
uci.set(CONFIG_NAME, target_rule, "nfqws2_opt", args_val);
|
|
else if (engine == "zapret")
|
|
uci.set(CONFIG_NAME, target_rule, "nfqws_opt", args_val);
|
|
else if (engine == "byedpi")
|
|
uci.set(CONFIG_NAME, target_rule, "byedpi_cmd_opts", args_val);
|
|
applied = true;
|
|
} else {
|
|
let provider_sec = engine;
|
|
let sec_obj = uci.get_all(CONFIG_NAME, provider_sec);
|
|
if (sec_obj == null) {
|
|
uci.set(CONFIG_NAME, provider_sec, "provider");
|
|
}
|
|
uci.set(CONFIG_NAME, provider_sec, "enabled", "1");
|
|
if (engine == "zapret2")
|
|
uci.set(CONFIG_NAME, provider_sec, "nfqws2_opt", args_val);
|
|
else if (engine == "zapret")
|
|
uci.set(CONFIG_NAME, provider_sec, "nfqws_opt", args_val);
|
|
else if (engine == "byedpi")
|
|
uci.set(CONFIG_NAME, provider_sec, "byedpi_cmd_opts", args_val);
|
|
applied = true;
|
|
}
|
|
|
|
uci.commit(CONFIG_NAME);
|
|
system(common.background_command("tachyon reload"));
|
|
|
|
print(sprintf("%J\n", {
|
|
success: true,
|
|
engine,
|
|
applied_to: target_rule != "" ? target_rule : "global",
|
|
args: args_val
|
|
}));
|
|
}
|
|
|
|
// CLI Dispatcher
|
|
let op = ARGV[0] || "status";
|
|
|
|
if (op == "start") {
|
|
start_fuzzer(ARGV[1], ARGV[2], ARGV[3], ARGV[4], ARGV[5], ARGV[6]);
|
|
} else if (op == "worker") {
|
|
run_fuzzer_worker(ARGV[1], ARGV[2], ARGV[3], ARGV[4], ARGV[5], ARGV[6]);
|
|
} else if (op == "status") {
|
|
print(sprintf("%J\n", get_fuzzer_state()));
|
|
} else if (op == "stop") {
|
|
stop_fuzzer();
|
|
} else if (op == "apply") {
|
|
apply_strategy(ARGV[1], ARGV[2], ARGV[3]);
|
|
} else if (op == "get_patterns" || op == "patterns") {
|
|
print(sprintf("%J\n", { success: true, patterns: get_patterns_config() }));
|
|
} else if (op == "save_patterns") {
|
|
let cfg = safe_json_parse(ARGV[1]);
|
|
save_patterns_config(cfg);
|
|
} else if (op == "reset_patterns") {
|
|
reset_patterns_config();
|
|
} else if (op == "ai_synthesize" || op == "synthesize") {
|
|
synthesize_ai_strategies(ARGV[1], ARGV[2], ARGV[3], ARGV[4]);
|
|
} else if (op == "generate" || op == "strategies_generate") {
|
|
print(sprintf("%J\n", get_strategies_for_engine(ARGV[1], ARGV[2] || "combinatorial")));
|
|
} else if (op == "strategies") {
|
|
let strat_mode = ARGV[1] || "presets";
|
|
print(sprintf("%J\n", {
|
|
available_engines: get_available_engines(),
|
|
target_suites: TARGET_SUITES,
|
|
patterns: get_patterns_config(),
|
|
zapret2: get_strategies_for_engine("zapret2", strat_mode),
|
|
zapret: get_strategies_for_engine("zapret", strat_mode),
|
|
byedpi: get_strategies_for_engine("byedpi", strat_mode)
|
|
}));
|
|
} else {
|
|
warn("Usage: fuzzer.uc [start|status|stop|apply|strategies|generate|get_patterns|save_patterns|reset_patterns|ai_synthesize|worker] ...\n");
|
|
exit(1);
|
|
}
|
|
|