IO Ninja – программируемый эмулятор терминала/сниффер (часть 3). Пишем «автоответчик»

jancyПродолжаем цикл статей о терминале/сниффере IO Ninja и переходим к рассмотрению одной из самых выигрышных сторон новой версии нашего продукта — программируемости. Она открывает новые применения такого, казалось бы, заурядного инструмента, как терминал или сниффер. Как и в предыдущей, второй версии продукта, исполнимые файлы третьей версии IO Ninja содержат лишь фреймворк необходимых компонентов (включая UI виджеты, движок для логгирования и классы для работы с IO, такие как io.Socket, io.Serial, io.PCap и т.д.). Логика же работы с конкретными транспортами содержится в плагинах, написанных на языке Jancy. Эти плагины лежат в выделенной папке «scripts» в виде исходных кодов и доступны как для ознакомления, так и для редактирования пользователями. Помимо модификации встроенных плагинов, также имеется возможность дописывать недостающий функционал с помощью пользовательских плагинов двух типов: Сессионный плагин (session plugin) — тяжёлый; Плагин-слой (layer plugin) — лёгкий. Первый тип плагинов — полноценный сессионный плагин, который создаёт сессию «с нуля». Вот что должно быть реализовано в типичном сессионном плагине: Определение формата записей в логе и создание лог-репрезентера — функции, которая превращает записи лога в финальное удобочитаемое представление; Формирование UI (меню, кнопки на тулбаре, свойства в редакторе свойств и т.д.); Создание транспортных и иных IO-объектов (сокеты, порты, файлы и т.д.); Обработка событий от UI-виджетов и IO-объектов и увязывание всего воедино. Например по нажатию на кнопку connect надо запустить процесс установки соединения, сделать в логе запись с кодом «начало-установки-соединения» и данными, описывающими адрес удалённого узла; по окончанию установки соединения создать соответствующую запись в логе и обновить UI (в частности, раздизэблить кнопку Send) и т.д.

В принципе, с помощью сессионного плагина можно написать решение для любой задачи, от тестирования некоей последовательности запросов-ответов до полноценного анализатора протоколов. Однако даже без ознакомления с исходным кодом встроенных сессий IO Ninja очевидно, что написание полноценной сессии — это достаточно трудоёмкий процесс, не очень подходящий для быстрого прототипирования и написания «на коленке» тестовых скриптов. Кроме того, совсем не хочется реализовывать заново или копипастить логику работы с транспортным соединением. Ведь это всё уже сделано в стандартных плагинах, как насчёт повторного использования? Наконец, написание полноценной сессии в значительной мере привязывает всё к одному конкретному транспорту, в то время как порой необходимо, например, проверить некую последовательность запросов сразу на нескольких разных транспортах. Аналогично, анализатор протоколов зачастую должен просто разбирать данные вне зависимости от того, по какому транспорту они были доставлены.Для решения всех вышеперечисленных проблем в IO Ninja предусмотрен второй тип плагинов — лёгкий плагин-слой (layer plugin).

Плагин-слой не может работать сам по себе. Вместо этого он подсоединяется к «несущей» сессии (carrier session) и соответствующим образом расширяет её функциональность. Делать это он может с помощью:

Изменения лог-репрезентера; Присоединения фильтра лога для «маскировки» нежелательных/неинтересных записей; Присоединения конвертера лога для генерации вторичного лога с целью декодирования сообщений протокола (это обычно требует также и смены лог-репрезентера); Присоединения слушателя лога для наблюдения за событиями несущей сессии и, возможно, для выполнения неких ответных действий; Создания своих элементов UI в дополнение к UI несущей сессии. Плагин-слой — это то, что, скорее всего, и придётся писать для большинства практических задач. Для облегчения разработки всех видов IO Ninja плагинов на Jancy мы предоставляем родную интегрированную среду разработки IO Ninja IDE. Данная среда основана на NetBeans Platform и предоставляет все стандартные средства code assist: синтаксическая подсветка, списки auto-completion, argument-tips, go-to-definition и т.д. Помимо этого, имеются визарды для генерации шаблонов типовых плагинов.IO Ninja IDE

