236 lines
22 KiB
Markdown
236 lines
22 KiB
Markdown
# Заметки по разработке burterm
|
||
|
||
Чистое описание архитектуры — в [ARCHITECTURE.ru.md](ARCHITECTURE.ru.md).
|
||
|
||
Этот файл — то, что не годится в ARCHITECTURE, потому что описывает не
|
||
«как устроено», а «почему так вышло» и «на что обратить внимание». Весь
|
||
код писался в среде без сети и без установленного Go — ни разу не
|
||
проходил через компилятор.
|
||
|
||
## Реальные баги, найденные и исправленные по ходу разработки
|
||
|
||
Не гипотетические — конкретные, с объяснением, как нашлись.
|
||
|
||
- **UTF-8 в `§маркерах§`.** Маркер `§` (U+00A7) — два байта в UTF-8
|
||
(`0xC2 0xA7`). Самая ранняя версия `ParseMarkers` сравнивала байты
|
||
напрямую и детектила маркер только по второму байту, оставляя первый
|
||
(`0xC2`) мусором в очищенной строке. Баг жил с первого черновика,
|
||
найден при разработке `DetectParamName` (умные заготовки payload'ов).
|
||
Исправлено: сканирование через `range` по рунам.
|
||
- **O(N²) в Results.** `resultsModel` на каждый входящий результат атаки
|
||
заново парсил текст всех полей фильтра и пересобирал весь список
|
||
отфильтрованных строк через `strings.Join`. На словаре в миллион строк
|
||
это останавливало TUI намертво. Исправлено тремя механизмами:
|
||
кэшированные предикаты (парсятся один раз при изменении текста поля, а
|
||
не на каждый результат), инкрементальный `strings.Builder` вместо
|
||
пересборки списка, дросселированный рендер во `viewport` по таймеру
|
||
раз в 300мс — и только для той атаки, что видна на экране.
|
||
- **`MaxIdleConnsPerHost` — баг с самого начала, не только для больших
|
||
словарей.** Голый `http.Transport` даёт дефолт 2 соединения на хост.
|
||
При `Concurrency > 2` (а дефолт формы — 10) большинство воркеров
|
||
закрывало и переоткрывало TCP/TLS-соединение на каждый запрос вместо
|
||
переиспользования из пула. Исправлено в `engine.New` и в
|
||
`spider.New` (там клиент был вообще голый — Transport==nil, то есть
|
||
неявно общий пакетный `http.DefaultTransport`).
|
||
- **Прокси не читал тело ответа.** Изначальная версия сохраняла только
|
||
заголовки/статус — без тела полнотекстовый поиск по Project был бы
|
||
пустым обещанием. Исправлено: `io.ReadAll` полного тела, капированная
|
||
копия (8КБ) для хранения, `resp.Body` восстанавливается через
|
||
`io.NopCloser` перед `resp.Write()` (тело можно прочитать только один
|
||
раз).
|
||
- **Утечка горутин при параллельных атаках.** Изначально `Model` хранила
|
||
один `resultsCh`/`cancel` на всё приложение — вторая атака без
|
||
дожидания первой тихо переключала отслеживание, а горутины первой
|
||
оставались писать в канал, который больше никто не читает
|
||
(небуферизованный канал — блокировка навечно). Исправлено переходом
|
||
на `[]*attackRun` (независимые атаки) и на `select` с `ctx.Done()` при
|
||
записи в `out` внутри `engine.send()`.
|
||
- **`proxyModel.SetSize` вообще не резервировал место под собственные
|
||
заголовок/футер.** `View()` всегда рисует "Перехвачено: N" + пустую
|
||
строку сверху и пустую строку + строку подсказки снизу — 4 строки
|
||
сверх `viewport`, а `SetSize` отдавал `viewport.Height` всю
|
||
переданную высоту без остатка. Из-за этого вкладка Proxy стабильно
|
||
вылезала за экран независимо от того, сколько высоты ей давал
|
||
`model.go` — сам бюджет внутри файла никогда не учитывал собственный
|
||
хром. Найдено построчным разбором `View()`, а не подбором чисел
|
||
снаружи. Исправлено вычитанием фиксированных 4 строк внутри
|
||
`SetSize`.
|
||
- **`comparerModel`: `SetSize` считал раскладку A/B side-by-side (делит
|
||
ширину пополам через `halfWidth`), а `View()` рисовал их друг под
|
||
другом.** Из-за этого реальная высота была примерно вдвое больше
|
||
того, что закладывал бюджет `diff.Height` — вкладка Comparer
|
||
вылезала за экран независимо от общего бюджета высоты. Исправлено не
|
||
подгонкой чисел, а починкой самого рассинхрона: `View()` теперь
|
||
реально кладёт A и B рядом через `lipgloss.JoinHorizontal`, как и
|
||
предполагала раскладка ширины с самого начала.
|
||
- **Футер Dashboard врал про `pgup`/`pgdn`.** Подсказка гласила
|
||
"pgup/pgdn — прокрутка", хотя после того как эти клавиши стали
|
||
перехватываться глобально под переключение вкладок (см. раздел
|
||
решений ниже), до `viewport` они больше не доходят и ничего не
|
||
прокручивают. Текст поправлен на "↑/↓ — прокрутка".
|
||
- **Бюджет высоты был неверен на 10 из 12 вкладок одновременно — и по
|
||
двум независимым причинам.** Первая: строки-подсказки в футере
|
||
доходили до 200+ символов (`targetview.go` — 425 символов исходно с
|
||
учётом экранирования, `repeater.go` — 219) — терминал переносил их
|
||
сам, а высота этого переноса нигде не учитывалась. Исправлено
|
||
разбиением каждого длинного футера на гарантированные 2 (иногда 3)
|
||
строки: специфичные подсказки вкладки отдельно, общая навигация
|
||
`f1-f12, pgup — вкладки` отдельно —
|
||
так поместится в одну физическую строку почти на любом терминале, а
|
||
не будет угадывания, перенесётся или нет. Вторая причина — независимая
|
||
от первой: у нескольких вкладок константа вычитания была просто
|
||
арифметически неверна **с самого начала**, до всей этой истории с
|
||
футерами (Fuzzer: `-12` вместо нужных `-20`, Repeater: `-2` вместо
|
||
`-8`, Decoder/Sequencer: `-4` вместо `-8`/`-6`, Spider/Project:
|
||
`-6`/`-8` вместо `-11`). Обе причины нашлись только построчным
|
||
подсчётом каждого `View()` — при этом подсчёте я сам дважды ошибся
|
||
на лету (Decoder сначала посчитал 5 вместо верных 8) и пересчитывал
|
||
заново. Итоговые числа для всех 12 файлов сведены в одну таблицу для
|
||
сверки:
|
||
```
|
||
comparerview.go: diff.Height = height - inputHeight - 6
|
||
dashboardview.go: vh = height - 3
|
||
decoderview.go: output.Height = height - inHeight - 8
|
||
fuzzerview.go: vh = height - 20 (минимум для "чистого" состояния)
|
||
projectview.go: vh = height - 11
|
||
proxyview.go: vh = height - 6 (плюс ещё -5 снаружи, в model.go, под "Прокси:...CA:...")
|
||
repeater.go: response.Height = height - reqHeight - 8
|
||
results.go: vh = height - 11 (плюс ещё -6 снаружи, в model.go, под заголовок атаки)
|
||
scannerview.go: vh = height - taHeight - 15 - len(scanner.AllCheckNames())
|
||
sequencerview.go: output.Height = height - inHeight - 6
|
||
spiderview.go: vh = height - 11
|
||
targetview.go: vh = height - 13
|
||
```
|
||
Request Builder сюда не входит — он не считает высоту `viewport` по
|
||
фиксированному бюджету (сайзинг пропорциональный, `height/3`), так
|
||
что этот класс бага его не касается, хотя длинный футер разбил и там
|
||
для единообразия.
|
||
- **Comparer вылезал за экран ещё дважды после починки JoinHorizontal**
|
||
(сначала `-5`, потом `-6` — оба раза построчный подсчёт по коду
|
||
казался верным, но на практике не сходилось). Рабочая гипотеза:
|
||
`textarea.View()` (компонент bubbles), возможно, рендерит **не ровно**
|
||
столько строк, сколько задано через `SetHeight` — то есть у неё есть
|
||
какой-то собственный оверхед (рамка/индикатор/что-то ещё), которого
|
||
нет в исходниках, доступных мне без реального терминала. Это касается
|
||
не только Comparer — `Decoder`, `Repeater`, `Scanner`, `Request
|
||
Builder` тоже используют `textarea` и МОГЛИ пострадать той же
|
||
проблемой, просто не в такой явной форме (или ещё не протестированы
|
||
так тщательно). Для Comparer вместо третьей попытки точного подсчёта
|
||
поставлен солидный запас (`-12` вместо расчётных `6`, `inputHeight`
|
||
урезан до `height/4`) — если и остальные текстовые вкладки продолжат
|
||
вылезать за экран после всех починок из записи выше, это будет
|
||
подтверждением гипотезы, и той же логике (запас вместо точного числа)
|
||
нужно последовать и там.
|
||
|
||
## Непроверенные места (порядок — от вероятных проблем к маловероятным)
|
||
|
||
1. **`go.mod`: `modernc.org/sqlite v1.34.4`** — версия по памяти,
|
||
`go mod tidy` может подтянуть другую, `go.sum` пересоберётся с нуля.
|
||
2. **bubbles/bubbletea API** — `Focus()` у `textinput`/`textarea` в
|
||
некоторых версиях возвращает `tea.Cmd`, в некоторых — нет; сигнатуры
|
||
`spinner.Tick` могли измениться между версиями.
|
||
3. **`exitErr.Sys().(syscall.WaitStatus)`** в `internal/fuzzer/fuzzer.go`
|
||
— platform-specific type assertion, работает только на Unix.
|
||
4. **Гонки данных, не проверенные race detector'ом.** Весь проект
|
||
писался с оглядкой на конкурентный доступ (`sync.RWMutex` в
|
||
`proxy.Server`, независимые снапшоты `sitemap.Scope`, `atomic` в
|
||
счётчиках фаззера), но ни разу не прогонялся под `go test -race`.
|
||
5. **`pgup`** (вкладка Fuzzer, единственная за пределами F1-F12 после
|
||
выделения Steganography/аудио в отдельный проект stegterm) —
|
||
четвёртая попытка навигации на вкладку, не влезающую в F1-F12
|
||
(история первых трёх — в разделе решений ниже). PageUp кодируется
|
||
терминалом так же атомарно, как F-клавиши (в отличие от Alt+буква,
|
||
которая на практике расщепляется на ESC+символ) — по конструкции
|
||
должен быть надёжнее, но конкретно в связке с Zellij ещё не
|
||
подтверждён.
|
||
|
||
## Сознательно отложенные и оставленные как есть решения
|
||
|
||
- **Навигация на Fuzzer — четыре попытки подряд, есть разобранная
|
||
причина третьего провала.** У физической клавиатуры нет F13/F14, а
|
||
13 вкладок в F1-F12 не помещаются (изначально этой проблеме были
|
||
подвержены сразу две вкладки — Fuzzer и Steganography, — но
|
||
Steganography с тех пор выделена в отдельный проект stegterm, и
|
||
сейчас в лишней клавише нуждается только Fuzzer). История:
|
||
1. `f13`/`f14` — не существуют физически, отпало сразу.
|
||
2. `shift+f1` — по историческому терминальному соглашению это и есть
|
||
F13 (`kf13` в terminfo), но подтверждённо не сработало в связке с
|
||
Zellij.
|
||
3. `alt+f` — подтверждённо не сработало, и в этот раз причина
|
||
найдена: терминал расщепляет `Alt+буква` на два отдельных байта
|
||
(`ESC`, затем сама буква) вместо единой комбинации. Если между
|
||
ними случается задержка (обычное дело через мультиплексор/SSH),
|
||
bubbletea не успевает склеить их обратно и видит одинокую букву —
|
||
а раз в фокусе текстовое поле (что почти всегда так по
|
||
умолчанию), эта буква просто печатается как обычный ввод, а не
|
||
распознаётся как комбинация. F-клавиши от этой болезни не страдают:
|
||
кодируются терминалом атомарно, не могут расщепиться в печатный
|
||
символ.
|
||
4. `pgup` — та же атомарная категория кодирования, что и у F-клавиш,
|
||
поэтому по конструкции не должен страдать той же болезнью, что
|
||
Alt-комбинации. Компромисс: `viewport` (Results, Proxy, Scanner и
|
||
т.д.) по умолчанию тоже вешает постраничную прокрутку на PageUp —
|
||
раз эта клавиша перехватывается глобально на уровне корневой
|
||
модели раньше, чем дойдёт до конкретной вкладки, постраничная
|
||
прокрутка пропадает везде, остаются только стрелки (построчно).
|
||
Осознанный выбор: надёжный доступ к вкладке важнее удобства
|
||
постраничной прокрутки. Не проверено на практике конкретно в
|
||
связке с Zellij.
|
||
- **Единая рамка вокруг тела каждой вкладки — откачена.** Была попытка
|
||
обернуть body любой вкладки (кроме Dashboard) в `panel()` для
|
||
визуальной консистентности со всем приложением. Дважды подряд не
|
||
смог вслепую подобрать для неё правильный бюджет высоты: сначала
|
||
меню обрезалось снизу на нескольких вкладках, потом (после первой
|
||
попытки исправить) сама рамка вылезала за край экрана на вкладках с
|
||
длинным содержимым (Comparer, Scanner). Без реального терминала под
|
||
рукой это угадывание, а не расчёт — после второго провала откатил
|
||
саму обёртку, а не в третий раз стал подбирать числа. Остались
|
||
только подтверждённо рабочие вещи: Dashboard со своими собственными
|
||
панелями и стилизованный таб-бар. Если понадобится вернуться к идее
|
||
единой рамки на каждой вкладке — делать это нужно с реальным
|
||
терминалом под рукой для итеративной проверки, не вслепую.
|
||
- **Плагины.** Обсуждалась возможность подключения внешних модулей.
|
||
Решение: не строить сейчас, направление на будущее — подпроцессы поверх
|
||
JSON Lines по stdin/stdout (не Go `plugin`-пакет — хрупкий, требует
|
||
точного совпадения версии Go; не встроенный Lua — новая непроверяемая
|
||
зависимость с тем же риском, что и sqlite). Подпроцесс живёт весь
|
||
прогон (атаку/скан), не спавнится на каждый вызов. Три типа плагинов
|
||
на будущее: `payload-generator`, `scanner-check`, `decoder`. В коде не
|
||
реализовано ничего.
|
||
- **Побочный эффект рекурсии Intruder.** У `Model` изначально было
|
||
заложено поведение «после старта атаки переключиться на Results» —
|
||
рекурсия тоже шлёт `attackStartedMsg` на каждую находку, значит при
|
||
активной рекурсии с несколькими совпадениями пользователя может
|
||
несколько раз подряд дёрнуть на вкладку Results. Решение неочевидно
|
||
(нужно различать «юзер нажал ctrl+r» от «рекурсия сама что-то
|
||
нашла»), оставлено как есть.
|
||
- **`drainGenerator` для Pitchfork/ClusterBomb** вычитывает генератор
|
||
целиком в память перед стартом. Для словарей на миллионы строк — разовая
|
||
пауза и всплеск памяти перед первым запросом. Не исправлено: риск
|
||
сломать корректность одометра выше выигрыша.
|
||
- **Только WAV, не MP3/FLAC.** Сознательный выбор, не недоделка: сжатие с
|
||
потерями убивает мелкую структуру спектра, в которую в CTF прячут
|
||
сообщения — задачи почти всегда именно в PCM WAV.
|
||
- **Поля "второе изображение"/"длина LSB" видны и в аудио-режиме
|
||
вкладки Steganography**, хотя там ничего не делают. Условный layout
|
||
ради их сокрытия усложнил бы `View()` заметнее, чем стоит того.
|
||
- **ASCII-логотип не в блочном стиле** (как у референсных TUI-инструментов
|
||
вроде Burp) — сознательно: точная посимвольная геометрия блочных букв
|
||
без реального терминала для проверки выравнивания легко разъезжается на
|
||
один символ. Вместо этого — разрядка текста акцентным цветом.
|
||
- **Заголовки панелей — не врезаны в саму рамку** (`┌─ TITLE ─┐`), а
|
||
первая строка жирным текстом внутри неё — по той же причине: точная
|
||
ширина без права на ошибку.
|
||
|
||
## Порядок отладки при первой сборке
|
||
|
||
1. `go mod tidy` — разберётся с зависимостями и `go.sum`.
|
||
2. `go build ./...` — почти наверняка всплывут точечные несовпадения
|
||
сигнатур bubbles/bubbletea. Правятся по месту, по тексту ошибки.
|
||
3. `go vet ./...` — статический анализ.
|
||
4. `go run -race ./cmd/burterm` — особенно при активном Proxy +
|
||
одновременной смене scope-правил.
|
||
5. Дальше — по вкладкам, от простых к сложным: Decoder/Comparer/Sequencer
|
||
(чистая логика, без сети) → Repeater (один запрос) → Intruder на
|
||
маленьком словаре → Proxy → Scanner/Fuzzer последними — там
|
||
наибольшая оставшаяся неопределённость.
|