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));