diff --git a/fe-app-tachyon/src/tachyon/tabs/diagnostic/partials/renderStrategyFuzzerModal.ts b/fe-app-tachyon/src/tachyon/tabs/diagnostic/partials/renderStrategyFuzzerModal.ts index 1867ee35..ad045d6a 100644 --- a/fe-app-tachyon/src/tachyon/tabs/diagnostic/partials/renderStrategyFuzzerModal.ts +++ b/fe-app-tachyon/src/tachyon/tabs/diagnostic/partials/renderStrategyFuzzerModal.ts @@ -1309,6 +1309,53 @@ export function renderStrategyFuzzerModal(ruleNames: string[] = []) { ), ]; + if (item.data_verified) { + statusChildren.push( + E( + 'span', + { + class: 'label badge-success', + style: + 'font-size: 9px; padding: 2px 5px; border-radius: 3px; margin-left: 4px; display: inline-block; background: #28a745; color: #fff; font-weight: 500;', + title: _( + 'Data transfer verified: streamed >= 32KB without throttling or disconnect', + ), + }, + '✓ 32KB Прокачано', + ), + ); + } else if (item.dpi_verdict === 'throttled_16k') { + statusChildren.push( + E( + 'span', + { + class: 'label badge-danger', + style: + 'font-size: 9px; padding: 2px 5px; border-radius: 3px; margin-left: 4px; display: inline-block; background: #dc3545; color: #fff; font-weight: 500;', + title: _( + 'DPI throttling: stream cut off after ~16KB payload transfer', + ), + }, + '✗ Задушено на 16KB', + ), + ); + } else if (item.dpi_verdict === 'server_fakes') { + statusChildren.push( + E( + 'span', + { + class: 'label badge-warning', + style: + 'font-size: 9px; padding: 2px 5px; border-radius: 3px; margin-left: 4px; display: inline-block; background: #ffc107; color: #111; font-weight: 500;', + title: _( + 'Fake packets reached remote server (HTTP 400 Bad Request)', + ), + }, + '⚠️ Сервер получил фейки', + ), + ); + } + if (item.sub_probes && item.sub_probes.length > 1) { const passedSub = item.sub_probes.filter((p) => p.success).length; const totalSub = item.sub_probes.length; @@ -1376,7 +1423,18 @@ export function renderStrategyFuzzerModal(ruleNames: string[] = []) { E( 'td', { style: 'padding: 8px 10px;' }, - item.success ? `${(item.speed_kbps / 1024).toFixed(1)}MB/s` : '—', + item.success + ? [ + E('div', {}, `${(item.speed_kbps / 1024).toFixed(1)}MB/s`), + item.data_bytes && item.data_bytes > 0 + ? E( + 'div', + { style: 'font-size: 10px; opacity: 0.65;' }, + `${(item.data_bytes / 1024).toFixed(1)} KB`, + ) + : '', + ] + : '—', ), E( 'td', @@ -1488,6 +1546,11 @@ export function renderStrategyFuzzerModal(ruleNames: string[] = []) { updateProgressUI(res.data); + if (res.data.dpi_detection) { + currentDpiDetection = res.data.dpi_detection; + updateDpiBanner(res.data.dpi_detection); + } + // Only re-render results when new results arrive or benchmark finishes const resultCount = res.data.results?.length || 0; const finishedAt = res.data.finished_at || 0; @@ -1568,6 +1631,11 @@ export function renderStrategyFuzzerModal(ruleNames: string[] = []) { border: '#17a2b8', icon: '🔵', }, + ip_block: { + bg: 'rgba(220, 53, 69, 0.18)', + border: '#dc3545', + icon: '🚫', + }, unknown: { bg: 'rgba(108, 117, 125, 0.12)', border: '#6c757d', @@ -1584,6 +1652,7 @@ export function renderStrategyFuzzerModal(ruleNames: string[] = []) { rst: 'TCP Reset Injection', throttle: 'Throttling / Deep Inspection', dns_block: 'DNS Blocking', + ip_block: _('Блокировка по IP (Таймаут TCP SYN)'), unknown: 'Unknown DPI Pattern', none: 'No Blocking Detected', }; @@ -1591,6 +1660,13 @@ export function renderStrategyFuzzerModal(ruleNames: string[] = []) { const colors = typeColors[detection.type] || typeColors.unknown; const label = typeLabels[detection.type] || detection.type; + const ipBlockNotice = + detection.type === 'ip_block' + ? `
+ ⚠️ ${_('Обнаружена блокировка по IP! Ресурс блокируется на сетевом уровне (нет ответа на TCP SYN). Методы обхода DPI (Zapret / ByeDPI) бессильны для этого адреса — настройте маршрутизацию через прокси/VPN (Sing-box) для данного домена!')} +
` + : ''; + banner.style.display = 'block'; banner.style.background = colors.bg; banner.style.borderLeft = `3px solid ${colors.border}`; @@ -1603,6 +1679,7 @@ export function renderStrategyFuzzerModal(ruleNames: string[] = []) { ${detection.recommended_engines.length > 0 ? `
${_('Recommended')}: ${detection.recommended_engines.join(', ')}
` : ''}
${detection.details}
+ ${ipBlockNotice} `; }; diff --git a/fe-app-tachyon/src/tachyon/types.ts b/fe-app-tachyon/src/tachyon/types.ts index 701b9d7e..0482bddb 100644 --- a/fe-app-tachyon/src/tachyon/types.ts +++ b/fe-app-tachyon/src/tachyon/types.ts @@ -918,6 +918,9 @@ export namespace Tachyon { handshake_ms: number; ttfb_ms: number; speed_kbps: number; + data_verified?: boolean; + data_bytes?: number; + dpi_verdict?: string; success: boolean; error?: string; } @@ -934,6 +937,9 @@ export namespace Tachyon { handshake_ms: number; ttfb_ms: number; speed_kbps: number; + data_verified?: boolean; + data_bytes?: number; + dpi_verdict?: string; score: number; error?: string; badge?: string; @@ -944,6 +950,9 @@ export namespace Tachyon { handshake_ms: number; ttfb_ms: number; speed_kbps: number; + data_verified?: boolean; + data_bytes?: number; + dpi_verdict?: string; score: number; success: boolean; error?: string; @@ -1047,7 +1056,7 @@ export namespace Tachyon { } export interface FuzzerDpiDetection { - type: 'rst' | 'throttle' | 'dns_block' | 'unknown' | 'none'; + type: 'rst' | 'throttle' | 'dns_block' | 'ip_block' | 'unknown' | 'none'; confidence: number; details: string; recommended_engines: string[]; @@ -1056,6 +1065,7 @@ export namespace Tachyon { handshake_ms: number; ttfb_ms: number; speed_kbps: number; + data_bytes?: number; error: string; }; } diff --git a/luci-app-tachyon/htdocs/luci-static/resources/view/tachyon/main.js b/luci-app-tachyon/htdocs/luci-static/resources/view/tachyon/main.js index db022b84..d916f6b9 100644 --- a/luci-app-tachyon/htdocs/luci-static/resources/view/tachyon/main.js +++ b/luci-app-tachyon/htdocs/luci-static/resources/view/tachyon/main.js @@ -13422,6 +13422,49 @@ function renderStrategyFuzzerModal(ruleNames = []) { item.success ? `HTTP ${item.http_code}` : "DROP" ) ]; + if (item.data_verified) { + statusChildren.push( + E( + "span", + { + class: "label badge-success", + style: "font-size: 9px; padding: 2px 5px; border-radius: 3px; margin-left: 4px; display: inline-block; background: #28a745; color: #fff; font-weight: 500;", + title: _( + "Data transfer verified: streamed >= 32KB without throttling or disconnect" + ) + }, + "\u2713 32KB \u041F\u0440\u043E\u043A\u0430\u0447\u0430\u043D\u043E" + ) + ); + } else if (item.dpi_verdict === "throttled_16k") { + statusChildren.push( + E( + "span", + { + class: "label badge-danger", + style: "font-size: 9px; padding: 2px 5px; border-radius: 3px; margin-left: 4px; display: inline-block; background: #dc3545; color: #fff; font-weight: 500;", + title: _( + "DPI throttling: stream cut off after ~16KB payload transfer" + ) + }, + "\u2717 \u0417\u0430\u0434\u0443\u0448\u0435\u043D\u043E \u043D\u0430 16KB" + ) + ); + } else if (item.dpi_verdict === "server_fakes") { + statusChildren.push( + E( + "span", + { + class: "label badge-warning", + style: "font-size: 9px; padding: 2px 5px; border-radius: 3px; margin-left: 4px; display: inline-block; background: #ffc107; color: #111; font-weight: 500;", + title: _( + "Fake packets reached remote server (HTTP 400 Bad Request)" + ) + }, + "\u26A0\uFE0F \u0421\u0435\u0440\u0432\u0435\u0440 \u043F\u043E\u043B\u0443\u0447\u0438\u043B \u0444\u0435\u0439\u043A\u0438" + ) + ); + } if (item.sub_probes && item.sub_probes.length > 1) { const passedSub = item.sub_probes.filter((p) => p.success).length; const totalSub = item.sub_probes.length; @@ -13482,7 +13525,14 @@ function renderStrategyFuzzerModal(ruleNames = []) { E( "td", { style: "padding: 8px 10px;" }, - item.success ? `${(item.speed_kbps / 1024).toFixed(1)}MB/s` : "\u2014" + item.success ? [ + E("div", {}, `${(item.speed_kbps / 1024).toFixed(1)}MB/s`), + item.data_bytes && item.data_bytes > 0 ? E( + "div", + { style: "font-size: 10px; opacity: 0.65;" }, + `${(item.data_bytes / 1024).toFixed(1)} KB` + ) : "" + ] : "\u2014" ), E( "td", @@ -13577,6 +13627,10 @@ function renderStrategyFuzzerModal(ruleNames = []) { startBtn.disabled = false; } updateProgressUI(res.data); + if (res.data.dpi_detection) { + currentDpiDetection = res.data.dpi_detection; + updateDpiBanner(res.data.dpi_detection); + } const resultCount = res.data.results?.length || 0; const finishedAt = res.data.finished_at || 0; if (resultCount !== lastRenderedCount || finishedAt !== lastRenderedFinishedAt) { @@ -13642,6 +13696,11 @@ function renderStrategyFuzzerModal(ruleNames = []) { border: "#17a2b8", icon: "\u{1F535}" }, + ip_block: { + bg: "rgba(220, 53, 69, 0.18)", + border: "#dc3545", + icon: "\u{1F6AB}" + }, unknown: { bg: "rgba(108, 117, 125, 0.12)", border: "#6c757d", @@ -13657,11 +13716,15 @@ function renderStrategyFuzzerModal(ruleNames = []) { rst: "TCP Reset Injection", throttle: "Throttling / Deep Inspection", dns_block: "DNS Blocking", + ip_block: _("\u0411\u043B\u043E\u043A\u0438\u0440\u043E\u0432\u043A\u0430 \u043F\u043E IP (\u0422\u0430\u0439\u043C\u0430\u0443\u0442 TCP SYN)"), unknown: "Unknown DPI Pattern", none: "No Blocking Detected" }; const colors = typeColors[detection.type] || typeColors.unknown; const label = typeLabels[detection.type] || detection.type; + const ipBlockNotice = detection.type === "ip_block" ? `
+ \u26A0\uFE0F ${_("\u041E\u0431\u043D\u0430\u0440\u0443\u0436\u0435\u043D\u0430 \u0431\u043B\u043E\u043A\u0438\u0440\u043E\u0432\u043A\u0430 \u043F\u043E IP! \u0420\u0435\u0441\u0443\u0440\u0441 \u0431\u043B\u043E\u043A\u0438\u0440\u0443\u0435\u0442\u0441\u044F \u043D\u0430 \u0441\u0435\u0442\u0435\u0432\u043E\u043C \u0443\u0440\u043E\u0432\u043D\u0435 (\u043D\u0435\u0442 \u043E\u0442\u0432\u0435\u0442\u0430 \u043D\u0430 TCP SYN). \u041C\u0435\u0442\u043E\u0434\u044B \u043E\u0431\u0445\u043E\u0434\u0430 DPI (Zapret / ByeDPI) \u0431\u0435\u0441\u0441\u0438\u043B\u044C\u043D\u044B \u0434\u043B\u044F \u044D\u0442\u043E\u0433\u043E \u0430\u0434\u0440\u0435\u0441\u0430 \u2014 \u043D\u0430\u0441\u0442\u0440\u043E\u0439\u0442\u0435 \u043C\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0446\u0438\u044E \u0447\u0435\u0440\u0435\u0437 \u043F\u0440\u043E\u043A\u0441\u0438/VPN (Sing-box) \u0434\u043B\u044F \u0434\u0430\u043D\u043D\u043E\u0433\u043E \u0434\u043E\u043C\u0435\u043D\u0430!")} +
` : ""; banner.style.display = "block"; banner.style.background = colors.bg; banner.style.borderLeft = `3px solid ${colors.border}`; @@ -13674,6 +13737,7 @@ function renderStrategyFuzzerModal(ruleNames = []) { ${detection.recommended_engines.length > 0 ? `
${_("Recommended")}: ${detection.recommended_engines.join(", ")}
` : ""}
${detection.details}
+ ${ipBlockNotice} `; }; const handleToggleRun = async () => { diff --git a/tachyon/files/usr/lib/components/updates.uc b/tachyon/files/usr/lib/components/updates.uc index 815e0884..cfbd535f 100644 --- a/tachyon/files/usr/lib/components/updates.uc +++ b/tachyon/files/usr/lib/components/updates.uc @@ -2975,6 +2975,7 @@ function list_update() { if (ok) { write_list_update_timestamp(now_seconds()); log_message("Lists update completed successfully", "info"); + reload_singbox_after_list_update(); } else { log_message("Lists update failed", "info"); @@ -3092,6 +3093,34 @@ function write_current_reload_state_clean() { ]); } +function reload_singbox_after_list_update() { + let sing_box_pid = trim(module_output([ LIB_DIR + "/service/state.uc", "sing-box-service-runtime-pid" ])); + if (sing_box_pid == "" || int(sing_box_pid) <= 0) + return true; + let sing_box_config_path = option(uci_settings(), "config_path", "") || "/etc/sing-box/config.json"; + let sing_box_config_hash_before = file_md5(sing_box_config_path); + if (!singbox_runtime_success([ "init-config", "0", "1", "1" ])) + return false; + let sing_box_config_hash_after = file_md5(sing_box_config_path); + if (sing_box_config_hash_before != sing_box_config_hash_after) { + log_message("Rulesets updated on disk; reloading sing-box with new configuration", "info"); + module_success([ DNS_FAILOVER_UC, "stop-runtime" ]); + module_success([ PRIORITY_UC, "stop-runtime" ]); + let ok = service_state_success([ + "reload-sing-box-runtime", + sing_box_pid, + sing_box_config_hash_before, + sing_box_config_hash_after, + "1" + ]); + module_success([ PRIORITY_UC, "start-runtime" ]); + module_success([ DNS_FAILOVER_UC, "start-runtime" ]); + write_current_reload_state_clean(); + return ok; + } + return true; +} + function mark_pending_reload(reason) { service_state_success([ "mark-pending-reload", PENDING_RELOAD_FILE, reason ]); } diff --git a/tachyon/files/usr/lib/diagnostics/fuzzer.uc b/tachyon/files/usr/lib/diagnostics/fuzzer.uc index cdb9a136..e29c99e4 100644 --- a/tachyon/files/usr/lib/diagnostics/fuzzer.uc +++ b/tachyon/files/usr/lib/diagnostics/fuzzer.uc @@ -583,6 +583,134 @@ const STRATEGIES_ZAPRET2 = [ engine: "zapret2", args: "--lua-desync=oob:pos=1", description: "TCP Out-Of-Band URG flag packet to desynchronize DPI reassembly." + }, + + // ── 8. BLOCKCHECKW & BLOCKCHECK2 COMBAT STRATEGIES ───────────────────────── + { + id: "z2_bc_multisplit_7point", + name: "7-Point MultiSplit (ClientHello Full Spectrum)", + engine: "zapret2", + args: "--payload=tls_client_hello --lua-desync=multisplit:pos=1,sniext+1,host+1,midsld-2,midsld,midsld+2,endhost-1", + description: "Splits at start, SNI extension, host header, mid-SLD, and end of host. Top blockcheck2 bypass." + }, + { + id: "z2_bc_multidisorder_7point", + name: "7-Point MultiDisorder (ClientHello Full Spectrum)", + engine: "zapret2", + args: "--payload=tls_client_hello --lua-desync=multidisorder:pos=1,sniext+1,host+1,midsld-2,midsld,midsld+2,endhost-1", + description: "Disorders all critical TLS ClientHello headers. Devastates stateful DPI reassembly." + }, + { + id: "z2_bc_multisplit_1220", + name: "Tri-Point MultiSplit (pos=1,midsld,1220)", + engine: "zapret2", + args: "--payload=tls_client_hello --lua-desync=multisplit:pos=1,midsld,1220", + description: "Splits at start, mid-SLD, and packet boundary (1220 B)." + }, + { + id: "z2_bc_multidisorder_1220", + name: "Tri-Point MultiDisorder (pos=1,midsld,1220)", + engine: "zapret2", + args: "--payload=tls_client_hello --lua-desync=multidisorder:pos=1,midsld,1220", + description: "Disorders stream at start, mid-SLD, and MTU boundary (1220 B)." + }, + { + id: "z2_bc_tcpseg_rep260", + name: "TCP Segment Desync (repeats=260, pos=0,1)", + engine: "zapret2", + args: "--payload=tls_client_hello --lua-desync=tcpseg:pos=0,1:ip_id=rnd:repeats=260", + description: "Forces randomized IP ID TCP segmentation burst with 260 repeats to overflow DPI state table." + }, + { + id: "z2_bc_tcpseg_rep100_midsld", + name: "TCP Segment Desync (repeats=100, midsld)", + engine: "zapret2", + args: "--payload=tls_client_hello --lua-desync=tcpseg:pos=0,midsld:ip_id=rnd:repeats=100", + description: "Mid-SLD TCP segmentation with 100 repeats." + }, + { + id: "z2_bc_oob_midsld", + name: "OOB Desync (urp=midsld)", + engine: "zapret2", + args: "--in-range=-s1 --lua-desync=oob:urp=midsld", + description: "TCP Out-Of-Band packet with urgent pointer pointing directly to mid-domain." + }, + { + id: "z2_bc_oob_b", + name: "OOB Desync (urp=b)", + engine: "zapret2", + args: "--in-range=-s1 --lua-desync=oob:urp=b", + description: "TCP Out-Of-Band with beginning urgent pointer offset." + }, + { + id: "z2_bc_seqovl_sniext", + name: "Exact SeqOvl Pattern Overlap (sniext+1, Max.ru)", + engine: "zapret2", + args: "--payload=tls_client_hello --lua-desync=multidisorder:pos=sniext+1:seqovl=sniext:seqovl_pattern=tls_max", + description: "Disorders at SNI extension with exact sequence overlap pattern." + }, + { + id: "z2_bc_seqovl_midsld", + name: "Exact SeqOvl Pattern Overlap (midsld, Google)", + engine: "zapret2", + args: "--payload=tls_client_hello --lua-desync=multidisorder:pos=midsld:seqovl=midsld-1:seqovl_pattern=tls_google", + description: "Disorders at mid-SLD with exact sequence overlap pattern from Google ClientHello." + }, + { + id: "z2_bc_lua_padencap", + name: "Dynamic Lua TLS Mod (padencap + dupsid) + MultiSplit", + engine: "zapret2", + args: "--payload=tls_client_hello --lua-desync=luaexec:code=desync.patmod=tls_mod(fake_default_tls,'rnd,dupsid,padencap',desync.reasm_data) --lua-desync=multisplit:pos=10,sniext+1:seqovl=#patmod:seqovl_pattern=patmod", + description: "Dynamically pads and encapsulates ClientHello payload via Lua runtime." + }, + { + id: "z2_bc_tcp_ack_offset", + name: "TCP ACK Offset Spoofing (-66000) + TS_UP", + engine: "zapret2", + args: "--payload=tls_client_hello --lua-desync=fake:blob=tls_max:tcp_ack=-66000:tcp_ts_up:repeats=6", + description: "Injects fake packets with corrupted ACK number and ascending TCP timestamps." + }, + { + id: "z2_bc_tcp_flags_unset_ack", + name: "TCP Flags Manipulation (Unset ACK)", + engine: "zapret2", + args: "--payload=tls_client_hello --lua-desync=fake:blob=tls_google:tcp_flags_unset=ACK:repeats=6", + description: "Fake packets with ACK flag cleared, accepted only by DPI state trackers." + }, + { + id: "z2_bc_tcp_flags_set_syn", + name: "TCP Flags Manipulation (Set SYN on Data)", + engine: "zapret2", + args: "--payload=tls_client_hello --lua-desync=fake:blob=tls_max:tcp_flags_set=SYN:repeats=6", + description: "Sets SYN flag on ClientHello fake packets to trigger DPI state desynchronization." + }, + { + id: "z2_bc_badsum", + name: "BadSum Checksum Invalidation", + engine: "zapret2", + args: "--payload=tls_client_hello --lua-desync=fake:blob=tls_google:badsum:repeats=6", + description: "Packets with invalid L4 checksum are dropped by remote server NIC but inspected by DPI." + }, + { + id: "z2_bc_autottl_1", + name: "Auto-TTL Adaptive Probe (autottl=-1, 3-20)", + engine: "zapret2", + args: "--payload=tls_client_hello --lua-desync=fake:blob=tls_max:ip_autottl=-1,3-20:repeats=6", + description: "Dynamically calculates hop distance to target and injects fake packets right before DPI hop." + }, + { + id: "z2_bc_autottl_2", + name: "Auto-TTL Adaptive Probe (autottl=-2, 3-20)", + engine: "zapret2", + args: "--payload=tls_client_hello --lua-desync=fake:blob=tls_google:ip_autottl=-2,3-20:repeats=6", + description: "Auto-TTL probe with 2 hops before target." + }, + { + id: "z2_bc_pktmod", + name: "PktMod Packet Mutation (ip_ttl=1)", + engine: "zapret2", + args: "--payload=tls_client_hello --lua-desync=fake:blob=tls_max:ip_ttl=1:repeats=6 --payload=empty --out-range=s1= 5 ? int(parts[4]) : 0; + let exit_code = length(parts) >= 6 ? int(parts[5]) : 0; 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); + result.data_bytes = size_download; + + // Check for HTTP 400 (Server Receives Fakes - desync corruption) + if (http_code == 400) { + result.success = false; + result.score = 0; + result.data_verified = false; + result.dpi_verdict = "server_fakes"; + result.error = "HTTP 400 (Remote server rejected corrupted/fake packet payload)"; + return result; + } + + // Check for 16KB DPI Throttling (TSPU stream drop / connection reset after 10-28KB) + if (exit_code != 0 && size_download >= 10240 && size_download <= 28672) { + result.success = false; + result.score = 0; + result.data_verified = false; + result.dpi_verdict = "throttled_16k"; + result.error = sprintf("16KB DPI Data Throttle (stream killed after %d B, curl exit %d)", size_download, exit_code); + return result; + } + + if (exit_code != 0 && http_code == 0) { + result.success = false; + result.score = 0; + result.data_verified = false; + result.dpi_verdict = "dropped"; + result.error = sprintf("Connection dropped by DPI (curl exit %d)", exit_code); + return result; + } + + if (exit_code != 0 && http_code >= 200 && http_code < 400) { + result.success = false; + result.score = 0; + result.data_verified = false; + result.dpi_verdict = "transfer_failed"; + result.error = sprintf("Data transfer aborted after %d B (curl exit %d)", size_download, exit_code); + return result; + } // Any valid HTTP response from origin (including 401/403/404/405 when hitting endpoints without auth headers) - // proves that TCP handshake, TLS ClientHello, and HTTP negotiation successfully reached the remote server past TSPU. if ((http_code >= 200 && http_code < 400) || http_code == 401 || http_code == 403 || http_code == 404 || http_code == 405) { result.success = true; let base_score = (http_code >= 200 && http_code < 400) ? 100 : 85; 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; + let data_bonus = size_download >= 32768 ? 50 : (size_download >= 1024 ? 20 : 0); + result.score = base_score + latency_score + speed_score + data_bonus; result.error = ""; + result.data_verified = size_download >= 32768; + result.dpi_verdict = size_download >= 32768 ? "verified_32k" : "available"; } else { result.success = false; result.score = 0; + result.data_verified = false; + result.dpi_verdict = "failed"; result.error = http_code > 0 ? sprintf("HTTP Status %d", http_code) : "Connection dropped by DPI"; } @@ -1521,7 +1860,7 @@ function parse_curl_output(output, result) { // ── DPI Type Detection ────────────────────────────────────────────────────── // Probes the target without any bypass to determine how it's being blocked. -// Returns: { type: "rst"|"throttle"|"dns_block"|"unknown"|"none", +// Returns: { type: "rst"|"throttle"|"dns_block"|"ip_block"|"unknown"|"none", // confidence: 0-100, details: string, recommended_engines: string[] } function detect_dpi_type(target_key, custom_url) { let urls_list = resolve_target_urls_list(target_key, custom_url); @@ -1542,7 +1881,7 @@ function detect_dpi_type(target_key, custom_url) { 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 %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", + "curl %s-so /dev/null -w '%%{http_code}\\t%%{time_appconnect}\\t%%{time_starttransfer}\\t%%{speed_download}\\t%%{size_download}' -L --connect-timeout 4 --max-time 6 %s 2>&1; printf '\\t%%d\\n' $?", dns_flags, shell_quote(target_url) ); @@ -1560,6 +1899,8 @@ function detect_dpi_type(target_key, custom_url) { handshake_ms: metrics.handshake_ms || 0, ttfb_ms: metrics.ttfb_ms || 0, speed_kbps: metrics.speed_kbps || 0, + data_bytes: metrics.data_bytes || 0, + dpi_verdict: metrics.dpi_verdict || "unknown", error: metrics.error || "" }; @@ -1589,6 +1930,16 @@ function detect_dpi_type(target_key, custom_url) { result.confidence = 90; result.details = sprintf("DNS resolution failed for %s — likely DNS-level blocking or hijacking", domain); result.recommended_engines = ["byedpi", "zapret2"]; + } else if (metrics.dpi_verdict == "throttled_16k" || (http_code == 200 && metrics.data_bytes >= 10240 && metrics.data_bytes <= 28672)) { + result.type = "throttle"; + result.confidence = 95; + result.details = sprintf("16KB DPI throttling detected on %s — handshake succeeded but stream dropped at ~16KB data transfer", domain); + result.recommended_engines = ["byedpi", "zapret2"]; + } else if ((http_code == 0 && handshake == 0) && (index(error_str, "timed out") >= 0 || index(error_str, "Connection timed out") >= 0 || index(error_str, "ETIMEDOUT") >= 0)) { + result.type = "ip_block"; + result.confidence = 95; + result.details = sprintf("TCP connect timed out before TLS handshake for %s — host is blocked at the IP layer. DPI bypass cannot unblock this; route via Sing-box VPN/Proxy outbound instead.", domain); + result.recommended_engines = []; } else if (index(error_str, "Connection reset") >= 0 || index(error_str, "ECONNRESET") >= 0) { result.type = "rst"; result.confidence = 85; @@ -1798,11 +2149,14 @@ function run_probe(engine, args_str, target_key, custom_url) { let sum_handshake = 0; let sum_ttfb = 0; let max_speed = 0; + let sum_data_bytes = 0; + let all_data_verified = true; let last_http = 0; + let last_dpi_verdict = "available"; 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 4 --max-time 6 %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}\\t%%{size_download}' -L --connect-timeout 4 --max-time 6 %s 2>/dev/null; printf '\\t%%d\\n' $?", BYEDPI_PORT, shell_quote(target_item.url) ); @@ -1819,11 +2173,16 @@ function run_probe(engine, args_str, target_key, custom_url) { passed_count++; sum_handshake += single_res.handshake_ms; sum_ttfb += single_res.ttfb_ms; + sum_data_bytes += single_res.data_bytes || 0; + if (!single_res.data_verified) all_data_verified = false; if (single_res.speed_kbps > max_speed) max_speed = single_res.speed_kbps; last_http = single_res.http_code; + last_dpi_verdict = single_res.dpi_verdict || "available"; } else { + all_data_verified = false; if (last_http == 0) last_http = single_res.http_code; if (single_res.error && result.error == "") result.error = single_res.error; + last_dpi_verdict = single_res.dpi_verdict || "failed"; break; } } @@ -1836,11 +2195,17 @@ function run_probe(engine, args_str, target_key, custom_url) { 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); + result.data_bytes = int(sum_data_bytes / double(total_urls)); + result.data_verified = all_data_verified; + result.dpi_verdict = all_data_verified ? "verified_32k" : last_dpi_verdict; + result.score = 100 + max(0, 1000 - result.ttfb_ms) + int(result.speed_kbps / 10.0) + (result.data_verified ? 50 : 20); result.error = ""; } else { result.success = false; result.http_code = last_http; + result.data_bytes = sum_data_bytes; + result.data_verified = false; + result.dpi_verdict = last_dpi_verdict; result.score = 0; if (result.error == "") { result.error = sprintf("Failed %d of %d endpoints", total_urls - passed_count, total_urls); @@ -1919,12 +2284,15 @@ function run_probe(engine, args_str, target_key, custom_url) { let sum_handshake = 0; let sum_ttfb = 0; let max_speed = 0; + let sum_data_bytes = 0; + let all_data_verified = true; let last_http = 0; + let last_dpi_verdict = "available"; let dns_flags = get_fuzzer_curl_dns_flags(); for (let target_item in urls_list) { let curl_cmd = sprintf( - "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", + "curl %s-so /dev/null -w '%%{http_code}\\t%%{time_appconnect}\\t%%{time_starttransfer}\\t%%{speed_download}\\t%%{size_download}' -L --connect-timeout 4 --max-time 6 %s 2>/dev/null; printf '\\t%%d\\n' $?", dns_flags, shell_quote(target_item.url) ); @@ -1941,11 +2309,16 @@ function run_probe(engine, args_str, target_key, custom_url) { passed_count++; sum_handshake += single_res.handshake_ms; sum_ttfb += single_res.ttfb_ms; + sum_data_bytes += single_res.data_bytes || 0; + if (!single_res.data_verified) all_data_verified = false; if (single_res.speed_kbps > max_speed) max_speed = single_res.speed_kbps; last_http = single_res.http_code; + last_dpi_verdict = single_res.dpi_verdict || "available"; } else { + all_data_verified = false; if (last_http == 0) last_http = single_res.http_code; if (single_res.error && result.error == "") result.error = single_res.error; + last_dpi_verdict = single_res.dpi_verdict || "failed"; break; } } @@ -1958,11 +2331,17 @@ function run_probe(engine, args_str, target_key, custom_url) { 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); + result.data_bytes = int(sum_data_bytes / double(total_urls)); + result.data_verified = all_data_verified; + result.dpi_verdict = all_data_verified ? "verified_32k" : last_dpi_verdict; + result.score = 100 + max(0, 1000 - result.ttfb_ms) + int(result.speed_kbps / 10.0) + (result.data_verified ? 50 : 20); result.error = ""; } else { result.success = false; result.http_code = last_http; + result.data_bytes = sum_data_bytes; + result.data_verified = false; + result.dpi_verdict = last_dpi_verdict; result.score = 0; if (result.error == "") { result.error = sprintf("Failed %d of %d endpoints", total_urls - passed_count, total_urls); @@ -2050,6 +2429,9 @@ function run_fuzzer_worker(engine, target, custom_url, rule_section, custom_file handshake_ms: probe.handshake_ms, ttfb_ms: probe.ttfb_ms, speed_kbps: probe.speed_kbps, + data_bytes: probe.data_bytes || 0, + data_verified: probe.data_verified || false, + dpi_verdict: probe.dpi_verdict || "unknown", score: probe.score, error: probe.error, sub_probes: probe.sub_probes || [], diff --git a/tachyon/files/usr/lib/nft/apply.uc b/tachyon/files/usr/lib/nft/apply.uc index 3b124700..49fe3044 100644 --- a/tachyon/files/usr/lib/nft/apply.uc +++ b/tachyon/files/usr/lib/nft/apply.uc @@ -2273,12 +2273,22 @@ function nft_populate_runtime_set_for_section(section, deferred_sections, table, "/tmp/sing-box/rulesets/community-subnets-" + service + ".lst", "/etc/tachyon/rulesets/community-subnets-" + service + ".lst" ]; + let found = false; for (let path in cached_paths) { if (helpers.file_is_usable(path, 50)) { nft_add_file_chunks_to_family_sets(path, table, sets.subnets, sets.subnets6, "ips", "", "5000"); + found = true; break; } } + if (!found) { + if (service == "discord") { + nft_add_set_elements(table, sets.subnets, "162.158.0.0/15, 172.64.0.0/13, 138.128.136.0/21"); + } + else if (service == "telegram") { + nft_add_set_elements(table, sets.subnets, "149.154.160.0/20, 91.108.4.0/22, 91.108.8.0/22, 91.108.12.0/22, 91.108.16.0/22, 91.108.56.0/22, 5.28.192.0/18, 91.105.192.0/23, 95.161.64.0/20, 185.76.151.0/24, 194.221.0.0/16"); + } + } } } diff --git a/tachyon/files/usr/lib/singbox/generator_routes.uc b/tachyon/files/usr/lib/singbox/generator_routes.uc index 9fddf03c..571f7df5 100644 --- a/tachyon/files/usr/lib/singbox/generator_routes.uc +++ b/tachyon/files/usr/lib/singbox/generator_routes.uc @@ -1213,10 +1213,11 @@ function add_protocol_matchers(rule, section) { function load_community_subnet_cidrs(community) { + let service = as_string(community); let paths = [ - "/tmp/sing-box/rulesets/community-subnets-" + as_string(community) + ".lst", - "/etc/tachyon/rulesets/community-subnets-" + as_string(community) + ".lst", - (ctx.runtime_ruleset_folder || runtime_ruleset_folder || "/tmp/sing-box/rulesets") + "/community-subnets-" + as_string(community) + ".lst" + "/tmp/sing-box/rulesets/community-subnets-" + service + ".lst", + "/etc/tachyon/rulesets/community-subnets-" + service + ".lst", + (ctx.runtime_ruleset_folder || runtime_ruleset_folder || "/tmp/sing-box/rulesets") + "/community-subnets-" + service + ".lst" ]; for (let path in paths) { let content = fs.readfile(path); @@ -1231,6 +1232,18 @@ function load_community_subnet_cidrs(community) { return cidrs; } } + // Hardcoded seed fallback for cold boot before list-update downloads .lst + if (service == "discord") { + return [ "162.158.0.0/15", "172.64.0.0/13", "138.128.136.0/21" ]; + } + if (service == "telegram") { + return [ + "149.154.160.0/20", "91.108.4.0/22", "91.108.8.0/22", + "91.108.12.0/22", "91.108.16.0/22", "91.108.56.0/22", + "5.28.192.0/18", "91.105.192.0/23", "95.161.64.0/20", + "185.76.151.0/24", "194.221.0.0/16" + ]; + } return []; } @@ -1252,9 +1265,14 @@ function add_combined_route_for_section(config, section) { for (let community in connections.community_lists(section)) { - let ensured = ensure_community_ruleset(config, section_name, as_string(community)); + let service = as_string(community); + let ensured = ensure_community_ruleset(config, section_name, service); push(rule_set_tags, ensured.tag); push(dns_rule_set_tags, ensured.tag); + if (service != "cloudflare" && service != "cloudfront" && service != "hetzner" && service != "ovh" && service != "digitalocean") { + for (let cidr in load_community_subnet_cidrs(community)) + push(ip_cidr, cidr); + } } for (let reference in connections.rule_sets(section)) { let ensured = ensure_custom_ruleset(config, as_string(reference));