Поставляется данная среда разработки в двух вариациях. Первая — это отдельный пакет IO Ninja IDE, который устанавливается как самостоятельный продукт. Вторая вариация подойдёт разработчикам, уже имеющим и использующим NetBeans для своих проектов (как у нескольких программистов в нашей конторе, включая меня). В этом случае, вместо того, чтобы устанавливать отдельную IO Ninja IDE, можно добавить плагины NetBeans для IO Ninja в уже присутсвующую установку NetBeans. Как первый, так и второй вариант можно взять тут: http://tibbo.com/ioninja/downloads.html.

Разработчики, по каким-то причинам не любящие NetBeans, или же не признающие IDE как таковые, могут вести разработку плагинов в любимом текстовом редакторе — vim, emacs, sublime, notepad наконец.

Далее мы продемонстрируем процесс создания с нуля типовых плагинов с использованием IO Ninja IDE. В качестве тестового протокола, с которым будут работать наши плагины, возьмём реальный протокол общения программируемых устройств Tibbo с интерактивным кросс-отладчиком. Для тестирования можно использовать Device Explorer, входящий в интегрированную среду разработки TIDE для программируемых устройств Tibbо (скачать TIDE или отдельный Device Explorer можно здесь: http://tibbo.com/downloads/basic/software.html).

Итак, приступим.

Первый плагин, который мы напишем, будет эмулировать сервер некоего протокола (в нашем примере — протокола программируемых устройств Tibbo), чтобы помогать нам в тестировании клиентской части данного протокола. Иными словами, наш плагин будет выполнять функции автоответичика, а именно: ждать данных, собирать их в пакеты, анализировать и слать что-то в ответ.Запускаем IO Ninja IDE (или NetBeans с плагинами для IO Ninja), щёлкаем на пункт меню File→New Project, и выбираем категорию проекта: Categories — IO Ninja, Projects — Plugin Project (если используется отдельная IO Ninja IDE, то данные категории будут единственно доступными). На следующем экране выбираем шаблон плагина Plugin Type — Answering Machine, вбиваем некое осмысленное имя в Project Name (например, TiosEmu) и жмём Next.

image

После заполнения на следующем экране строковых описателей (Layer Name, Layer Description и т.д) и нажатия на кнопку Finish будет сгенерирован рабочий костяк плагина: в исходном виде плагин шлёт осмысленный ответ на команды «help», «about» и «version», завершённые символами CR (возврат каретки) или LF (перевод строки).

Запустим плагин, чтобы посмотреть на его работу. Как уже было сказано выше, плагин-слой не может работать сам по себе и требует наличия несущей сессии, которая должна обеспечивать транспорт. Когда вы запускаете плагин-слой первый раз, IO Ninja IDE спросит вас, что использовать в качетсве несущей сессии.

Для тестирования выбираем TCP Listener, адаптер «All IPv6 adapters», порт 1001, жмём Listen. Открываем вторую сессию TCP Connection, удалённый адрес »[::1]:1001», жмём Connect (как вы догадались, это была ненавязчивая демонстрация поддержки IPv6, появившейся в версии 3.4.0;)

Теперь посылаем команды «help», «about» или «version» (можно кусками) и наслаждаемся результатом:

image

Теперь давайте немного проанализируем наш плагин. Откроем единственный исходный файл плагина TiosEmuLayer.jnc. Плагин по сути состоит из одного класса TiosEmuLayer, унаследованного от doc.Layer — базового класса для плагинов-слоёв (с помощью Ctrl+Click можно «погулять» по объявлениям API и ознакомиться с фреймворком доступных компонентов IO Ninja).

Посмотрим на конструткор нашего плагина:

construct (doc.PluginHost* pluginHost) { basetype.construct (pluginHost); m_rxScanner.construct (scanRx); pluginHost.m_log.attachListener (onLogRecord @ pluginHost.m_mainThreadScheduler); } Первая и вторая строчка понятна без комментариев — это, как можно догадаться, вызовы конструкторов родительского класса и поля-члена. А вот последняя строчка требует некоторых пояснений.Файл лога в IO Ninja представляет собой последовательность записей, каждая из которых содержит таймстамп, целочисленный код записи, а также (опционально) блок произвольных двоичных данных. Сразу отметим, что этот блок несёт не сам текст в том виде, в котором пользователь видит его в окне лога, а необходимые параметры, по которым этот текст можно восстановить (про восстановление текстовых строк лога с помощью функций-репрезентеров будет более подробно рассказано в разделе, посвящённом созданию плагина-анализатора протокола).

Когда сессионный плагин получает или отправляет данные — в лог добавляются соответствующие записи. Плагин-слой может «мониторить» лог на предмет появления новых записей, отбирать из них те, которые представляют интерес (например, RX) и выполнять некие действия в ответ.

Данная модель — хотя поначалу она может казаться несколько неестесвенной (почему бы вместо лога не мониторить события сразу на сокете?), — была выбрана по целому ряду параметров: проста в реализации, прозрачна и предсказуема, обеспечивает стандартизацию во взаимодействиях плагинов произвольной природы, подходит для создания каскадов обработки, не вносит сильной связности и т.д. Разумеется, у такой модели есть и определённые ограничения, но мы не будем сейчас подробно на этом останавливаться.

Метод attachListener как раз и добавляет слушателя лога. Следующий неочевидный момент — использование собаки @. В языке Jancy собакой @ (читается «at») обозначается бинарный оператор планировки для создания указателей на функции, которые гарантированно будут вызваны в нужном окружении. В данном случае мы хотим, чтобы слушатель был вызван в главном потоке плагина.

Внутри нашего слушателя мы проверяем код записи и, если это log.StdRecordCode.Rx (то есть входящие данные) — передаём его для дальнейшей обработки, а в противном случае — игнорируем:

onLogRecord ( uint64_t timestamp, uint_t recordCode, void const* p, size_t size ) { if (recordCode == log.StdRecordCode.Rx) try m_rxScanner.write (p, size); } Теперь немного остановимся на механизме обработки входящих данных в эмуляторах и анализаторах протоколов.Коммуникационные протоколы можно условно разделять на категории по различным критериям: уровень протокола в модели OSI, пакетно-ориентированные (message-oriented protocols) и потоково-ориентированные (stream-oriented-protocols), бинарные и текстовые и т.д.

С точки зрения написания анализатора протокола наибольшее значение представляют два фактора:

полагается ли протокол на доставку сообщения как целого (или же требуется предварительное накопление); используются ли фиксированные заголовки (или же запросы и ответы имеют различную длину, и требуется разбор языка запросов/ответов неким парсером). Например, протоколы стека TCP/IP вплоть до транспортного уровня включительно полагаются на доставку сообщений как целого и используют фиксированные заголовки, а на уровнях выше возможны варианты: скажем, DHCP полагается на доставку как целого и использует заголовки, а HTTP работает с потоком и использует текстовые запросы/ответы.В случае, когда используются фиксированные заголовки, протоколы удобнее всего разбирать с помощью поддерживаемого языком Jancy и хорошо знакомого программистам C/C++ механизма структур, указателей и адресной арифметики. Здесь здорово поможет тот факт, что Jancy имеет высокую степень совместимости с C/C++ — это позволяет просто копировать C-определения заголовков протоколов из общедоступных источников и напрямую вставлять их в Jancy-скрипт (иногда не требуется даже косметических изменений).

К сожалению, данный механизм совершенно не подходит для разбора протоколов, относящихся к противоположной категории, а именно, когда вместо прохода по структурам заголовков надо анализировать язык запросов/ответов. В этом случае требуется некий парсер — значит, логично применить методологию, которая используется при построении компиляторов. Справедливости ради надо отметить, что грамматика языка запросов ответов в протоколах, как правило, неизмеримо проще таковой в пусть даже самых примитивных языках программирования.

Итак. Первый этап в каскаде компиляции — это лексический анализ (или токенизация), то есть разбиение на лексемы и превращение входного потока символов в поток токенов. Модуль, выполняющий эту задачу, называется лексер (или сканнер). В отличие от компиляторов, работающих с файлами, написание лексера для потокового протокола осложнено тем, что далеко не всегда легко разбить входной IO поток на юниты компиляции.

В Jancy, как в языке, который специально создавался под использование в IO Ninja, имеется удобное средство для разбора IO потоков. Это встроенный генератор инкрементальных лексических анализаторов. Он работает по принципу известных инструментов типа Lex, Flex, Ragel и т.д. Посмотрим на код, сгенерированный визардом:

bool automaton TiosEmuLayer.scanRx () { %% «about» [\r\n] try transmitString («IO Ninja — Tios Emu\r\n»);

%% «version» [\r\n] try transmitString («Version 1.0.0\r\n»);

%% «help» [\r\n] try transmitString («This is a starting point for your test utility\r\n»); %% . // ignore everything else } Функции-автоматы компилируются в ДКА для распознавания лексем, каждая из которых описана с помощью регулярного выражения. Когда автомат обнаруживает лексему, он выполняет соответствующее ей действие.Саму функцию-автомат вызвать напрямую нельзя — ведь надо где-то хранить состояние автомата (в том числе и накопленную часть лексемы). Для этой цели используется объект класса jnc.Recognizer, а уже он будет неявно вызвать наш автомат, производить необходимые переходы между состояниями, откаты, накопление символов лексемы и т.д.

Вот экземпляр этого класса в сгенерированном плагине:

class TiosEmuLayer: doc.Layer { jnc.Recognizer m_rxScanner;

// … } В качестве понятной всем аналогии можно привести следующий пример. Имеется игровая приставка и имеется диск с игрой. Чтобы поиграть в игру, надо вставить в приставку диск. При этом игру можно будет проходить инкрементально, так как состояние прохождения сохранено на жёстком диске приставки. В данном примере замените приставку на экземпляр класса jnc.Recognizer, диск с игрой — на функцию-автомат, а прохождение игры — на разбор входного потока, и всё встанет на свои места.Но довольно теории, займёмся практикой: давайте добавим нашему плагину распознавание команд TiOS.

Во-первых, TiOS работает через UDP, а значит, инкрементальный разбор с сохранением состояния между полученными сегментами данных не имеет смысла и даже вреден: одна датаграмма — одна команда. Поэтому вместо jnc.Recognizer.write мы будем использовать метод jnc.Recognizer.recognize (который разворачивается в последовательность reset, write, eof)

Далее, так как TiOS-устройство в изначальном состоянии вообще не имеет IP-адреса (он должен быть назначен крутящимся на устройстве приложением — либо из настроек, либо от DHCP-сервера), прямая адресация по IP в общем случае невозможна. Поэтому в TiOS-протоколе используется широковещательный IP-адрес для общения с устройством, а для адресации устройства при широковещательной рассылке используется MAC-адрес в теле пакета.

Примечание MAC-адрес ethernet-фрейма, кстати, не обязан быть широковещательным. Многие современные свичи искусственно ограничивают разрешённую скорость широковещательного трафика, поэтому наш интерактивный кросс-отладчик TIDE использует PCap для генерации UDP-фреймов, в которых IP — широковещательный, а вот MAC — конкретного устройства.

Добавим поле с MAC нашего фиктивного устройства в класс плагина (обратите внимание на hex-литерал — средство языка Jancy удобного задания «захардкоженных» бинарных констант, например, иконок, курсоров, публичных ключей и т.д.): typedef uchar_t MacAddress [6];

class TiosEmuLayer: doc.Layer { MacAddress m_macAddress = 0x»1 2 3 4 5 6»; char const* m_macAddressString = formatMacAddress (m_macAddress);

// … } Напишем парочку хелперов для работы с MAC-адресами — ведь нам надо будет проверять на соответствие MAC-адреса пришедших команд, а также добавлять MAC в ответные пакеты. Исходный код MacAddress parseMacAddress (char const* string) { MacAddress address; char const* p = string; for (size_t i = 0; i < 6; i++) { address [i] = (uchar_t) atoi (p); p = strchr (p, '.'); if (!p) break; p++; } return address; }

char const* formatMacAddress (MacAddress address) { return $»%1.%2.%3.%4.%5.%6» ( address [0], address [1], address [2], address [3], address [4], address [5] ); } Помимо MAC-префикса любой TiOS-пакет может быть завершён суффиксом для идентификации и сопоставления запросов и ответов (некий аналог sequence number/acknowledgment number в TCP) — если запрос содержал такой суффикс, то и ответ на этот запрос должен иметь тот же суффикс.Наш плагин-эмулятор будет поддерживать команды »?» (echo), «X» (get-device-status), «PC» (get-vm-status), «B» (buzz) — этого достаточно, чтобы его было видно из Tibbo Device Explorer. Первая команда используется для авто-обнаружения устройств в локальном сегменте, вторая и третья команда возвращают расширенный статус устройства и виртуальной машины.

Итак, ваяем анализатор пакетов с использованием функций-автоматов.

TiosEmuLayer.onLogRecord ( uint64_t timestamp, uint_t recordCode, void const* p, size_t size ) { if (recordCode!= log.StdRecordCode.Rx) return; bool result = try m_packetScanner.recognize (scanPacket, p, size); if (! result) m_reply = «C»; else if (! m_reply) return; char const* id = strchr (p, '|'); // find id — if there’s any try transmitString ($»[$m_macAddressString]$m_reply$id»); }

jnc.AutomatonResult automaton TiosEmuLayer.scanPacket (jnc.Recognizer* recognizer) { %% »_?» m_reply = «A»; %% »_[» (\d+ '.')* \d+ ']' MacAddress address = parseMacAddress (recognizer.m_lexeme + 2); if (memcmp (address, m_macAddress, sizeof (address)) != 0) { m_reply = null; return jnc.AutomatonResult.Stop; // don’t scan further } recognizer.m_automatonFunc = scanCommand; // change automaton } Функция onLogRecord передаёт RX данные для анализа в функцию-автомат scanPacket и после этого шлёт ответный пакет (или молча завершает работу, если ответ не нужен). Для формирования ответных пакетов используются форматирующие литералы языка Jancy (Perl-подобное форматирование).Автомат scanPacket демонстрирует крайне полезную возможность разбора мультиязыковых входных данных (аналог fgoto/fcall для смены машин в Ragel). После разбора и проверки первой части пакета, содержащей MAC-префикс, мы «прыгаем» на второй автомат, scanCommand, для разбора тела пакета с собственно командой и идентификационным суффиксом:

jnc.AutomatonResult automaton TiosEmuLayer.scanCommand (jnc.Recognizer* recognizer) { %% id = '|' .* %% 'X' id? m_reply = «A/ec8ec901-bb4b-4468-bfb9-bf482589cc17/test!»;

%% «PC» id? m_reply = «A*R*/00/&h A:0000, B:0000, PC:00000000, SP:00, FL: Z**/65535»;

%% 'B' id? m_reply = «A»; } Второй автомат демонстрирует возможность определения именованных регулярных выражений для их последующего использования. Отметим, что стандартные классы символов типа пробелов, десятичных цифр и т.д. не нуждаются в специальных определениях и доступны, как и в Perl, через escape-последовательности \d, \D, s, \S, \w, \W.Запускаем наш плагин на несущей UDP-сессии, выбираем адаптер «All IPv4 Adapters» и открываем порт 65535. Мы хотим, чтобы наш плагин овтечал на тот адрес, с которого пришёл запрос — для этого устанавливаем флажок Auto-switch Remote Address (кнопочка с компасом). Теперь запускаем Device Explorer и видим фиктивное устройство, порождённое нашим эмулятором. Фанфары, салют, туш.

image

Полный текст плагина можно скачать здесь: http://tibbo.com/downloads/open/ioninja-plugin-samples/TiosEmu.zip

Разумеется, на выходе у нас получился не полноценный эмулятор TiOS, а скорее заготовка-пустышка. Тем не менее, этого должно быть вполне достаточно для демонстрации самой концепции и для понимания того, в каком направлении копать дальше — этот эмулятор вполне может служить отправной точкой для написания какой-то более полезной тестировочной утилиты в вашем конкретном случае.В следующей части статьи, мы рассмотрим создание плагина-слоя для анализа протокола.

Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.

© Habrahabr.ru