feat(fuzzer): DPI-детектор, авто-применение, история результатов
- DPI-детектор: перед бенчмарком определяет тип блокировки (RST/throttle/DNS) и переранжирует стратегии по приоритету (badseq для RST, multisplit для throttle, fake+TTL для DNS-блока) - Авто-применение: галочка «Auto-apply best strategy» автоматически применяет выигравшую стратегию после завершения бенчмарка - История: результаты бенчмарков сохраняются в /etc/tachyon/fuzzer_history.json (до 50 записей) — новая вкладка «History» в UI - CLI: новые команды detect_dpi, auto_apply, history, clear_history - Локализация: русские и английские строки для всех новых элементов UI - Frontend: вкладка History, баннер DPI-детекции, чекбокс авто-применения
This commit is contained in:
@@ -7614,3 +7614,75 @@ msgstr ""
|
|||||||
#: src/tachyon/tabs/diagnostic/checks/runZapret2Check.ts:107
|
#: src/tachyon/tabs/diagnostic/checks/runZapret2Check.ts:107
|
||||||
msgid "Zapret2 sing-box outbound is not configured"
|
msgid "Zapret2 sing-box outbound is not configured"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/tachyon/tabs/diagnostic/partials/renderStrategyFuzzerModal.ts
|
||||||
|
msgid "DPI Detected"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/tachyon/tabs/diagnostic/partials/renderStrategyFuzzerModal.ts
|
||||||
|
msgid "confidence"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/tachyon/tabs/diagnostic/partials/renderStrategyFuzzerModal.ts
|
||||||
|
msgid "Recommended"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/tachyon/tabs/diagnostic/partials/renderStrategyFuzzerModal.ts
|
||||||
|
msgid "Auto-apply best strategy"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/tachyon/tabs/diagnostic/partials/renderStrategyFuzzerModal.ts
|
||||||
|
msgid "Benchmark History"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/tachyon/tabs/diagnostic/partials/renderStrategyFuzzerModal.ts
|
||||||
|
msgid "Refresh"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/tachyon/tabs/diagnostic/partials/renderStrategyFuzzerModal.ts
|
||||||
|
msgid "Clear History"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/tachyon/tabs/diagnostic/partials/renderStrategyFuzzerModal.ts
|
||||||
|
msgid "No benchmark history yet. Run a benchmark to see results here."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/tachyon/tabs/diagnostic/partials/renderStrategyFuzzerModal.ts
|
||||||
|
msgid "Failed to load history"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/tachyon/tabs/diagnostic/partials/renderStrategyFuzzerModal.ts
|
||||||
|
msgid "Date"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/tachyon/tabs/diagnostic/partials/renderStrategyFuzzerModal.ts
|
||||||
|
msgid "DPI Type"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/tachyon/tabs/diagnostic/partials/renderStrategyFuzzerModal.ts
|
||||||
|
msgid "Best Strategy"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/tachyon/tabs/diagnostic/partials/renderStrategyFuzzerModal.ts
|
||||||
|
msgid "Working"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/tachyon/tabs/diagnostic/partials/renderStrategyFuzzerModal.ts
|
||||||
|
msgid "Auto-applied best strategy"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/tachyon/tabs/diagnostic/partials/renderStrategyFuzzerModal.ts
|
||||||
|
msgid "Auto-apply failed"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/tachyon/tabs/diagnostic/partials/renderStrategyFuzzerModal.ts
|
||||||
|
msgid "Failed to detect DPI type"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/tachyon/tabs/diagnostic/partials/renderStrategyFuzzerModal.ts
|
||||||
|
msgid "Failed to auto-apply strategy"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/tachyon/tabs/diagnostic/partials/renderStrategyFuzzerModal.ts
|
||||||
|
msgid "History cleared"
|
||||||
|
msgstr ""
|
||||||
|
|||||||
@@ -5092,3 +5092,57 @@ msgstr "Восстановление прямого интернета..."
|
|||||||
|
|
||||||
msgid "Find optimal DPI bypass strategies with isolated live probing"
|
msgid "Find optimal DPI bypass strategies with isolated live probing"
|
||||||
msgstr "Автоматический подбор оптимальных параметров обхода DPI в изолированном сэндбоксе"
|
msgstr "Автоматический подбор оптимальных параметров обхода DPI в изолированном сэндбоксе"
|
||||||
|
|
||||||
|
msgid "DPI Detected"
|
||||||
|
msgstr "DPI обнаружен"
|
||||||
|
|
||||||
|
msgid "confidence"
|
||||||
|
msgstr "уверенность"
|
||||||
|
|
||||||
|
msgid "Recommended"
|
||||||
|
msgstr "Рекомендовано"
|
||||||
|
|
||||||
|
msgid "Auto-apply best strategy"
|
||||||
|
msgstr "Авто-применение лучшей стратегии"
|
||||||
|
|
||||||
|
msgid "Benchmark History"
|
||||||
|
msgstr "История бенчмарков"
|
||||||
|
|
||||||
|
msgid "Refresh"
|
||||||
|
msgstr "Обновить"
|
||||||
|
|
||||||
|
msgid "Clear History"
|
||||||
|
msgstr "Очистить историю"
|
||||||
|
|
||||||
|
msgid "No benchmark history yet. Run a benchmark to see results here."
|
||||||
|
msgstr "История бенчмарков пуста. Запустите бенчмарк, чтобы увидеть результаты здесь."
|
||||||
|
|
||||||
|
msgid "Failed to load history"
|
||||||
|
msgstr "Не удалось загрузить историю"
|
||||||
|
|
||||||
|
msgid "Date"
|
||||||
|
msgstr "Дата"
|
||||||
|
|
||||||
|
msgid "DPI Type"
|
||||||
|
msgstr "Тип DPI"
|
||||||
|
|
||||||
|
msgid "Best Strategy"
|
||||||
|
msgstr "Лучшая стратегия"
|
||||||
|
|
||||||
|
msgid "Working"
|
||||||
|
msgstr "Рабочих"
|
||||||
|
|
||||||
|
msgid "Auto-applied best strategy"
|
||||||
|
msgstr "Лучшая стратегия автоматически применена"
|
||||||
|
|
||||||
|
msgid "Auto-apply failed"
|
||||||
|
msgstr "Авто-применение не удалось"
|
||||||
|
|
||||||
|
msgid "Failed to detect DPI type"
|
||||||
|
msgstr "Не удалось определить тип DPI"
|
||||||
|
|
||||||
|
msgid "Failed to auto-apply strategy"
|
||||||
|
msgstr "Не удалось автоматически применить стратегию"
|
||||||
|
|
||||||
|
msgid "History cleared"
|
||||||
|
msgstr "История очищена"
|
||||||
|
|||||||
@@ -1352,4 +1352,106 @@ export const TachyonShellMethods = {
|
|||||||
_('Failed to reset fuzzer patterns'),
|
_('Failed to reset fuzzer patterns'),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
detectFuzzerDpi: async (target: string, customUrl?: string) => {
|
||||||
|
const response = await executeShellCommand({
|
||||||
|
command: '/usr/bin/tachyon',
|
||||||
|
args: [
|
||||||
|
Tachyon.AvailableMethods.FUZZER_DETECT_DPI,
|
||||||
|
target,
|
||||||
|
...(customUrl ? [customUrl] : []),
|
||||||
|
],
|
||||||
|
timeout: COMPONENT_ACTION_RPC_TIMEOUT_MS,
|
||||||
|
});
|
||||||
|
const parsed = parseJsonObjectOutput<Tachyon.FuzzerDetectDpiResponse>(
|
||||||
|
response.stdout,
|
||||||
|
);
|
||||||
|
|
||||||
|
if ((response.code ?? 0) !== 0 || !parsed) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: response.stderr || _('Failed to detect DPI type'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: parsed,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
autoApplyFuzzerStrategy: async (targetRule?: string) => {
|
||||||
|
const response = await executeShellCommand({
|
||||||
|
command: '/usr/bin/tachyon',
|
||||||
|
args: [
|
||||||
|
Tachyon.AvailableMethods.FUZZER_AUTO_APPLY,
|
||||||
|
...(targetRule ? [targetRule] : []),
|
||||||
|
],
|
||||||
|
timeout: UI_ACTION_RPC_TIMEOUT_MS,
|
||||||
|
});
|
||||||
|
const parsed = parseJsonObjectOutput<Tachyon.FuzzerAutoApplyResponse>(
|
||||||
|
response.stdout,
|
||||||
|
);
|
||||||
|
|
||||||
|
if ((response.code ?? 0) !== 0 || !parsed?.success) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error:
|
||||||
|
parsed?.error ||
|
||||||
|
response.stderr ||
|
||||||
|
_('Failed to auto-apply strategy'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: parsed,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
getFuzzerHistory: async (limit?: number) => {
|
||||||
|
const response = await executeShellCommand({
|
||||||
|
command: '/usr/bin/tachyon',
|
||||||
|
args: [
|
||||||
|
Tachyon.AvailableMethods.FUZZER_HISTORY,
|
||||||
|
String(limit || 20),
|
||||||
|
],
|
||||||
|
timeout: COMPONENT_ACTION_RPC_TIMEOUT_MS,
|
||||||
|
});
|
||||||
|
const parsed = parseJsonObjectOutput<Tachyon.FuzzerHistoryResult>(
|
||||||
|
response.stdout,
|
||||||
|
);
|
||||||
|
|
||||||
|
if ((response.code ?? 0) !== 0 || !parsed) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: response.stderr || _('Failed to get fuzzer history'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: parsed,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
clearFuzzerHistory: async () => {
|
||||||
|
const response = await executeShellCommand({
|
||||||
|
command: '/usr/bin/tachyon',
|
||||||
|
args: [Tachyon.AvailableMethods.FUZZER_CLEAR_HISTORY],
|
||||||
|
timeout: COMPONENT_ACTION_RPC_TIMEOUT_MS,
|
||||||
|
});
|
||||||
|
const parsed = parseJsonObjectOutput<{
|
||||||
|
success: boolean;
|
||||||
|
error?: string;
|
||||||
|
}>(response.stdout);
|
||||||
|
|
||||||
|
if ((response.code ?? 0) !== 0 || !parsed?.success) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: response.stderr || _('Failed to clear fuzzer history'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: { message: _('History cleared') },
|
||||||
|
};
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ export function renderStrategyFuzzerModal(ruleNames: string[] = []) {
|
|||||||
let isPolling = false;
|
let isPolling = false;
|
||||||
|
|
||||||
// Modal State
|
// Modal State
|
||||||
let activeTab: 'benchmark' | 'patterns' | 'custom' | 'ai' = 'benchmark';
|
let activeTab: 'benchmark' | 'patterns' | 'custom' | 'ai' | 'history' =
|
||||||
|
'benchmark';
|
||||||
let selectedEngine: Tachyon.FuzzerEngine = 'zapret2';
|
let selectedEngine: Tachyon.FuzzerEngine = 'zapret2';
|
||||||
let selectedTarget: Tachyon.FuzzerTarget = 'youtube_suite';
|
let selectedTarget: Tachyon.FuzzerTarget = 'youtube_suite';
|
||||||
let customUrl = '';
|
let customUrl = '';
|
||||||
@@ -17,6 +18,8 @@ export function renderStrategyFuzzerModal(ruleNames: string[] = []) {
|
|||||||
let isRunning = false;
|
let isRunning = false;
|
||||||
let currentState: Tachyon.FuzzerState | null = null;
|
let currentState: Tachyon.FuzzerState | null = null;
|
||||||
let resultFilter: 'all' | 'success' | 'fast' = 'all';
|
let resultFilter: 'all' | 'success' | 'fast' = 'all';
|
||||||
|
let autoApplyEnabled = false;
|
||||||
|
let currentDpiDetection: Tachyon.FuzzerDpiDetection | null = null;
|
||||||
|
|
||||||
// Patterns Config State
|
// Patterns Config State
|
||||||
let patternsConfig: Tachyon.FuzzerPatternsConfig = {
|
let patternsConfig: Tachyon.FuzzerPatternsConfig = {
|
||||||
@@ -56,7 +59,7 @@ export function renderStrategyFuzzerModal(ruleNames: string[] = []) {
|
|||||||
const tabButtons: Record<string, HTMLElement> = {};
|
const tabButtons: Record<string, HTMLElement> = {};
|
||||||
|
|
||||||
const createTabButton = (
|
const createTabButton = (
|
||||||
id: 'benchmark' | 'patterns' | 'custom' | 'ai',
|
id: 'benchmark' | 'patterns' | 'custom' | 'ai' | 'history',
|
||||||
icon: string,
|
icon: string,
|
||||||
label: string,
|
label: string,
|
||||||
) => {
|
) => {
|
||||||
@@ -89,6 +92,7 @@ export function renderStrategyFuzzerModal(ruleNames: string[] = []) {
|
|||||||
createTabButton('patterns', '🛠️', _('Pattern Builder')),
|
createTabButton('patterns', '🛠️', _('Pattern Builder')),
|
||||||
createTabButton('custom', '➕', _('My Strategies')),
|
createTabButton('custom', '➕', _('My Strategies')),
|
||||||
createTabButton('ai', '🧠', _('AI Doctor RAG')),
|
createTabButton('ai', '🧠', _('AI Doctor RAG')),
|
||||||
|
createTabButton('history', '📋', _('History')),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -104,6 +108,9 @@ export function renderStrategyFuzzerModal(ruleNames: string[] = []) {
|
|||||||
const tabContentAi = E('div', {
|
const tabContentAi = E('div', {
|
||||||
style: 'display: none; flex-direction: column; gap: 14px;',
|
style: 'display: none; flex-direction: column; gap: 14px;',
|
||||||
});
|
});
|
||||||
|
const tabContentHistory = E('div', {
|
||||||
|
style: 'display: none; flex-direction: column; gap: 14px;',
|
||||||
|
});
|
||||||
|
|
||||||
const updateTabVisibility = () => {
|
const updateTabVisibility = () => {
|
||||||
for (const key in tabButtons) {
|
for (const key in tabButtons) {
|
||||||
@@ -116,9 +123,12 @@ export function renderStrategyFuzzerModal(ruleNames: string[] = []) {
|
|||||||
activeTab === 'patterns' ? 'flex' : 'none';
|
activeTab === 'patterns' ? 'flex' : 'none';
|
||||||
tabContentCustom.style.display = activeTab === 'custom' ? 'flex' : 'none';
|
tabContentCustom.style.display = activeTab === 'custom' ? 'flex' : 'none';
|
||||||
tabContentAi.style.display = activeTab === 'ai' ? 'flex' : 'none';
|
tabContentAi.style.display = activeTab === 'ai' ? 'flex' : 'none';
|
||||||
|
tabContentHistory.style.display =
|
||||||
|
activeTab === 'history' ? 'flex' : 'none';
|
||||||
|
|
||||||
if (activeTab === 'patterns') renderPatternsTab();
|
if (activeTab === 'patterns') renderPatternsTab();
|
||||||
if (activeTab === 'custom') renderCustomTab();
|
if (activeTab === 'custom') renderCustomTab();
|
||||||
|
if (activeTab === 'history') renderHistoryTab();
|
||||||
};
|
};
|
||||||
|
|
||||||
// ═════════════════════════════════════════════════════════════════════════════
|
// ═════════════════════════════════════════════════════════════════════════════
|
||||||
@@ -283,9 +293,43 @@ export function renderStrategyFuzzerModal(ruleNames: string[] = []) {
|
|||||||
ruleSelect,
|
ruleSelect,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
controlsGrid.append(engineGroup, targetGroup, modeGroup, ruleGroup);
|
controlsGrid.append(engineGroup, targetGroup, modeGroup, ruleGroup);
|
||||||
|
|
||||||
|
// Auto-Apply Toggle
|
||||||
|
const autoApplyCheckbox = E('input', {
|
||||||
|
type: 'checkbox',
|
||||||
|
id: 'tachyon-fuzzer-auto-apply',
|
||||||
|
style: 'margin: 0;',
|
||||||
|
});
|
||||||
|
autoApplyCheckbox.addEventListener('change', () => {
|
||||||
|
autoApplyEnabled = (autoApplyCheckbox as HTMLInputElement).checked;
|
||||||
|
});
|
||||||
|
|
||||||
|
const autoApplyGroup = E(
|
||||||
|
'div',
|
||||||
|
{
|
||||||
|
style:
|
||||||
|
'display: flex; align-items: end; padding: 4px 0; grid-column: 1 / -1;',
|
||||||
|
},
|
||||||
|
[
|
||||||
|
E(
|
||||||
|
'div',
|
||||||
|
{
|
||||||
|
style: 'font-size: 11px; cursor: pointer; display: flex; align-items: center; gap: 4px;',
|
||||||
|
},
|
||||||
|
[autoApplyCheckbox, E('span', {}, _('Auto-apply best strategy'))],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
controlsGrid.append(autoApplyGroup);
|
||||||
|
|
||||||
|
// DPI Detection Banner
|
||||||
|
const dpiDetectionBanner = E('div', {
|
||||||
|
id: 'tachyon-fuzzer-dpi-banner',
|
||||||
|
style:
|
||||||
|
'display: none; padding: 10px 14px; border-radius: 6px; font-size: 12px; line-height: 1.4;',
|
||||||
|
});
|
||||||
|
|
||||||
// Progress Bar
|
// Progress Bar
|
||||||
const progressContainer = E(
|
const progressContainer = E(
|
||||||
'div',
|
'div',
|
||||||
@@ -505,6 +549,7 @@ export function renderStrategyFuzzerModal(ruleNames: string[] = []) {
|
|||||||
|
|
||||||
tabContentBenchmark.append(
|
tabContentBenchmark.append(
|
||||||
controlsGrid,
|
controlsGrid,
|
||||||
|
dpiDetectionBanner,
|
||||||
progressContainer,
|
progressContainer,
|
||||||
filterToolbar,
|
filterToolbar,
|
||||||
resultsContainer,
|
resultsContainer,
|
||||||
@@ -1044,6 +1089,148 @@ export function renderStrategyFuzzerModal(ruleNames: string[] = []) {
|
|||||||
|
|
||||||
tabContentAi.append(aiSynthesizerCard);
|
tabContentAi.append(aiSynthesizerCard);
|
||||||
|
|
||||||
|
// ═════════════════════════════════════════════════════════════════════════════
|
||||||
|
// TAB 5: HISTORY
|
||||||
|
// ═════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
const renderHistoryTab = async () => {
|
||||||
|
tabContentHistory.replaceChildren();
|
||||||
|
|
||||||
|
const historyHeader = E(
|
||||||
|
'div',
|
||||||
|
{
|
||||||
|
style:
|
||||||
|
'display: flex; justify-content: space-between; align-items: center;',
|
||||||
|
},
|
||||||
|
[
|
||||||
|
E(
|
||||||
|
'div',
|
||||||
|
{ style: 'font-size: 13px; font-weight: bold;' },
|
||||||
|
'📋 ' + _('Benchmark History'),
|
||||||
|
),
|
||||||
|
E('div', { style: 'display: flex; gap: 8px;' }, [
|
||||||
|
renderButton({
|
||||||
|
text: _('🔄 Refresh'),
|
||||||
|
classNames: ['cbi-button-neutral'],
|
||||||
|
onClick: () => renderHistoryTab(),
|
||||||
|
}),
|
||||||
|
renderButton({
|
||||||
|
text: _('🗑️ Clear History'),
|
||||||
|
classNames: ['cbi-button-reset'],
|
||||||
|
onClick: async () => {
|
||||||
|
await TachyonShellMethods.clearFuzzerHistory();
|
||||||
|
showToast(_('History cleared'), 'success');
|
||||||
|
renderHistoryTab();
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
const historyContainer = E('div', {
|
||||||
|
style:
|
||||||
|
'border: 1px solid var(--border-color, rgba(255,255,255,0.1)); border-radius: 6px; overflow: hidden;',
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await TachyonShellMethods.getFuzzerHistory(20);
|
||||||
|
const entries = res.data?.entries || [];
|
||||||
|
|
||||||
|
if (entries.length === 0) {
|
||||||
|
historyContainer.appendChild(
|
||||||
|
E(
|
||||||
|
'div',
|
||||||
|
{
|
||||||
|
style:
|
||||||
|
'padding: 24px; text-align: center; opacity: 0.6; font-size: 12px;',
|
||||||
|
},
|
||||||
|
_('No benchmark history yet. Run a benchmark to see results here.'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const table = E(
|
||||||
|
'table',
|
||||||
|
{
|
||||||
|
class: 'table',
|
||||||
|
style: 'width: 100%; margin: 0; font-size: 12px;',
|
||||||
|
},
|
||||||
|
[
|
||||||
|
E('thead', {}, [
|
||||||
|
E('tr', {}, [
|
||||||
|
E('th', { style: 'padding: 8px;' }, _('Date')),
|
||||||
|
E('th', { style: 'padding: 8px;' }, _('Engine')),
|
||||||
|
E('th', { style: 'padding: 8px;' }, _('Target')),
|
||||||
|
E('th', { style: 'padding: 8px;' }, _('DPI Type')),
|
||||||
|
E('th', { style: 'padding: 8px;' }, _('Best Strategy')),
|
||||||
|
E('th', { style: 'padding: 8px; width: 60px;' }, _('Score')),
|
||||||
|
E('th', { style: 'padding: 8px; width: 70px;' }, _('Working')),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
E(
|
||||||
|
'tbody',
|
||||||
|
{},
|
||||||
|
entries.reverse().map((entry) => {
|
||||||
|
const date = new Date(entry.timestamp * 1000);
|
||||||
|
const dateStr = `${date.toLocaleDateString()} ${date.toLocaleTimeString()}`;
|
||||||
|
const dpiType = entry.dpi_detection
|
||||||
|
? `${entry.dpi_detection.type} (${entry.dpi_detection.confidence}%)`
|
||||||
|
: '—';
|
||||||
|
const bestName = entry.best_strategy
|
||||||
|
? entry.best_strategy.name
|
||||||
|
: '—';
|
||||||
|
const bestScore = entry.best_strategy
|
||||||
|
? String(entry.best_strategy.score)
|
||||||
|
: '0';
|
||||||
|
const working = `${entry.working_count}/${entry.total_tested}`;
|
||||||
|
|
||||||
|
return E('tr', {}, [
|
||||||
|
E('td', { style: 'padding: 8px;' }, dateStr),
|
||||||
|
E(
|
||||||
|
'td',
|
||||||
|
{
|
||||||
|
style: 'padding: 8px; font-family: monospace;',
|
||||||
|
},
|
||||||
|
entry.engine,
|
||||||
|
),
|
||||||
|
E('td', { style: 'padding: 8px;' }, entry.target),
|
||||||
|
E('td', { style: 'padding: 8px;' }, dpiType),
|
||||||
|
E(
|
||||||
|
'td',
|
||||||
|
{
|
||||||
|
style:
|
||||||
|
'padding: 8px; font-size: 11px; max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;',
|
||||||
|
},
|
||||||
|
bestName,
|
||||||
|
),
|
||||||
|
E(
|
||||||
|
'td',
|
||||||
|
{ style: 'padding: 8px; font-weight: bold;' },
|
||||||
|
bestScore,
|
||||||
|
),
|
||||||
|
E('td', { style: 'padding: 8px;' }, working),
|
||||||
|
]);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
historyContainer.appendChild(table);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
historyContainer.appendChild(
|
||||||
|
E(
|
||||||
|
'div',
|
||||||
|
{
|
||||||
|
style:
|
||||||
|
'padding: 24px; text-align: center; opacity: 0.6; font-size: 12px;',
|
||||||
|
},
|
||||||
|
_('Failed to load history'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
tabContentHistory.append(historyHeader, historyContainer);
|
||||||
|
};
|
||||||
|
|
||||||
// ── Results Rendering ─────────────────────────────────────────────────────
|
// ── Results Rendering ─────────────────────────────────────────────────────
|
||||||
const renderResults = (state: Tachyon.FuzzerState) => {
|
const renderResults = (state: Tachyon.FuzzerState) => {
|
||||||
const tbody = document.getElementById('tachyon-fuzzer-results-tbody');
|
const tbody = document.getElementById('tachyon-fuzzer-results-tbody');
|
||||||
@@ -1228,6 +1415,29 @@ export function renderStrategyFuzzerModal(ruleNames: string[] = []) {
|
|||||||
stopPolling();
|
stopPolling();
|
||||||
startBtn.innerText = _('🚀 Start Benchmark');
|
startBtn.innerText = _('🚀 Start Benchmark');
|
||||||
(startBtn as HTMLButtonElement).disabled = false;
|
(startBtn as HTMLButtonElement).disabled = false;
|
||||||
|
|
||||||
|
// Auto-apply best strategy if enabled
|
||||||
|
if (autoApplyEnabled && currentState?.best_strategy) {
|
||||||
|
try {
|
||||||
|
const applyRes =
|
||||||
|
await TachyonShellMethods.autoApplyFuzzerStrategy(
|
||||||
|
selectedRuleSection || undefined,
|
||||||
|
);
|
||||||
|
if (applyRes.success) {
|
||||||
|
showToast(
|
||||||
|
`${_('✅ Auto-applied best strategy')}: ${currentState.best_strategy.name}`,
|
||||||
|
'success',
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
showToast(
|
||||||
|
`${_('Auto-apply failed')}: ${applyRes.error || _('Unknown error')}`,
|
||||||
|
'error',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
showToast(_('Auto-apply failed'), 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
@@ -1250,6 +1460,64 @@ export function renderStrategyFuzzerModal(ruleNames: string[] = []) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const updateDpiBanner = (detection: Tachyon.FuzzerDpiDetection | null) => {
|
||||||
|
const banner = document.getElementById('tachyon-fuzzer-dpi-banner');
|
||||||
|
if (!banner) return;
|
||||||
|
if (!detection) {
|
||||||
|
banner.style.display = 'none';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const typeColors: Record<string, { bg: string; border: string; icon: string }> = {
|
||||||
|
rst: { bg: 'rgba(220, 53, 69, 0.12)', border: '#dc3545', icon: '🔴' },
|
||||||
|
throttle: {
|
||||||
|
bg: 'rgba(255, 193, 7, 0.12)',
|
||||||
|
border: '#ffc107',
|
||||||
|
icon: '🟡',
|
||||||
|
},
|
||||||
|
dns_block: {
|
||||||
|
bg: 'rgba(23, 162, 184, 0.12)',
|
||||||
|
border: '#17a2b8',
|
||||||
|
icon: '🔵',
|
||||||
|
},
|
||||||
|
unknown: {
|
||||||
|
bg: 'rgba(108, 117, 125, 0.12)',
|
||||||
|
border: '#6c757d',
|
||||||
|
icon: '⚪',
|
||||||
|
},
|
||||||
|
none: {
|
||||||
|
bg: 'rgba(40, 167, 69, 0.12)',
|
||||||
|
border: '#28a745',
|
||||||
|
icon: '🟢',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const typeLabels: Record<string, string> = {
|
||||||
|
rst: 'TCP Reset Injection',
|
||||||
|
throttle: 'Throttling / Deep Inspection',
|
||||||
|
dns_block: 'DNS Blocking',
|
||||||
|
unknown: 'Unknown DPI Pattern',
|
||||||
|
none: 'No Blocking Detected',
|
||||||
|
};
|
||||||
|
|
||||||
|
const colors = typeColors[detection.type] || typeColors.unknown;
|
||||||
|
const label = typeLabels[detection.type] || detection.type;
|
||||||
|
|
||||||
|
banner.style.display = 'block';
|
||||||
|
banner.style.background = colors.bg;
|
||||||
|
banner.style.borderLeft = `3px solid ${colors.border}`;
|
||||||
|
banner.innerHTML = `
|
||||||
|
<div style="display: flex; justify-content: space-between; align-items: start;">
|
||||||
|
<div>
|
||||||
|
<span style="font-weight: bold;">${colors.icon} ${_('DPI Detected')}: ${label}</span>
|
||||||
|
<span style="opacity: 0.7; margin-left: 8px;">${detection.confidence}% ${_('confidence')}</span>
|
||||||
|
</div>
|
||||||
|
${detection.recommended_engines.length > 0 ? `<div style="font-size: 11px; opacity: 0.7;">${_('Recommended')}: ${detection.recommended_engines.join(', ')}</div>` : ''}
|
||||||
|
</div>
|
||||||
|
<div style="margin-top: 4px; opacity: 0.8; font-size: 11px;">${detection.details}</div>
|
||||||
|
`;
|
||||||
|
};
|
||||||
|
|
||||||
const handleToggleRun = async () => {
|
const handleToggleRun = async () => {
|
||||||
if (isRunning) {
|
if (isRunning) {
|
||||||
await TachyonShellMethods.stopFuzzer();
|
await TachyonShellMethods.stopFuzzer();
|
||||||
@@ -1279,6 +1547,20 @@ export function renderStrategyFuzzerModal(ruleNames: string[] = []) {
|
|||||||
'Preparing strategies and isolated nftables queue...',
|
'Preparing strategies and isolated nftables queue...',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Run DPI detection before starting benchmark
|
||||||
|
try {
|
||||||
|
const dpiRes = await TachyonShellMethods.detectFuzzerDpi(
|
||||||
|
selectedTarget,
|
||||||
|
selectedTarget === 'custom' ? customUrl : undefined,
|
||||||
|
);
|
||||||
|
if (dpiRes.success && dpiRes.data) {
|
||||||
|
currentDpiDetection = dpiRes.data;
|
||||||
|
updateDpiBanner(dpiRes.data);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// DPI detection is non-critical, continue with benchmark
|
||||||
|
}
|
||||||
|
|
||||||
const tbody = document.getElementById('tachyon-fuzzer-results-tbody');
|
const tbody = document.getElementById('tachyon-fuzzer-results-tbody');
|
||||||
if (tbody) {
|
if (tbody) {
|
||||||
tbody.replaceChildren(
|
tbody.replaceChildren(
|
||||||
@@ -1453,6 +1735,7 @@ export function renderStrategyFuzzerModal(ruleNames: string[] = []) {
|
|||||||
tabContentPatterns,
|
tabContentPatterns,
|
||||||
tabContentCustom,
|
tabContentCustom,
|
||||||
tabContentAi,
|
tabContentAi,
|
||||||
|
tabContentHistory,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Check initial state
|
// Check initial state
|
||||||
|
|||||||
@@ -107,6 +107,10 @@ export namespace Tachyon {
|
|||||||
FUZZER_GET_PATTERNS = 'fuzzer_get_patterns',
|
FUZZER_GET_PATTERNS = 'fuzzer_get_patterns',
|
||||||
FUZZER_SAVE_PATTERNS = 'fuzzer_save_patterns',
|
FUZZER_SAVE_PATTERNS = 'fuzzer_save_patterns',
|
||||||
FUZZER_RESET_PATTERNS = 'fuzzer_reset_patterns',
|
FUZZER_RESET_PATTERNS = 'fuzzer_reset_patterns',
|
||||||
|
FUZZER_DETECT_DPI = 'fuzzer_detect_dpi',
|
||||||
|
FUZZER_AUTO_APPLY = 'fuzzer_auto_apply',
|
||||||
|
FUZZER_HISTORY = 'fuzzer_history',
|
||||||
|
FUZZER_CLEAR_HISTORY = 'fuzzer_clear_history',
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum AvailableClashAPIMethods {
|
export enum AvailableClashAPIMethods {
|
||||||
@@ -905,6 +909,7 @@ export namespace Tachyon {
|
|||||||
error?: string | null;
|
error?: string | null;
|
||||||
started_at: number;
|
started_at: number;
|
||||||
finished_at: number;
|
finished_at: number;
|
||||||
|
dpi_detection?: FuzzerDpiDetection | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FuzzerStartResponse {
|
export interface FuzzerStartResponse {
|
||||||
@@ -978,4 +983,65 @@ export namespace Tachyon {
|
|||||||
name: string;
|
name: string;
|
||||||
urls: FuzzerTargetSuiteItem[];
|
urls: FuzzerTargetSuiteItem[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface FuzzerDpiDetection {
|
||||||
|
type: 'rst' | 'throttle' | 'dns_block' | 'unknown' | 'none';
|
||||||
|
confidence: number;
|
||||||
|
details: string;
|
||||||
|
recommended_engines: string[];
|
||||||
|
probe_metrics: {
|
||||||
|
http_code: number;
|
||||||
|
handshake_ms: number;
|
||||||
|
ttfb_ms: number;
|
||||||
|
speed_kbps: number;
|
||||||
|
error: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FuzzerHistoryEntry {
|
||||||
|
timestamp: number;
|
||||||
|
engine: string;
|
||||||
|
target: string;
|
||||||
|
mode: string;
|
||||||
|
best_strategy: {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
engine: string;
|
||||||
|
args: string;
|
||||||
|
score: number;
|
||||||
|
ttfb_ms: number;
|
||||||
|
speed_kbps: number;
|
||||||
|
} | null;
|
||||||
|
total_tested: number;
|
||||||
|
working_count: number;
|
||||||
|
dpi_detection: FuzzerDpiDetection | null;
|
||||||
|
duration_sec: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FuzzerHistoryResult {
|
||||||
|
success: boolean;
|
||||||
|
entries: FuzzerHistoryEntry[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FuzzerDetectDpiResponse {
|
||||||
|
type: 'rst' | 'throttle' | 'dns_block' | 'unknown' | 'none';
|
||||||
|
confidence: number;
|
||||||
|
details: string;
|
||||||
|
recommended_engines: string[];
|
||||||
|
probe_metrics: {
|
||||||
|
http_code: number;
|
||||||
|
handshake_ms: number;
|
||||||
|
ttfb_ms: number;
|
||||||
|
speed_kbps: number;
|
||||||
|
error: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FuzzerAutoApplyResponse {
|
||||||
|
success: boolean;
|
||||||
|
engine: string;
|
||||||
|
applied_to: string;
|
||||||
|
args: string;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3123,6 +3123,10 @@ var Tachyon;
|
|||||||
AvailableMethods2["FUZZER_GET_PATTERNS"] = "fuzzer_get_patterns";
|
AvailableMethods2["FUZZER_GET_PATTERNS"] = "fuzzer_get_patterns";
|
||||||
AvailableMethods2["FUZZER_SAVE_PATTERNS"] = "fuzzer_save_patterns";
|
AvailableMethods2["FUZZER_SAVE_PATTERNS"] = "fuzzer_save_patterns";
|
||||||
AvailableMethods2["FUZZER_RESET_PATTERNS"] = "fuzzer_reset_patterns";
|
AvailableMethods2["FUZZER_RESET_PATTERNS"] = "fuzzer_reset_patterns";
|
||||||
|
AvailableMethods2["FUZZER_DETECT_DPI"] = "fuzzer_detect_dpi";
|
||||||
|
AvailableMethods2["FUZZER_AUTO_APPLY"] = "fuzzer_auto_apply";
|
||||||
|
AvailableMethods2["FUZZER_HISTORY"] = "fuzzer_history";
|
||||||
|
AvailableMethods2["FUZZER_CLEAR_HISTORY"] = "fuzzer_clear_history";
|
||||||
})(AvailableMethods = Tachyon2.AvailableMethods || (Tachyon2.AvailableMethods = {}));
|
})(AvailableMethods = Tachyon2.AvailableMethods || (Tachyon2.AvailableMethods = {}));
|
||||||
let AvailableClashAPIMethods;
|
let AvailableClashAPIMethods;
|
||||||
((AvailableClashAPIMethods2) => {
|
((AvailableClashAPIMethods2) => {
|
||||||
@@ -4160,6 +4164,94 @@ var TachyonShellMethods = {
|
|||||||
success: false,
|
success: false,
|
||||||
error: parsed?.error || response.stderr || _("Failed to reset fuzzer patterns")
|
error: parsed?.error || response.stderr || _("Failed to reset fuzzer patterns")
|
||||||
};
|
};
|
||||||
|
},
|
||||||
|
detectFuzzerDpi: async (target, customUrl) => {
|
||||||
|
const response = await executeShellCommand({
|
||||||
|
command: "/usr/bin/tachyon",
|
||||||
|
args: [
|
||||||
|
Tachyon.AvailableMethods.FUZZER_DETECT_DPI,
|
||||||
|
target,
|
||||||
|
...customUrl ? [customUrl] : []
|
||||||
|
],
|
||||||
|
timeout: COMPONENT_ACTION_RPC_TIMEOUT_MS
|
||||||
|
});
|
||||||
|
const parsed = parseJsonObjectOutput(
|
||||||
|
response.stdout
|
||||||
|
);
|
||||||
|
if ((response.code ?? 0) !== 0 || !parsed) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: response.stderr || _("Failed to detect DPI type")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: parsed
|
||||||
|
};
|
||||||
|
},
|
||||||
|
autoApplyFuzzerStrategy: async (targetRule) => {
|
||||||
|
const response = await executeShellCommand({
|
||||||
|
command: "/usr/bin/tachyon",
|
||||||
|
args: [
|
||||||
|
Tachyon.AvailableMethods.FUZZER_AUTO_APPLY,
|
||||||
|
...targetRule ? [targetRule] : []
|
||||||
|
],
|
||||||
|
timeout: UI_ACTION_RPC_TIMEOUT_MS
|
||||||
|
});
|
||||||
|
const parsed = parseJsonObjectOutput(
|
||||||
|
response.stdout
|
||||||
|
);
|
||||||
|
if ((response.code ?? 0) !== 0 || !parsed?.success) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: parsed?.error || response.stderr || _("Failed to auto-apply strategy")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: parsed
|
||||||
|
};
|
||||||
|
},
|
||||||
|
getFuzzerHistory: async (limit) => {
|
||||||
|
const response = await executeShellCommand({
|
||||||
|
command: "/usr/bin/tachyon",
|
||||||
|
args: [
|
||||||
|
Tachyon.AvailableMethods.FUZZER_HISTORY,
|
||||||
|
String(limit || 20)
|
||||||
|
],
|
||||||
|
timeout: COMPONENT_ACTION_RPC_TIMEOUT_MS
|
||||||
|
});
|
||||||
|
const parsed = parseJsonObjectOutput(
|
||||||
|
response.stdout
|
||||||
|
);
|
||||||
|
if ((response.code ?? 0) !== 0 || !parsed) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: response.stderr || _("Failed to get fuzzer history")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: parsed
|
||||||
|
};
|
||||||
|
},
|
||||||
|
clearFuzzerHistory: async () => {
|
||||||
|
const response = await executeShellCommand({
|
||||||
|
command: "/usr/bin/tachyon",
|
||||||
|
args: [Tachyon.AvailableMethods.FUZZER_CLEAR_HISTORY],
|
||||||
|
timeout: COMPONENT_ACTION_RPC_TIMEOUT_MS
|
||||||
|
});
|
||||||
|
const parsed = parseJsonObjectOutput(response.stdout);
|
||||||
|
if ((response.code ?? 0) !== 0 || !parsed?.success) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: response.stderr || _("Failed to clear fuzzer history")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
data: { message: _("History cleared") }
|
||||||
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -11831,6 +11923,8 @@ function renderStrategyFuzzerModal(ruleNames = []) {
|
|||||||
let isRunning = false;
|
let isRunning = false;
|
||||||
let currentState = null;
|
let currentState = null;
|
||||||
let resultFilter = "all";
|
let resultFilter = "all";
|
||||||
|
let autoApplyEnabled = false;
|
||||||
|
let currentDpiDetection = null;
|
||||||
let patternsConfig = {
|
let patternsConfig = {
|
||||||
zapret2: {
|
zapret2: {
|
||||||
splits: ["1", "2", "3", "midsld", "sniext+4", "1,midsld", "1,sniext+2"],
|
splits: ["1", "2", "3", "midsld", "sniext+4", "1,midsld", "1,sniext+2"],
|
||||||
@@ -11888,7 +11982,8 @@ function renderStrategyFuzzerModal(ruleNames = []) {
|
|||||||
createTabButton("benchmark", "\u26A1", _("Benchmark & Test")),
|
createTabButton("benchmark", "\u26A1", _("Benchmark & Test")),
|
||||||
createTabButton("patterns", "\u{1F6E0}\uFE0F", _("Pattern Builder")),
|
createTabButton("patterns", "\u{1F6E0}\uFE0F", _("Pattern Builder")),
|
||||||
createTabButton("custom", "\u2795", _("My Strategies")),
|
createTabButton("custom", "\u2795", _("My Strategies")),
|
||||||
createTabButton("ai", "\u{1F9E0}", _("AI Doctor RAG"))
|
createTabButton("ai", "\u{1F9E0}", _("AI Doctor RAG")),
|
||||||
|
createTabButton("history", "\u{1F4CB}", _("History"))
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
const tabContentBenchmark = E("div", {
|
const tabContentBenchmark = E("div", {
|
||||||
@@ -11903,6 +11998,9 @@ function renderStrategyFuzzerModal(ruleNames = []) {
|
|||||||
const tabContentAi = E("div", {
|
const tabContentAi = E("div", {
|
||||||
style: "display: none; flex-direction: column; gap: 14px;"
|
style: "display: none; flex-direction: column; gap: 14px;"
|
||||||
});
|
});
|
||||||
|
const tabContentHistory = E("div", {
|
||||||
|
style: "display: none; flex-direction: column; gap: 14px;"
|
||||||
|
});
|
||||||
const updateTabVisibility = () => {
|
const updateTabVisibility = () => {
|
||||||
for (const key in tabButtons) {
|
for (const key in tabButtons) {
|
||||||
tabButtons[key].className = `cbi-button ${activeTab2 === key ? "cbi-button-action" : "cbi-button-neutral"}`;
|
tabButtons[key].className = `cbi-button ${activeTab2 === key ? "cbi-button-action" : "cbi-button-neutral"}`;
|
||||||
@@ -11911,8 +12009,10 @@ function renderStrategyFuzzerModal(ruleNames = []) {
|
|||||||
tabContentPatterns.style.display = activeTab2 === "patterns" ? "flex" : "none";
|
tabContentPatterns.style.display = activeTab2 === "patterns" ? "flex" : "none";
|
||||||
tabContentCustom.style.display = activeTab2 === "custom" ? "flex" : "none";
|
tabContentCustom.style.display = activeTab2 === "custom" ? "flex" : "none";
|
||||||
tabContentAi.style.display = activeTab2 === "ai" ? "flex" : "none";
|
tabContentAi.style.display = activeTab2 === "ai" ? "flex" : "none";
|
||||||
|
tabContentHistory.style.display = activeTab2 === "history" ? "flex" : "none";
|
||||||
if (activeTab2 === "patterns") renderPatternsTab();
|
if (activeTab2 === "patterns") renderPatternsTab();
|
||||||
if (activeTab2 === "custom") renderCustomTab();
|
if (activeTab2 === "custom") renderCustomTab();
|
||||||
|
if (activeTab2 === "history") renderHistoryTab();
|
||||||
};
|
};
|
||||||
const controlsGrid = E("div", {
|
const controlsGrid = E("div", {
|
||||||
style: "display: grid; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); gap: 10px; align-items: end;"
|
style: "display: grid; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); gap: 10px; align-items: end;"
|
||||||
@@ -12053,6 +12153,34 @@ function renderStrategyFuzzerModal(ruleNames = []) {
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
controlsGrid.append(engineGroup, targetGroup, modeGroup, ruleGroup);
|
controlsGrid.append(engineGroup, targetGroup, modeGroup, ruleGroup);
|
||||||
|
const autoApplyCheckbox = E("input", {
|
||||||
|
type: "checkbox",
|
||||||
|
id: "tachyon-fuzzer-auto-apply",
|
||||||
|
style: "margin: 0;"
|
||||||
|
});
|
||||||
|
autoApplyCheckbox.addEventListener("change", () => {
|
||||||
|
autoApplyEnabled = autoApplyCheckbox.checked;
|
||||||
|
});
|
||||||
|
const autoApplyGroup = E(
|
||||||
|
"div",
|
||||||
|
{
|
||||||
|
style: "display: flex; align-items: end; padding: 4px 0; grid-column: 1 / -1;"
|
||||||
|
},
|
||||||
|
[
|
||||||
|
E(
|
||||||
|
"div",
|
||||||
|
{
|
||||||
|
style: "font-size: 11px; cursor: pointer; display: flex; align-items: center; gap: 4px;"
|
||||||
|
},
|
||||||
|
[autoApplyCheckbox, E("span", {}, _("Auto-apply best strategy"))]
|
||||||
|
)
|
||||||
|
]
|
||||||
|
);
|
||||||
|
controlsGrid.append(autoApplyGroup);
|
||||||
|
const dpiDetectionBanner = E("div", {
|
||||||
|
id: "tachyon-fuzzer-dpi-banner",
|
||||||
|
style: "display: none; padding: 10px 14px; border-radius: 6px; font-size: 12px; line-height: 1.4;"
|
||||||
|
});
|
||||||
const progressContainer = E(
|
const progressContainer = E(
|
||||||
"div",
|
"div",
|
||||||
{
|
{
|
||||||
@@ -12245,6 +12373,7 @@ function renderStrategyFuzzerModal(ruleNames = []) {
|
|||||||
);
|
);
|
||||||
tabContentBenchmark.append(
|
tabContentBenchmark.append(
|
||||||
controlsGrid,
|
controlsGrid,
|
||||||
|
dpiDetectionBanner,
|
||||||
progressContainer,
|
progressContainer,
|
||||||
filterToolbar,
|
filterToolbar,
|
||||||
resultsContainer,
|
resultsContainer,
|
||||||
@@ -12720,6 +12849,126 @@ function renderStrategyFuzzerModal(ruleNames = []) {
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
tabContentAi.append(aiSynthesizerCard);
|
tabContentAi.append(aiSynthesizerCard);
|
||||||
|
const renderHistoryTab = async () => {
|
||||||
|
tabContentHistory.replaceChildren();
|
||||||
|
const historyHeader = E(
|
||||||
|
"div",
|
||||||
|
{
|
||||||
|
style: "display: flex; justify-content: space-between; align-items: center;"
|
||||||
|
},
|
||||||
|
[
|
||||||
|
E(
|
||||||
|
"div",
|
||||||
|
{ style: "font-size: 13px; font-weight: bold;" },
|
||||||
|
"\u{1F4CB} " + _("Benchmark History")
|
||||||
|
),
|
||||||
|
E("div", { style: "display: flex; gap: 8px;" }, [
|
||||||
|
renderButton({
|
||||||
|
text: _("\u{1F504} Refresh"),
|
||||||
|
classNames: ["cbi-button-neutral"],
|
||||||
|
onClick: () => renderHistoryTab()
|
||||||
|
}),
|
||||||
|
renderButton({
|
||||||
|
text: _("\u{1F5D1}\uFE0F Clear History"),
|
||||||
|
classNames: ["cbi-button-reset"],
|
||||||
|
onClick: async () => {
|
||||||
|
await TachyonShellMethods.clearFuzzerHistory();
|
||||||
|
showToast(_("History cleared"), "success");
|
||||||
|
renderHistoryTab();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
])
|
||||||
|
]
|
||||||
|
);
|
||||||
|
const historyContainer = E("div", {
|
||||||
|
style: "border: 1px solid var(--border-color, rgba(255,255,255,0.1)); border-radius: 6px; overflow: hidden;"
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
const res = await TachyonShellMethods.getFuzzerHistory(20);
|
||||||
|
const entries = res.data?.entries || [];
|
||||||
|
if (entries.length === 0) {
|
||||||
|
historyContainer.appendChild(
|
||||||
|
E(
|
||||||
|
"div",
|
||||||
|
{
|
||||||
|
style: "padding: 24px; text-align: center; opacity: 0.6; font-size: 12px;"
|
||||||
|
},
|
||||||
|
_("No benchmark history yet. Run a benchmark to see results here.")
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const table = E(
|
||||||
|
"table",
|
||||||
|
{
|
||||||
|
class: "table",
|
||||||
|
style: "width: 100%; margin: 0; font-size: 12px;"
|
||||||
|
},
|
||||||
|
[
|
||||||
|
E("thead", {}, [
|
||||||
|
E("tr", {}, [
|
||||||
|
E("th", { style: "padding: 8px;" }, _("Date")),
|
||||||
|
E("th", { style: "padding: 8px;" }, _("Engine")),
|
||||||
|
E("th", { style: "padding: 8px;" }, _("Target")),
|
||||||
|
E("th", { style: "padding: 8px;" }, _("DPI Type")),
|
||||||
|
E("th", { style: "padding: 8px;" }, _("Best Strategy")),
|
||||||
|
E("th", { style: "padding: 8px; width: 60px;" }, _("Score")),
|
||||||
|
E("th", { style: "padding: 8px; width: 70px;" }, _("Working"))
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
E(
|
||||||
|
"tbody",
|
||||||
|
{},
|
||||||
|
entries.reverse().map((entry) => {
|
||||||
|
const date = new Date(entry.timestamp * 1e3);
|
||||||
|
const dateStr = `${date.toLocaleDateString()} ${date.toLocaleTimeString()}`;
|
||||||
|
const dpiType = entry.dpi_detection ? `${entry.dpi_detection.type} (${entry.dpi_detection.confidence}%)` : "\u2014";
|
||||||
|
const bestName = entry.best_strategy ? entry.best_strategy.name : "\u2014";
|
||||||
|
const bestScore = entry.best_strategy ? String(entry.best_strategy.score) : "0";
|
||||||
|
const working = `${entry.working_count}/${entry.total_tested}`;
|
||||||
|
return E("tr", {}, [
|
||||||
|
E("td", { style: "padding: 8px;" }, dateStr),
|
||||||
|
E(
|
||||||
|
"td",
|
||||||
|
{
|
||||||
|
style: "padding: 8px; font-family: monospace;"
|
||||||
|
},
|
||||||
|
entry.engine
|
||||||
|
),
|
||||||
|
E("td", { style: "padding: 8px;" }, entry.target),
|
||||||
|
E("td", { style: "padding: 8px;" }, dpiType),
|
||||||
|
E(
|
||||||
|
"td",
|
||||||
|
{
|
||||||
|
style: "padding: 8px; font-size: 11px; max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;"
|
||||||
|
},
|
||||||
|
bestName
|
||||||
|
),
|
||||||
|
E(
|
||||||
|
"td",
|
||||||
|
{ style: "padding: 8px; font-weight: bold;" },
|
||||||
|
bestScore
|
||||||
|
),
|
||||||
|
E("td", { style: "padding: 8px;" }, working)
|
||||||
|
]);
|
||||||
|
})
|
||||||
|
)
|
||||||
|
]
|
||||||
|
);
|
||||||
|
historyContainer.appendChild(table);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
historyContainer.appendChild(
|
||||||
|
E(
|
||||||
|
"div",
|
||||||
|
{
|
||||||
|
style: "padding: 24px; text-align: center; opacity: 0.6; font-size: 12px;"
|
||||||
|
},
|
||||||
|
_("Failed to load history")
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
tabContentHistory.append(historyHeader, historyContainer);
|
||||||
|
};
|
||||||
const renderResults = (state) => {
|
const renderResults = (state) => {
|
||||||
const tbody = document.getElementById("tachyon-fuzzer-results-tbody");
|
const tbody = document.getElementById("tachyon-fuzzer-results-tbody");
|
||||||
if (!tbody) return;
|
if (!tbody) return;
|
||||||
@@ -12875,6 +13124,26 @@ function renderStrategyFuzzerModal(ruleNames = []) {
|
|||||||
stopPolling();
|
stopPolling();
|
||||||
startBtn.innerText = _("\u{1F680} Start Benchmark");
|
startBtn.innerText = _("\u{1F680} Start Benchmark");
|
||||||
startBtn.disabled = false;
|
startBtn.disabled = false;
|
||||||
|
if (autoApplyEnabled && currentState?.best_strategy) {
|
||||||
|
try {
|
||||||
|
const applyRes = await TachyonShellMethods.autoApplyFuzzerStrategy(
|
||||||
|
selectedRuleSection || void 0
|
||||||
|
);
|
||||||
|
if (applyRes.success) {
|
||||||
|
showToast(
|
||||||
|
`${_("\u2705 Auto-applied best strategy")}: ${currentState.best_strategy.name}`,
|
||||||
|
"success"
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
showToast(
|
||||||
|
`${_("Auto-apply failed")}: ${applyRes.error || _("Unknown error")}`,
|
||||||
|
"error"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
showToast(_("Auto-apply failed"), "error");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
@@ -12893,6 +13162,59 @@ function renderStrategyFuzzerModal(ruleNames = []) {
|
|||||||
pollingInterval = null;
|
pollingInterval = null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
const updateDpiBanner = (detection) => {
|
||||||
|
const banner = document.getElementById("tachyon-fuzzer-dpi-banner");
|
||||||
|
if (!banner) return;
|
||||||
|
if (!detection) {
|
||||||
|
banner.style.display = "none";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const typeColors = {
|
||||||
|
rst: { bg: "rgba(220, 53, 69, 0.12)", border: "#dc3545", icon: "\u{1F534}" },
|
||||||
|
throttle: {
|
||||||
|
bg: "rgba(255, 193, 7, 0.12)",
|
||||||
|
border: "#ffc107",
|
||||||
|
icon: "\u{1F7E1}"
|
||||||
|
},
|
||||||
|
dns_block: {
|
||||||
|
bg: "rgba(23, 162, 184, 0.12)",
|
||||||
|
border: "#17a2b8",
|
||||||
|
icon: "\u{1F535}"
|
||||||
|
},
|
||||||
|
unknown: {
|
||||||
|
bg: "rgba(108, 117, 125, 0.12)",
|
||||||
|
border: "#6c757d",
|
||||||
|
icon: "\u26AA"
|
||||||
|
},
|
||||||
|
none: {
|
||||||
|
bg: "rgba(40, 167, 69, 0.12)",
|
||||||
|
border: "#28a745",
|
||||||
|
icon: "\u{1F7E2}"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const typeLabels = {
|
||||||
|
rst: "TCP Reset Injection",
|
||||||
|
throttle: "Throttling / Deep Inspection",
|
||||||
|
dns_block: "DNS Blocking",
|
||||||
|
unknown: "Unknown DPI Pattern",
|
||||||
|
none: "No Blocking Detected"
|
||||||
|
};
|
||||||
|
const colors = typeColors[detection.type] || typeColors.unknown;
|
||||||
|
const label = typeLabels[detection.type] || detection.type;
|
||||||
|
banner.style.display = "block";
|
||||||
|
banner.style.background = colors.bg;
|
||||||
|
banner.style.borderLeft = `3px solid ${colors.border}`;
|
||||||
|
banner.innerHTML = `
|
||||||
|
<div style="display: flex; justify-content: space-between; align-items: start;">
|
||||||
|
<div>
|
||||||
|
<span style="font-weight: bold;">${colors.icon} ${_("DPI Detected")}: ${label}</span>
|
||||||
|
<span style="opacity: 0.7; margin-left: 8px;">${detection.confidence}% ${_("confidence")}</span>
|
||||||
|
</div>
|
||||||
|
${detection.recommended_engines.length > 0 ? `<div style="font-size: 11px; opacity: 0.7;">${_("Recommended")}: ${detection.recommended_engines.join(", ")}</div>` : ""}
|
||||||
|
</div>
|
||||||
|
<div style="margin-top: 4px; opacity: 0.8; font-size: 11px;">${detection.details}</div>
|
||||||
|
`;
|
||||||
|
};
|
||||||
const handleToggleRun = async () => {
|
const handleToggleRun = async () => {
|
||||||
if (isRunning) {
|
if (isRunning) {
|
||||||
await TachyonShellMethods.stopFuzzer();
|
await TachyonShellMethods.stopFuzzer();
|
||||||
@@ -12918,6 +13240,17 @@ function renderStrategyFuzzerModal(ruleNames = []) {
|
|||||||
currentStratEl.innerText = _(
|
currentStratEl.innerText = _(
|
||||||
"Preparing strategies and isolated nftables queue..."
|
"Preparing strategies and isolated nftables queue..."
|
||||||
);
|
);
|
||||||
|
try {
|
||||||
|
const dpiRes = await TachyonShellMethods.detectFuzzerDpi(
|
||||||
|
selectedTarget,
|
||||||
|
selectedTarget === "custom" ? customUrl : void 0
|
||||||
|
);
|
||||||
|
if (dpiRes.success && dpiRes.data) {
|
||||||
|
currentDpiDetection = dpiRes.data;
|
||||||
|
updateDpiBanner(dpiRes.data);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
}
|
||||||
const tbody = document.getElementById("tachyon-fuzzer-results-tbody");
|
const tbody = document.getElementById("tachyon-fuzzer-results-tbody");
|
||||||
if (tbody) {
|
if (tbody) {
|
||||||
tbody.replaceChildren(
|
tbody.replaceChildren(
|
||||||
@@ -13064,7 +13397,8 @@ function renderStrategyFuzzerModal(ruleNames = []) {
|
|||||||
tabContentBenchmark,
|
tabContentBenchmark,
|
||||||
tabContentPatterns,
|
tabContentPatterns,
|
||||||
tabContentCustom,
|
tabContentCustom,
|
||||||
tabContentAi
|
tabContentAi,
|
||||||
|
tabContentHistory
|
||||||
);
|
);
|
||||||
pollStatus();
|
pollStatus();
|
||||||
ui.showModal(_("\u26A1 Strategy Fuzzer & Auto-Tuner"), modalContainer);
|
ui.showModal(_("\u26A1 Strategy Fuzzer & Auto-Tuner"), modalContainer);
|
||||||
|
|||||||
@@ -268,6 +268,10 @@ function command_spec(command) {
|
|||||||
fuzzer_get_patterns: [ "diagnostics/fuzzer.uc", "get_patterns", 0 ],
|
fuzzer_get_patterns: [ "diagnostics/fuzzer.uc", "get_patterns", 0 ],
|
||||||
fuzzer_save_patterns: [ "diagnostics/fuzzer.uc", "save_patterns", 1 ],
|
fuzzer_save_patterns: [ "diagnostics/fuzzer.uc", "save_patterns", 1 ],
|
||||||
fuzzer_reset_patterns: [ "diagnostics/fuzzer.uc", "reset_patterns", 0 ],
|
fuzzer_reset_patterns: [ "diagnostics/fuzzer.uc", "reset_patterns", 0 ],
|
||||||
|
fuzzer_detect_dpi: [ "diagnostics/fuzzer.uc", "detect_dpi", 2 ],
|
||||||
|
fuzzer_auto_apply: [ "diagnostics/fuzzer.uc", "auto_apply", 1 ],
|
||||||
|
fuzzer_history: [ "diagnostics/fuzzer.uc", "history", 1 ],
|
||||||
|
fuzzer_clear_history: [ "diagnostics/fuzzer.uc", "clear_history", 0 ],
|
||||||
generate_warp: [ "service/warp_generator.uc", "", 1 ],
|
generate_warp: [ "service/warp_generator.uc", "", 1 ],
|
||||||
package_prerm: [ "service/package.uc", "prerm", 1 ],
|
package_prerm: [ "service/package.uc", "prerm", 1 ],
|
||||||
package_postinst: [ "service/package.uc", "postinst", 0 ],
|
package_postinst: [ "service/package.uc", "postinst", 0 ],
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ const CONFIG_NAME = getenv("TACHYON_CONFIG_NAME") || "tachyon";
|
|||||||
const STATE_DIR = getenv("TACHYON_FUZZER_STATE_DIR") || "/var/run/tachyon";
|
const STATE_DIR = getenv("TACHYON_FUZZER_STATE_DIR") || "/var/run/tachyon";
|
||||||
const STATE_FILE = STATE_DIR + "/fuzzer-state.json";
|
const STATE_FILE = STATE_DIR + "/fuzzer-state.json";
|
||||||
const PID_FILE = STATE_DIR + "/fuzzer-worker.pid";
|
const PID_FILE = STATE_DIR + "/fuzzer-worker.pid";
|
||||||
|
const HISTORY_FILE = "/etc/tachyon/fuzzer_history.json";
|
||||||
const BYEDPI_PORT = 11089;
|
const BYEDPI_PORT = 11089;
|
||||||
const NFQUEUE_QNUM_ZAPRET = 298;
|
const NFQUEUE_QNUM_ZAPRET = 298;
|
||||||
const NFQUEUE_QNUM_ZAPRET2 = 299;
|
const NFQUEUE_QNUM_ZAPRET2 = 299;
|
||||||
@@ -1078,6 +1079,203 @@ function parse_curl_output(output, result) {
|
|||||||
return result;
|
return 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",
|
||||||
|
// 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);
|
||||||
|
let target_url = urls_list[0] ? urls_list[0].url : "https://www.google.com";
|
||||||
|
|
||||||
|
let result = {
|
||||||
|
type: "unknown",
|
||||||
|
confidence: 0,
|
||||||
|
details: "",
|
||||||
|
recommended_engines: [],
|
||||||
|
probe_metrics: { http_code: 0, handshake_ms: 0, ttfb_ms: 0, speed_kbps: 0, error: "" }
|
||||||
|
};
|
||||||
|
|
||||||
|
// Direct probe without any bypass
|
||||||
|
let curl_cmd = sprintf(
|
||||||
|
"curl -so /dev/null -w '%%{http_code}\\t%%{time_appconnect}\\t%%{time_starttransfer}\\t%%{speed_download}' -L --connect-timeout 3 --max-time 5 %s 2>&1",
|
||||||
|
shell_quote(target_url)
|
||||||
|
);
|
||||||
|
let pipe = fs.popen(curl_cmd, "r");
|
||||||
|
let output = pipe ? pipe.read("all") : "";
|
||||||
|
if (pipe) pipe.close();
|
||||||
|
output = trim(output);
|
||||||
|
|
||||||
|
let metrics = {};
|
||||||
|
parse_curl_output(output, metrics);
|
||||||
|
result.probe_metrics = {
|
||||||
|
http_code: metrics.http_code || 0,
|
||||||
|
handshake_ms: metrics.handshake_ms || 0,
|
||||||
|
ttfb_ms: metrics.ttfb_ms || 0,
|
||||||
|
speed_kbps: metrics.speed_kbps || 0,
|
||||||
|
error: metrics.error || ""
|
||||||
|
};
|
||||||
|
|
||||||
|
// Also check for DNS-level blocking
|
||||||
|
let domain = target_url;
|
||||||
|
let dm = match(domain, /https?:\/\/([^/]+)/);
|
||||||
|
if (dm && dm[1]) domain = dm[1];
|
||||||
|
|
||||||
|
let dns_cmd = sprintf("nslookup %s 2>&1", shell_quote(domain));
|
||||||
|
let dns_pipe = fs.popen(dns_cmd, "r");
|
||||||
|
let dns_out = dns_pipe ? dns_pipe.read("all") : "";
|
||||||
|
if (dns_pipe) dns_pipe.close();
|
||||||
|
|
||||||
|
let dns_blocked = false;
|
||||||
|
if (index(dns_out, "NXDOMAIN") >= 0 || index(dns_out, "can't resolve") >= 0 || index(dns_out, "** server can't find") >= 0) {
|
||||||
|
dns_blocked = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Analyze failure patterns
|
||||||
|
let http_code = metrics.http_code || 0;
|
||||||
|
let handshake = metrics.handshake_ms || 0;
|
||||||
|
let ttfb = metrics.ttfb_ms || 0;
|
||||||
|
let error_str = metrics.error || "";
|
||||||
|
|
||||||
|
if (dns_blocked) {
|
||||||
|
result.type = "dns_block";
|
||||||
|
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 (index(error_str, "Connection reset") >= 0 || index(error_str, "ECONNRESET") >= 0) {
|
||||||
|
result.type = "rst";
|
||||||
|
result.confidence = 85;
|
||||||
|
result.details = sprintf("TCP RST received from DPI — active TCP reset injection detected");
|
||||||
|
result.recommended_engines = ["zapret2", "zapret"];
|
||||||
|
} else if (http_code == 0 || index(error_str, "Connection refused") >= 0 || index(error_str, "ECONNREFUSED") >= 0) {
|
||||||
|
result.type = "rst";
|
||||||
|
result.confidence = 70;
|
||||||
|
result.details = sprintf("Connection refused — likely RST or blackhole by DPI");
|
||||||
|
result.recommended_engines = ["zapret2", "zapret"];
|
||||||
|
} else if (http_code >= 400 && http_code < 500) {
|
||||||
|
result.type = "throttle";
|
||||||
|
result.confidence = 60;
|
||||||
|
result.details = sprintf("HTTP %d returned — DPI may be injecting HTTP errors or throttling", http_code);
|
||||||
|
result.recommended_engines = ["zapret2", "byedpi"];
|
||||||
|
} else if (handshake > 2000) {
|
||||||
|
result.type = "throttle";
|
||||||
|
result.confidence = 75;
|
||||||
|
result.details = sprintf("Very slow TLS handshake (%dms) — likely DPI deep inspection causing delay", handshake);
|
||||||
|
result.recommended_engines = ["zapret2", "zapret"];
|
||||||
|
} else if (ttfb > 3000 && http_code >= 200 && http_code < 400) {
|
||||||
|
result.type = "throttle";
|
||||||
|
result.confidence = 65;
|
||||||
|
result.details = sprintf("High TTFB (%dms) despite successful connection — likely bandwidth throttling", ttfb);
|
||||||
|
result.recommended_engines = ["zapret2", "byedpi"];
|
||||||
|
} else if (http_code >= 200 && http_code < 400) {
|
||||||
|
result.type = "none";
|
||||||
|
result.confidence = 95;
|
||||||
|
result.details = sprintf("Target accessible — no DPI blocking detected (HTTP %d, TTFB %dms)", http_code, ttfb);
|
||||||
|
result.recommended_engines = [];
|
||||||
|
} else {
|
||||||
|
result.type = "unknown";
|
||||||
|
result.confidence = 30;
|
||||||
|
result.details = sprintf("Inconclusive — HTTP %d, error: %s", http_code, error_str != "" ? error_str : "none");
|
||||||
|
result.recommended_engines = ["zapret2", "zapret", "byedpi"];
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── History Persistence ──────────────────────────────────────────────────────
|
||||||
|
function load_history() {
|
||||||
|
let data = read_json_file(HISTORY_FILE);
|
||||||
|
if (data && type(data) == "object" && data.entries && type(data.entries) == "array") {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
return { entries: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
function save_history(history) {
|
||||||
|
common.ensure_dir("/etc/tachyon");
|
||||||
|
while (length(history.entries) > 50) {
|
||||||
|
shift(history.entries);
|
||||||
|
}
|
||||||
|
write_json_file(HISTORY_FILE, history);
|
||||||
|
}
|
||||||
|
|
||||||
|
function append_history(entry) {
|
||||||
|
let history = load_history();
|
||||||
|
push(history.entries, {
|
||||||
|
timestamp: entry.timestamp || clock()[0],
|
||||||
|
engine: entry.engine || "unknown",
|
||||||
|
target: entry.target || "unknown",
|
||||||
|
mode: entry.mode || "presets",
|
||||||
|
best_strategy: entry.best_strategy || null,
|
||||||
|
total_tested: entry.total_tested || 0,
|
||||||
|
working_count: entry.working_count || 0,
|
||||||
|
dpi_detection: entry.dpi_detection || null,
|
||||||
|
duration_sec: entry.duration_sec || 0
|
||||||
|
});
|
||||||
|
save_history(history);
|
||||||
|
}
|
||||||
|
|
||||||
|
function get_history(limit) {
|
||||||
|
let history = load_history();
|
||||||
|
let entries = history.entries || [];
|
||||||
|
let n = int(limit) || 0;
|
||||||
|
if (n > 0 && length(entries) > n) {
|
||||||
|
let start = length(entries) - n;
|
||||||
|
let sliced = [];
|
||||||
|
for (let i = start; i < length(entries); i++) {
|
||||||
|
push(sliced, entries[i]);
|
||||||
|
}
|
||||||
|
entries = sliced;
|
||||||
|
}
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Strategy Priority Reranking (based on DPI type) ─────────────────────────
|
||||||
|
function rerank_strategies_by_dpi(strategies, dpi_type) {
|
||||||
|
if (!dpi_type || dpi_type.type == "none" || dpi_type.type == "unknown")
|
||||||
|
return strategies;
|
||||||
|
|
||||||
|
let priority_ids = [];
|
||||||
|
if (dpi_type.type == "rst") {
|
||||||
|
priority_ids = ["badseq", "md5sig", "multisplit", "disorder"];
|
||||||
|
} else if (dpi_type.type == "throttle") {
|
||||||
|
priority_ids = ["multisplit", "seqovl", "wsize", "split2"];
|
||||||
|
} else if (dpi_type.type == "dns_block") {
|
||||||
|
priority_ids = ["fake", "ttl=3", "ttl=4", "sniext"];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (length(priority_ids) == 0)
|
||||||
|
return strategies;
|
||||||
|
|
||||||
|
let scored = [];
|
||||||
|
for (let s in strategies) {
|
||||||
|
let score = 0;
|
||||||
|
let args_lower = lc(as_string(s.args));
|
||||||
|
let name_lower = lc(as_string(s.name));
|
||||||
|
for (let pid in priority_ids) {
|
||||||
|
if (index(args_lower, pid) >= 0 || index(name_lower, pid) >= 0) {
|
||||||
|
score += 10;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
push(scored, { strat: s, score: score });
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < length(scored) - 1; i++) {
|
||||||
|
for (let j = i + 1; j < length(scored); j++) {
|
||||||
|
if (scored[j].score > scored[i].score) {
|
||||||
|
let tmp = scored[i];
|
||||||
|
scored[i] = scored[j];
|
||||||
|
scored[j] = tmp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = [];
|
||||||
|
for (let item in scored) {
|
||||||
|
push(result, item.strat);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
function run_probe(engine, args_str, target_key, custom_url) {
|
function run_probe(engine, args_str, target_key, custom_url) {
|
||||||
cleanup_temp_daemons();
|
cleanup_temp_daemons();
|
||||||
|
|
||||||
@@ -1256,6 +1454,10 @@ function run_probe(engine, args_str, target_key, custom_url) {
|
|||||||
|
|
||||||
function run_fuzzer_worker(engine, target, custom_url, rule_section, custom_file, mode) {
|
function run_fuzzer_worker(engine, target, custom_url, rule_section, custom_file, mode) {
|
||||||
let target_url = resolve_target_url(target, custom_url);
|
let target_url = resolve_target_url(target, custom_url);
|
||||||
|
|
||||||
|
// ── Pre-fuzz DPI detection ────────────────────────────────────────────
|
||||||
|
let dpi_detection = detect_dpi_type(target, custom_url);
|
||||||
|
|
||||||
let strategies = null;
|
let strategies = null;
|
||||||
if (custom_file && custom_file != "" && fs.stat(custom_file) != null) {
|
if (custom_file && custom_file != "" && fs.stat(custom_file) != null) {
|
||||||
strategies = common.read_json_file(custom_file);
|
strategies = common.read_json_file(custom_file);
|
||||||
@@ -1263,6 +1465,10 @@ function run_fuzzer_worker(engine, target, custom_url, rule_section, custom_file
|
|||||||
if (!strategies || type(strategies) != "array" || length(strategies) == 0) {
|
if (!strategies || type(strategies) != "array" || length(strategies) == 0) {
|
||||||
strategies = get_strategies_for_engine(engine, mode);
|
strategies = get_strategies_for_engine(engine, mode);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Rerank strategies based on detected DPI type
|
||||||
|
strategies = rerank_strategies_by_dpi(strategies, dpi_detection);
|
||||||
|
|
||||||
let total = length(strategies);
|
let total = length(strategies);
|
||||||
|
|
||||||
let state = {
|
let state = {
|
||||||
@@ -1282,7 +1488,8 @@ function run_fuzzer_worker(engine, target, custom_url, rule_section, custom_file
|
|||||||
best_strategy: null,
|
best_strategy: null,
|
||||||
error: null,
|
error: null,
|
||||||
started_at: clock()[0],
|
started_at: clock()[0],
|
||||||
finished_at: 0
|
finished_at: 0,
|
||||||
|
dpi_detection: dpi_detection
|
||||||
};
|
};
|
||||||
|
|
||||||
save_fuzzer_state(state);
|
save_fuzzer_state(state);
|
||||||
@@ -1290,6 +1497,7 @@ function run_fuzzer_worker(engine, target, custom_url, rule_section, custom_file
|
|||||||
try {
|
try {
|
||||||
let highest_score = -1;
|
let highest_score = -1;
|
||||||
let best = null;
|
let best = null;
|
||||||
|
let working_count = 0;
|
||||||
|
|
||||||
for (let i = 0; i < total; i++) {
|
for (let i = 0; i < total; i++) {
|
||||||
let strat = strategies[i];
|
let strat = strategies[i];
|
||||||
@@ -1318,6 +1526,8 @@ function run_fuzzer_worker(engine, target, custom_url, rule_section, custom_file
|
|||||||
badge: ""
|
badge: ""
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (item_result.success) working_count++;
|
||||||
|
|
||||||
if (item_result.score > highest_score && item_result.success) {
|
if (item_result.score > highest_score && item_result.success) {
|
||||||
highest_score = item_result.score;
|
highest_score = item_result.score;
|
||||||
best = item_result;
|
best = item_result;
|
||||||
@@ -1351,6 +1561,28 @@ function run_fuzzer_worker(engine, target, custom_url, rule_section, custom_file
|
|||||||
state.current_strategy = null;
|
state.current_strategy = null;
|
||||||
state.finished_at = clock()[0];
|
state.finished_at = clock()[0];
|
||||||
save_fuzzer_state(state);
|
save_fuzzer_state(state);
|
||||||
|
|
||||||
|
// ── Persist to history ────────────────────────────────────────────
|
||||||
|
let duration = state.finished_at - state.started_at;
|
||||||
|
append_history({
|
||||||
|
timestamp: state.finished_at,
|
||||||
|
engine: engine,
|
||||||
|
target: target,
|
||||||
|
mode: mode,
|
||||||
|
best_strategy: best ? {
|
||||||
|
id: best.id,
|
||||||
|
name: best.name,
|
||||||
|
engine: best.engine,
|
||||||
|
args: best.args,
|
||||||
|
score: best.score,
|
||||||
|
ttfb_ms: best.ttfb_ms,
|
||||||
|
speed_kbps: best.speed_kbps
|
||||||
|
} : null,
|
||||||
|
total_tested: total,
|
||||||
|
working_count: working_count,
|
||||||
|
dpi_detection: dpi_detection,
|
||||||
|
duration_sec: int(duration)
|
||||||
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
state.running = false;
|
state.running = false;
|
||||||
state.error = as_string(err);
|
state.error = as_string(err);
|
||||||
@@ -1456,6 +1688,21 @@ function apply_strategy(engine, args_val, target_rule) {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function auto_apply_best(target_rule) {
|
||||||
|
let state = get_fuzzer_state();
|
||||||
|
if (!state.best_strategy || state.best_strategy.score <= 0) {
|
||||||
|
print(sprintf("%J\n", { success: false, error: "No winning strategy found — run a benchmark first" }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let best = state.best_strategy;
|
||||||
|
apply_strategy(best.engine, best.args, target_rule);
|
||||||
|
}
|
||||||
|
|
||||||
|
function clear_history() {
|
||||||
|
save_history({ entries: [] });
|
||||||
|
print(sprintf("%J\n", { success: true, message: "Fuzzer history cleared" }));
|
||||||
|
}
|
||||||
|
|
||||||
// CLI Dispatcher
|
// CLI Dispatcher
|
||||||
let op = ARGV[0] || "status";
|
let op = ARGV[0] || "status";
|
||||||
|
|
||||||
@@ -1478,6 +1725,16 @@ if (op == "start") {
|
|||||||
reset_patterns_config();
|
reset_patterns_config();
|
||||||
} else if (op == "ai_synthesize" || op == "synthesize") {
|
} else if (op == "ai_synthesize" || op == "synthesize") {
|
||||||
synthesize_ai_strategies(ARGV[1], ARGV[2], ARGV[3], ARGV[4]);
|
synthesize_ai_strategies(ARGV[1], ARGV[2], ARGV[3], ARGV[4]);
|
||||||
|
} else if (op == "detect_dpi") {
|
||||||
|
let detection = detect_dpi_type(ARGV[1], ARGV[2]);
|
||||||
|
print(sprintf("%J\n", detection));
|
||||||
|
} else if (op == "auto_apply") {
|
||||||
|
auto_apply_best(ARGV[1]);
|
||||||
|
} else if (op == "history") {
|
||||||
|
let entries = get_history(ARGV[1]);
|
||||||
|
print(sprintf("%J\n", { success: true, entries: entries }));
|
||||||
|
} else if (op == "clear_history") {
|
||||||
|
clear_history();
|
||||||
} else if (op == "generate" || op == "strategies_generate") {
|
} else if (op == "generate" || op == "strategies_generate") {
|
||||||
print(sprintf("%J\n", get_strategies_for_engine(ARGV[1], ARGV[2] || "combinatorial")));
|
print(sprintf("%J\n", get_strategies_for_engine(ARGV[1], ARGV[2] || "combinatorial")));
|
||||||
} else if (op == "strategies") {
|
} else if (op == "strategies") {
|
||||||
@@ -1491,7 +1748,7 @@ if (op == "start") {
|
|||||||
byedpi: get_strategies_for_engine("byedpi", strat_mode)
|
byedpi: get_strategies_for_engine("byedpi", strat_mode)
|
||||||
}));
|
}));
|
||||||
} else {
|
} else {
|
||||||
warn("Usage: fuzzer.uc [start|status|stop|apply|strategies|generate|get_patterns|save_patterns|reset_patterns|ai_synthesize|worker] ...\n");
|
warn("Usage: fuzzer.uc [start|status|stop|apply|strategies|generate|get_patterns|save_patterns|reset_patterns|ai_synthesize|detect_dpi|auto_apply|history|clear_history|worker] ...\n");
|
||||||
exit(1);
|
exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user