И почему меня это волнует?
Вот небольшая выдержка из обобщённого вывода tcpdump для ssh-сеанса, в рамках которого я всего один раз нажал на клавишу:
$ ./first_lines_of_pcap.sh single-key.pcap 1 0.000s CLIENT->SERVER 36 bytes 2 0.007s SERVER->CLIENT 564 bytes 3 0.015s CLIENT->SERVER 0 bytes 4 0.015s CLIENT->SERVER 36 bytes 5 0.015s SERVER->CLIENT 36 bytes 6 0.026s CLIENT->SERVER 0 bytes 7 0.036s CLIENT->SERVER 36 bytes 8 0.036s SERVER->CLIENT 36 bytes 9 0.046s CLIENT->SERVER 0 bytes 10 0.059s CLIENT->SERVER 36 bytes
Действительно «небольшая», так как на самом деле строк там было очень много.
$ ./summarize_pcap.sh single-key.pcap Total packets: 270 36-byte msgs: 179 packets ( 66.3%) 6444 bytes Other data: 1 packet ( 0.4%) 564 bytes TCP ACKs: 90 packets ( 33.3%) Data sent: 6444 bytes in 36-byte messages, 564 bytes in other data Ratio: 11.4x more data in 36-byte messages than other data Data packet rate: ~90 packets/second (avg 11.1 ms between data packets)
Очень много пакетов, учитывая, что клавиша была нажата всего один раз. Что здесь происходит? И почему меня это заинтересовало?
Вот весь скрипт, если вам интересно:
# первые_строки_из_pcap.sh tshark -r "$1" \ -T fields -e frame.number -e frame.time_relative -e ip.src -e ip.dst -e tcp.len | \ awk 'NR<=10 {dir = ($3 ~ /71\.190/ ? "CLIENT->SERVER" : "SERVER->CLIENT"); printf "%3d %6.3fs %-4s %3s bytes\n", $1, $2, dir, $5}' # обобщение_pcap.sh tshark -r "$1" -Y "frame.time_relative <= 2.0" -T fields -e frame.time_relative -e tcp.len | awk ' { count++ payload = $2 if (payload == 0) { acks++ } else if (payload == 36) { mystery++ if (NR > 1 && prev_data_time > 0) { delta = $1 - prev_data_time sum_data_deltas += delta data_intervals++ } prev_data_time = $1 } else { game_data++ game_bytes = payload if (NR > 1 && prev_data_time > 0) { delta = $1 - prev_data_time sum_data_deltas += delta data_intervals++ } prev_data_time = $1 } } END { print "Total packets:", count print "" printf " 36-byte msgs: %3d packets (%5.1f%%) %5d bytes\n", mystery, 100*mystery/count, mystery*36 printf " Other data: %3d packet (%5.1f%%) %5d bytes\n", game_data, 100*game_data/count, game_bytes printf " TCP ACKs: %3d packets (%5.1f%%)\n", acks, 100*acks/count print "" printf " Data sent: %d bytes in 36-byte messages, %d bytes in other data\n", mystery*36, game_bytes printf " Ratio: %.1fx more data in 36-byte messages than other data\n", (mystery*36)/game_bytes print "" avg_ms = (sum_data_deltas / data_intervals) * 1000 printf " Data packet rate: ~%d packets/second (avg %.1f ms between data packets)\n", int(1000/avg_ms + 0.5), avg_ms }'
Я разрабатываю высокопроизводительную игру, которая работает по ssh. Интерфейс TUI для этой игры написан при помощи bubbletea, и информацию я отправлял через ssh при помощи wish.
Игра воспроизводится в окне размером 80x60, которое обновляется 10 раз в секунду. Я намерен обслуживать не менее 2000 пользователей, играющих одновременно. Таким образом, мне требуется обновлять ~100 миллионов ячеек в секунду. Поэтому меня интересует производительность.
Итак, у меня есть скрипт, соединяющий друг с другом несколько сотен ботов по ssh, а затем приказывающий ботам сделать ход. Затем я пользуюсь отличными профилировочными инструментами, имеющимися в Go, и смотрю, что происходит.
Вчера я случайно повредил мою тестовую обвязку. Вместо регулярной отправки игровых данных мой сервер стал с равным интервалом отправлять ботам единственное сообщение: «ваш экран слишком мал». На это потратилась половина процессорной мощности, доступной для моей игры, и полоса передачи данных также сузилась наполовину.
Поначалу я был разочарован. (Ненадолго) я подумал, что мне прямо в руки даром попал способ кардинально ускорить игру, а оказалось, что это просто ошибка, возникшая при тестировании.
Но подождите.
Если я не отправлял игровые данные ботам обратно, то почему использование ЦП падало на 50%, а не на 100%?
Устраняя эту проблему с тестовой обвязкой, я логировал при помощи tcpdump игровой трафик как с разрушающими изменениями, так и без них. В таком роде:
# Игра работает через порт 22 timeout 30s tcpdump -i eth0 'port 22' -w with-breaking-change.pcap # Обратить изменение timeout 30s tcpdump -i eth0 'port 22' -w without-breaking-change.pcap
При разрушающем изменении исчезала возможность отрисовывать игру через ssh. Так что with-breaking-change.pcap содержит те пакеты, которые являются накладными расходами при каждом соединении, а отображать игровое поле эти пакеты не позволяют.
Отладку я выполнял при помощи Claude Code, так что я попросил эту модель резюмировать, что она видит в pcap.
Может быть, посмотришь сам? Я положил with-breaking-change.pcap в этот каталог -- Вау! Вот что я нашёл: Распределение пакетов по размеру (всего 413 703 пакета): 274 907 пакетов (66%): ровно по 36 байт 138 778 пакетов (34%): по 0 байт (операции TCP ACK) 18 пакетов (<0,1%): по 72 байта
Когда я затем проанализировал другой pcap, поменьше, оказалось, что эти таинственные пакеты поступают с интервалом примерно по 20 мс.
Меня это изрядно озадачило (как и Claude Code). Мы накидали несколько идей о том, что бы это могло быть:
Сообщения, связанные с управлением потоком по SSH
Опрос о размере PTY или другие проверки состояния
Какие‑то причуды bubbletea или wish
Выделялось одно обстоятельство: эти проверки инициировал – мой ssh-клиент (то есть, заводской ssh, установленный на MacOS), а не мой сервер.
Как по наитию я сделал tcpdump обычного ssh-сеанса.
# у меня на mac, в одной вкладке sudo tcpdump -ien0 'port 22' # у меня на mac, в другой вкладке ssh $some_vm_of_mine
Я дождался, пока уляжется шум, характерный для начального этапа соединения, отправил на удалённую виртуальную машину сигнал ровно с одной клавиши и посмотрел вывод tcpdump.
Возник точно такой же паттерн! Что же происходит?
Стоило мне осознать, что это свойство присуще заводскому ssh, а не моей игре, отладка значительно упростилась.
Выполнив ssh -vvv, я составил достаточно полное впечатление о том, что же происходит:
debug3: obfuscate_keystroke_timing: starting: interval ~20ms debug3: obfuscate_keystroke_timing: stopping: chaff time expired (49 chaff packets sent) debug3: obfuscate_keystroke_timing: starting: interval ~20ms debug3: obfuscate_keystroke_timing: stopping: chaff time expired (101 chaff packets sent)
Именно эти 20 мс выдавали проблему. Такой период в точности соответствует тому загадочному паттерну, который мы наблюдали ранее! Оставшаяся часть сообщения также весьма информативна: мы отправляем 49 «chaff» пакетов при первом нажатии клавиши и 101 «chaff» при втором.
В 2023 году в ssh была добавлена обфускация длительности нажатий на клавиши. Смысл в том, что скорость, с которой набираются различные буквы, выдаёт часть информации о том, какой именно текст вы вводите. Поэтому ssh вместе с сигналами от нажимаемых вами клавиш отправляет множество chaff-пакетов, из-за которых злоумышленнику становится сложнее определить, какие именно нажатия соответствуют смысловому вводу.
Это весьма целесообразно для обычных ssh-сеансов, при которых критически важно соблюдать приватность. Но влечёт серьёзнейшие издержки для игры, распахнутой на весь Интернет, тем более, что критически важное требование к этой игре — минимизировать задержку.
Обфускацию нажатий клавиш можно отключить на стороне клиента. Откатив принципиальные изменения, внесённые ранее, я попытался обновить тестовую обвязку так, чтобы она проходила ObscureKeystrokeTiming=no при запуске ssh-сеансов.
Сработало отлично. Расход ресурса ЦП резко упал, а боты всё равно получали корректные данные.
Но едва ли такое решение применимо в реальной практике. Я хочу, чтобы ssh mygame Просто Работало, и мне не приходилось просить пользователей передавать мне опции, которых они, возможно, не понимают.
Claude Code исходно не верила, что нам удастся отключить эту функциональность и на стороне сервера.
К счастью, здесь описано, как именно устроена обфускация нажатий клавиш при работе с SSH. Поэтому мне не составило труда посмотреть соответствующий код в ssh-библиотеке, написанной на go (от которой я настроил транзитивную зависимость).
Сообщение лога: Ввестьи возможность пингования на транспортном уровне В таком случае добавляется пара сообщений транспортного протокола SSH SSH2_MSG_PING/PONG для реализации пингования. Эти сообщения используют числа, относящиеся к пространству номеров "локальные расширения" и объявляются при помощи "[email protected]" ext-info сообщения, в которой число "0" представлено как строка.
«Chaff»-сообщения, при помощи которых ssh маскирует нажатия клавиш – это сообщения SSH2_MSG_PING. Причём, они отправляются на серверы, которые оповещают о доступности расширения [email protected]. Почему ms просто… не сообщать об [email protected]?
Я поискал в библиотеке для ssh на go, что там сказано о [email protected] и нашёл коммит, в рамках которого была добавлена поддержка этой функции. Коммит был крошечный, и казалось, что откатить его будет очень просто.
Я склонировал репозиторий go crypto, приказал Claude откатить это изменение и обновить наши зависимости так, чтобы в коде стал использоваться наш клон (благодаря директиве replace языка go сделать форк библиотеки не составляет труда).
Затем я повторно прогнал мою тестовую обвязку. Результаты были…очень хороши:
Total CPU 29.90% -> 11.64% Syscalls 3.10s -> 0.66s Crypto 1.6s -> 0.11s Bandwidth ~6.5 Mbit/sec -> ~3 Mbit/sec
Claude также был весьма воодушевлён:
Разумеется, делать форк библиотеки crypto из go страшновато, и мне требовалось тщательно продумать, как организовать безопасную поддержку этого маленького патча.
Но улучшение было огромным. Большую часть предыдущей недели я потратил на то, чтобы выжимать дополнительную производительность по капле. Я просто представить себе не мог, что смогу сократить трату ресурсов более чем на 50%.
Я думал о том, смогут ли БЯМ частично взять на себя решение части задач, притом, что самому решать задачи мне очень нравится. Но должен сказать, отладка этой задачи с привлечением Claude Code оказалась суперинтересной.
Я достаточно хорошо знаком с tcpdump, tshark и им подобными, знаю, на что они способны. Но я недостаточно регулярно ими пользуюсь, поэтому рука в обращении с ними не набита. Мне, в самом деле, понравилось, что можно сказать агенту: «вот тут какая-то странная фиговина — расскажи, что происходит». Наблюдая, как агент выполняет команды, я мог постоянно актуализировать в голове текущее состояние решаемой задачи.
Всё равно остаются пограничные случаи. В какой-то момент, запутавшись, я переключился на ChatGPT, и она очень уверенно сообщила, что получившийся у меня вывод tcpdump — это нормальное поведение ssh.
Аналогично, мне пришлось наводить Claude Code на идею — а не сделать ли форк библиотеки ssh на go. Причём, мне пришлось применить тот самый выход из плоскости: «подождите… если тестовая обвязка сбоит, то почему использование ЦП не падает до 0%»?
В ответ на «БЯМ не полностью справляются с этой задачей» некоторые отвечают: «вы неправильно их понимаете!»
Думаю, иногда так и есть! Взаимодействие с БЯМ — это совершенно новый навык, и взаимодействие с ними воспринимается очень странно, если вы привыкли писать код так, как это делалось в 2020. Возможно, более талантливый пользователь БЯМ запросто решил бы с её помощью такую задачу.
Любой навык всегда лучше всего нарабатывается на практике. Для меня это означает – самостоятельно выяснить, как привить мой интуитивный подход к решению задач тем инструментам, с которыми я работаю.
Кроме того. Быть частью системы круто. А иначе как бы я написал этот пост?
Спасибо, что дочитали !
Источник

