Приложение работает, но никто не знает как: разбираем по трафику и логам
Приложение работает не первый год и приносит деньги — это не под вопросом, отключать его никто не собирается. Вопрос в другом: как оно работает изнутри, не знает никто в команде. Исходников нет, они утеряны, написаны на технологии, с которой никто не работал, или превратились в тысячи строк без структуры, где проще написать заново, чем понять. А переписывать нельзя, потому что непонятно, что именно переписывать — неизвестно, какие входы приложение принимает, какие вызовы делает наружу и по какой логике решает. Единственный практичный путь в такой ситуации — не читать код, а наблюдать за поведением: что летит по сети, что пишется в логи, и по этим следам восстанавливать карту того, как система устроена на самом деле.
Содержание
- Когда это ваш случай, а когда — соседняя статья
- С чего начать: карта процессов и точек входа до первого захваченного пакета
- Слушаем трафик: tcpdump и Wireshark на живой системе
- Когда трафик зашифрован: смотрим на уровне процесса
- Паттерны в логах: восстанавливаем алгоритм по последовательности событий
- От чёрного ящика к карте вызовов между компонентами
- Проверяем гипотезы, не ломая продакшн
Когда это ваш случай, а когда — соседняя статья
Прежде чем открывать tcpdump, стоит на минуту остановиться и убедиться, что задача именно эта, а не одна из двух соседних, которые легко перепутать по первому впечатлению.
Если вопрос стоит «а нужен ли этот процесс вообще, или его можно выключить» — это не сюда. Там задача не понять логику, а установить сам факт нужности: есть ли у процесса потребители, кто и когда с ним связывался, можно ли аккуратно его остановить. Этому посвящена статья «Сервис, о котором никто не помнит: как безопасно проверить, нужен ли он» — там протокол для установления факта нужности, а не разбора внутренней логики.
Если у вас есть конкретный файл с исходным кодом — пусть без единого комментария, но код физически существует и его можно открыть в редакторе — задача решается через статическое чтение с осторожными экспериментами: тесты вокруг него, версионирование, правки под логированием. Это разбирает статья «Нашли скрипт без комментариев, который держит продажи: что делать».
Эта статья — про третий случай, встречающийся чаще, чем хотелось бы: кода либо нет вообще (собранная бинарная поставка, докер-образ, лицензионный продукт стороннего вендора), либо он есть, но прочитать его целиком нереально за разумное время — десятки тысяч строк без архитектурной документации, либо часть логики вообще не в коде, а в конфигурации или внешнем сервисе без доступа. Единственный практичный путь здесь — реверс-инжиниринг поведения снаружи: что приложение слушает, куда стучится само, что пишет в логи. Ниже — как это делать по шагам, без остановки продакшна и без предположений там, где можно получить факт.
С чего начать: карта процессов и точек входа до первого захваченного пакета
Наблюдение за трафиком без понимания, что вообще происходит на сервере, превращается в бессмысленное чтение мегабайт данных. Сначала нужна грубая карта: из скольких процессов состоит приложение, какие порты слушает и какие соединения открывает наружу. Если по серверу в целом такой карты ещё нет, сначала стоит сделать её — этому посвящена статья «Как понять, что вообще крутится на чужом сервере: карта за вечер», здесь же карта нужна прицельно по одному приложению.
# все процессы приложения и их иерархия — что от чего порождено
ps auxwwf | grep -i имя_приложения
# что слушает и что уже открыто прямо сейчас
ss -tulnp | grep <PID_или_имя>
lsof -p <PID> -a -i
# исходящие соединения — куда приложение стучится само
ss -tnp | grep <PID>
Отдельно зафиксируйте способ запуска — юнит systemd (systemctl cat имя_юнита покажет ExecStart, рабочую директорию и окружение) или докер-контейнер (docker inspect имя_контейнера | grep -A30 '"Env"') — переменные окружения контейнера часто выдают URL внешних сервисов, которые по трафику снаружи увидеть сложнее.
На этом этапе цель не понять логику, а составить список: с какими портами и хостами вообще стоит работать дальше. Без такого списка захват трафика превращается в стрельбу по площадям.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверСлушаем трафик: tcpdump и Wireshark на живой системе
Дальше — самый прямой источник фактов: что реально идёт по сети. tcpdump пишет сырой трафик в файл, Wireshark (или его консольная часть tshark) даёт этот файл разобрать по протоколам и собрать в читаемые сессии.
Прежде чем захватывать, ограничьте объём — на боевом сервере с заметной нагрузкой безфильтровый захват на any быстро съедает диск и создаёт заметную нагрузку на CPU от самого дампа:
# захват только нужного порта, с ротацией файлов по 100 МБ и хранением последних 10
tcpdump -i any -s 0 -w /var/tmp/app-%Y%m%d-%H%M%S.pcap \
-C 100 -W 10 'port 8443 or port 5432'
# короткий захват на глаз — сколько соединений и куда за минуту
timeout 60 tcpdump -i any -nn 'host 10.0.0.5' | tee /tmp/quick.txt
Флаг -nn отключает резолвинг имён хостов и портов — без него tcpdump может тормозить на DNS-запросах и путать вывод. -s 0 берёт пакет целиком, без обрезки — иначе Wireshark потом не сможет собрать TCP-поток полностью.
Готовый .pcap разбирайте в Wireshark: Statistics → Conversations сразу даёт список пар хост:порт с количеством байт — видно, какие соединения основные, а какие фоновый шум (health-check-и, метрики). Statistics → Protocol Hierarchy показывает, какие протоколы вообще присутствуют — HTTP, HTTP/2, чистый TCP с бинарным форматом или что-то похожее на очередь сообщений. А правый клик на пакете — Follow → TCP Stream — склеивает весь диалог в одном окне: обычно это и есть момент, когда виден реальный протокол приложения.
Если Wireshark на сервере ставить не хочется (или это не разрешено политикой), тот же Follow TCP Stream можно получить консольно через tshark:
# только HTTP-запросы: метод, хост, URI — без разбора вручную
tshark -r /var/tmp/app.pcap -Y http.request -T fields \
-e frame.time -e ip.dst -e http.host -e http.request.method -e http.request.uri
Для быстрой проверки гипотезы «а есть ли вообще что-то похожее на X в трафике» без полного захвата удобен ngrep — ищет текстовый паттерн прямо в пакетах на лету: ngrep -d any -q 'POST' port 80.
Важная оговорка: если протокол — TLS (а сейчас это почти всегда так, даже для внутреннего трафика между сервисами), в дампе видно только метаданные — кто с кем соединился, сколько данных, но не содержимое. Расшифровать чужой TLS-трафик без приватного ключа сервера нельзя — это свойство протокола, а не сложность конкретной задачи. Если ключ вашего собственного сервера у вас есть, Wireshark расшифрует поток через Edit → Preferences → Protocols → TLS → RSA keys list — но чаще проще получить нужное на уровне процесса, о чём ниже.
Когда трафик зашифрован: смотрим на уровне процесса
Если содержимое из сети не достать, следующий уровень наблюдения — сам процесс: с кем он соединяется, какие файлы читает и пишет, какие системные вызовы делает. Это не даёт расшифрованное тело запроса, зато даёт точный список адресатов и таймингов, а нередко и открытый (нешифрованный) трафик до localhost-прокси, если приложение внутри терминирует TLS само и дальше ходит в соседний процесс уже в открытую.
# все сетевые системные вызовы процесса в реальном времени: connect, sendto, recvfrom
strace -f -tt -e trace=network -p <PID> 2>&1 | tee /tmp/strace-net.log
# только сами связи, без содержимого — быстрее и безопаснее на нагруженном проде
ss -tnp | grep <PID>
strace с трассировкой сети заметно замедляет процесс — на боевой системе ограничивайте время (timeout 30 strace ...) и по возможности работайте в тихое время. Если просадка недопустима даже на секунды, ограничьтесь ss и lsof — они читают текущее состояние ядра, а не перехватывают вызовы, и почти не влияют на производительность.
Отдельно посмотрите открытые файлы и unix-сокеты — часто именно там прячется связь с соседним компонентом, которая по TCP не видна: lsof -p <PID> | grep -E 'REG|unix'. Если приложение живёт в контейнере, а strace/tcpdump внутри образа нет — те же команды запускаются через nsenter -t $(docker inspect -f '{{.State.Pid}}' контейнер) -n.
Паттерны в логах: восстанавливаем алгоритм по последовательности событий
Трафик показывает, что приложение делает наружу. Логи чаще показывают, почему оно это делает — какое решение приняло и на основании чего. Даже без единой строки документации почти любой лог несёт структуру, если читать его не построчно, а как последовательность.
Первый шаг — найти, где вообще пишутся логи: если стандартный /var/log пуст или устарел, тот же приём с lsof -p <PID> находит открытые файловые дескрипторы лога даже без конфига — в списке будет обычный файл, который процесс держит открытым на запись.
Дальше — не читать лог целиком, а искать повторяющиеся структуры. Практический приём: взять один инцидент или один известный внешний запрос (например, вы сами дёрнули эндпоинт curl-ом и знаете точное время) и вытащить все строки лога в узком временном окне вокруг него из всех источников сразу:
# собираем все строки логов приложения и системы в диапазоне +-5 секунд от известного события,
# сортируем по времени — так видно, что что вызвало
for f in /var/log/app/*.log /var/log/nginx/*.log; do
awk -v from="14:32:05" -v to="14:32:15" '$0 ~ from,$0 ~ to' "$f"
done | sort -k1,2 > /tmp/timeline.txt
Если в логах есть хоть какой-то идентификатор запроса (request-id, trace-id, session-id — часто присутствует, даже если не задокументирован), это резко упрощает задачу: можно вытащить полный путь одного запроса через все компоненты, которые он затронул.
# все строки во всех логах с конкретным request-id — полный путь одного запроса через систему
grep -rh "req-a1b2c3d4" /var/log/*/*.log | sort
Полезно временно поднять уровень логирования, если приложение это поддерживает (LOG_LEVEL=debug, -v) — но с осторожностью: смена почти всегда требует перезапуска, а незапланированный рестарт критичного приложения — риск сам по себе. Если перезапустить нельзя, довольствуйтесь тем, что пишется сейчас, и компенсируйте это более плотным наблюдением за трафиком в тот же момент. При systemd-юнитах journalctl -u имя_юнита --since "14:32:00" --until "14:33:00" -o short-precise даёт готовую сортировку по времени без ручного склеивания файлов.
От чёрного ящика к карте вызовов между компонентами
Когда набралось достаточно наблюдений — из трафика и из логов — пора свести их в один артефакт, а не держать в голове. Практичнее всего таблица: источник, куда идёт вызов, протокол и порт, как часто наблюдается, и предполагаемое (по косвенным признакам) назначение.
| Источник | Куда | Протокол/порт | Частота | Предположительное назначение |
|---|---|---|---|---|
| app (PID 4211) | 127.0.0.1:6379 | TCP/6379 (Redis) | на каждый запрос | кэш сессий или блокировки |
| app | api.partner.example:443 | TLS | раз в ~40 сек | периодический опрос статуса заказов |
| app | 10.0.0.5:5432 | Postgres | пачками по ~200 мс | запись транзакций |
| nginx | app:8443 | TLS, HTTP/2 | входящий, постоянно | фронт |
| cron (внутри контейнера) | app:8443/internal/reindex | HTTP | раз в сутки, 03:00 | пересборка какого-то индекса |
Такая таблица — уже рабочая карта: по ней видно, какие связи однократные, а какие несут основную нагрузку и критичны для отказоустойчивости. Если всплывают связи, которых не видно ни по трафику, ни по логам — общий файл на диске, общая база без сетевого вызова, cron одного компонента, чинящий данные другого — это отдельный класс скрытых зависимостей, ему посвящена статья «Недокументированные связи между сервисами: кто кого держит на этом сервере».
Стрелки стоит рисовать по направлению вызова, а не данных — это разное, и путаница здесь потом стоит времени при отладке. Если связей много и таблица перестаёт читаться, текстовый граф в формате dot (Graphviz) держать проще, чем диаграмму в отдельном редакторе — версионируется через git как обычный файл.
Проверяем гипотезы, не ломая продакшн
Наблюдение даёт гипотезы, а не доказанные факты — то, что вы видите в трафике «запрос всегда идёт в Redis перед записью в базу», может быть совпадением на конкретной выборке, а не жёсткой частью алгоритма. Прежде чем закладывать гипотезу в план миграции, её стоит аккуратно проверить, не трогая боевой поток.
Самый безопасный вариант — тестовый запрос с заведомо отличимым payload-ом на staging-копии, если её можно поднять из бэкапа. Второй по безопасности — тестовый запрос на проде с данными, не задевающими реальных клиентов (тестовый аккаунт, синтетический заказ), с тем же наблюдением трафика и логов вокруг него — так гипотеза либо подтверждается фактом, либо опровергается.
Отдельно держите в уме: наблюдение снаружи никогда не докажет отсутствие поведения — только его присутствие в наблюдаемых случаях. Если приложение раз в квартал делает что-то нетипичное (закрытие периода, ротация ключей), а вы наблюдали его неделю, этого в карте не будет — пометьте это явным пробелом, а не молчаливо считайте карту полной. Проверьте crontab и systemd-таймеры на задачи с редким расписанием — они подсказывают, где искать такие пробелы.
Как только карта достигла состояния, которому можно доверять, зафиксируйте её письменно, пока знание свежее и не растворилось снова в «помню примерно» — иначе вложенные в реверс-инжиниринг часы достаются заново каждому следующему, кто столкнётся с тем же приложением.
Нужен сервер под эту задачу?
Разверните VPS MAATRIX за пару минут: NVMe, AMD EPYC, root-доступ, локации UK, США, Франция и РФ. Оплата картой РФ и по СБП.
Арендовать серверНужны сами нейросети для контента?
Генерируйте изображения, видео и озвучку нейросетями на falapi.io — десятки моделей в одном окне. Оплата картой РФ и по СБП.
Частые вопросы
Можно ли расшифровать TLS-трафик приложения без доступа к его приватному ключу?
Нет — если это действительно TLS и ключ сервера недоступен, расшифровка невозможна математически, а не «сложна». Если сервер ваш и ключ есть, Wireshark расшифрует поток через Preferences → Protocols → TLS. Если ключа нет, переходите на уровень процесса (strace, ss, lsof) — там видно, с кем и когда, но не что именно передаётся.
Насколько безопасно ставить tcpdump на критичный боевой сервер?
Захват с разумным фильтром по порту и хосту создаёт минимальную нагрузку — заметно меньше, чем полный strace. Риск не в CPU, а в диске: без ограничения (-C/-W) файл захвата может вырасти неконтролируемо. Всегда ставьте лимит размера и время автоматической остановки (timeout).
Что делать, если и трафика нет — приложение вообще не отвечает и непонятно, живо ли оно?
Это более базовая проверка: слушает ли процесс порт (ss -tulnp), отвечает ли на локальный запрос (curl -v localhost:порт), не завис ли в состоянии, где процесс есть, а обработка встала. Наблюдение за трафиком имеет смысл только для процесса, который точно живой и отвечает.
Стоит ли параллельно декомпилировать бинарник, если он есть в таком виде?
Как дополнение — да, если время позволяет. Но как основной путь — нет: декомпилированный код без символов часто читается тяжелее, чем восстановленное по трафику и логам поведение, а по времени выходит дороже.
Сколько времени закладывать на такой разбор приложения средней сложности?
Сильно зависит от числа внешних связей — ориентир от одного дня на простое приложение до полутора-двух недель на что-то с десятком внешних систем и нерегулярными фоновыми задачами. Это ориентир, а не норматив.
Нужно ли разрешение на захват трафика, если в нём могут быть персональные данные клиентов?
Да, это стоит согласовать заранее — pcap-файл может содержать то же, что видит сервер: платёжные данные, токены авторизации. Храните такие файлы минимально необходимое время и удаляйте после того, как гипотеза проверена.
Обсудить статью, задать вопрос или начать новую тему
Есть вопрос по этой статье, идея для обсуждения или просто хотите поделиться опытом? Сообщество MAATRIX ждёт. Для общения, пожалуйста, зарегистрируйтесь в нашем личном кабинете.
Перейти в сообщество →