Всем привет. Эксперименты с эмуляцией сети продолжаются. В этот раз, как и обещал, будем делать вид, что в нашей виртуальной сети завелась машина с честным snmp-агентом.
SNMP — довольно старый протокол и знаком каждому сисадмину. На этот протокол в своё время возлагали весьма большие надежды, но в последнее время его использование сильно ограничено — как правило, это чтение переменных стандартных mib-ов на железках, не имеющих нормальных операционок (читай linux, ios и т.д.). А вот с хостов, соответственно — под управлением нормальных операционных систем, предпочитают забирать информацию с помощью cli или агентов на python/perl/bash, которые могут залезть в интимные места файловой системы /proc, парсить логи, запускать вспомогательные процессы и отдавать результаты в json/xml в неограниченных объёмах по защищённым ssl каналам.
С точки зрения системного администратора протокол действительно Simple. Всего-то пара типов запросов — get/getnext да оперативное информирование — trap. Есть ещё set, но про него в основном все знают только в теории, т.к. из-за слабой безопасности практиковать даже не пытаются. Ну и вишенка на торте — стандартные mib немного не успевают за жизнью, а в .enterprises уже никто не хочет ковыряться, даже производители.
Однако, несмотря на свои недостатки, данный протокол ещё остаётся безотказной «рабочей лошадкой» для большинства систем мониторинга. Да, стандартные MIB не позволяют в полной мере отразить топологию сети содержащей различные криптошлюзы, туннели, виртуальные роутеры, асимметричные маршруты и т.д., но базовую структуру сети собрать можно даже на коленке — утилитами командной строки snmpget и snmpwalk. Также некоторым достоинством является использование протокола UDP и кодирование ASN1 позволяющие передать всю необходимую информацию в объёме одного крохотного пакета без установки сессии. Плюс реализация snmp-агента может быть весьма небольшой по размеру и встраиваться в системы с очень ограниченными ресурсами.
Разумеется, вышеприведённой информации недостаточно, чтобы создать рукотворный «мираж» в виде фантомного хоста сети, который будет корректно отзываться на snmp-запросы. Попытаемся погрузиться в теорию — сначала лайтовенько «SNMP: Simple? Network Management Protocol», потом чуток поглубже «ASN.1 простыми словами». Мы уже в полушаге от создания собственной реализации net-snmp. Впрочем, кого я обманываю? В первой части я уже написал, что использовал библиотеку csnmp :), значит, идём на гитхаб и берём её там. Но статьи всё же прочитайте.
▍ Подготовка
Достаём из архивов исходники предыдущей части, будем их дописывать.
Скачиваем csnmp (Copyright © 2019 Nikifor Seryakov) и распаковываем рядом с каталогом, где ведутся эксперименты. Для нашего проекта будут задействованы
asn1.h, asn1.c, snmp.h, snmp.c
. Возникает вопрос — почему не скопировать их в наш каталог, как мы поступили с LaBrea, и таким образом облегчить объём исходников? Дело в том, что все исходные файлы LaBrea в самом начале имеют обширнейшие комментарии с реквизитами и ссылками на создателей, лицензионные ограничения и т.д., а вот csnmp прекрасно обходится без всего вот этого :). Единственным источником идентификации автора и правил использования является файл LICENSE. Поэтому я и предлагаю сохранить исходники csnmp в полном объёме.Компиляция немного меняется:
$ gcc -o netemu -ldnet -lpcap netemu.c pkt.c bget.c ../csnmp/asn1.c ../csnmp/snmp.c
И кажется пора рисовать Makefile…
▍ Обрабатываем snmpget
В данном примере будет показан самый минимум — ответ на SNMP_GET по OID system.sysDescr.0. Для существенного облегчения задачи будем использовать протокол SNMPv1 UDP/161. Безопасность и раньше в snmp была не очень, а здесь я даже не смотрю в поле community :). Конечно, данную проверку прикрутить несложно, но для демонстрации работы с пакетами snmp это избыточно.
В начало файла добавляем ссылки на заголовочные файлы, пути должны соответствовать реальному размещению исходников csnmp:
#include "../csnmp/asn1.h"
#include "../csnmp/snmp.h"
Немного корректируем функцию ip_handler — находим в области обработки протокола ICMP объявление
struct addr a;
и переносим в самое начало функции. Это необходимочтобы мы могли воспользоваться этой переменной при обработке пакета UDP.
Там, где мы выводим заголовки пакета UDP, добавляем следующий код:
addr_aton("10.0.0.5", &a);
if ((pkt->pkt_ip->ip_dst == a.addr_ip)
&& (ntohs(pkt->pkt_udp->uh_dport) == 161)) send_snmp_reply(pkt);
Здесь я думаю всё понятно — по прилёту пакета snmp (udp/161) будем отзываться, только если в качестве приёмника там фигурирует адрес
10.0.0.5
.Готовим функцию send_snmp_reply. Но предварительно необходимо объявить пару функций из csnmp. Дело в том, что мы не планируем отправку пакетов средствами csnmp, нам от неё необходим только разбор пакетов, манипуляции с переменными и формирование пакета в буфер. И как раз работа с буфером скрыта в недрах snmp.c и не задекларирована в snmp.h. Вносить какие-либо изменения в snmp.h не будем, а просто объявим их у себя:
extern int snmp_dec_pdu(const char *buf, int buf_len, snmp_pdu_t *p);
extern int snmp_enc_pdu(char **buf, int *i, int *buf_len, snmp_pdu_t *p);
void send_snmp_reply(struct pkt *pkt)
{
snmp_pdu_t p = {};
snmp_dec_pdu(pkt->pkt_udp_data, ntohs(pkt->pkt_udp->uh_ulen) - UDP_HDR_LEN, &p);
/* show snmp request */
snmp_dump_pdu(NULL, &p);
snmp_var_t *v;
switch (p.command) {
case SNMP_CMD_GET:
p.command = SNMP_CMD_RESPONSE;
for (int i = 0; i < p.vars_len; i++) {
v = &p.vars[i];
snmp_free_var_value(v);
if (asn1_cmp_oids(v->oid, asn1_crt_oid((int[9]){1,3,6,1,2,1,1,1,0}, 9)) == 0) {
v->type = SNMP_TP_OCT_STR;
v->value = asn1_new_str("APC Web/SNMP Management Card", 0);
}
else
v->type = SNMP_TP_NO_SUCH_OBJ;
}
break;
case SNMP_CMD_GET_NEXT:
break;
default:
break;
}
if (p.command == SNMP_CMD_RESPONSE) {
p.error = (asn1_error_t){0};
int buf_len = 20 * (1<<10);
char *buf = malloc(buf_len);
int pdu_len = 0;
snmp_enc_pdu(&buf, &pdu_len, &buf_len, &p);
/* show snmp reply */
snmp_dump_pdu(NULL, &p);
struct pkt *new = NULL;
if ((new = pkt_new()) == NULL) return;
eth_pack_hdr(new->pkt_eth,
pkt->pkt_eth->eth_src, /* orig src MAC becomes new dest MAC */
io.mymac, /* my own mac becomes new src MAC */
ETH_TYPE_IP);
ip_pack_hdr(new->pkt_ip,
0, /* tos */
(IP_HDR_LEN + UDP_HDR_LEN + pdu_len), /* IP hdr length */
rand_uint16( io.rnd ), /* ipid */
0, /* frag offset */
IP_TTL_DEFAULT,
IP_PROTO_UDP, /* ip protocol of original pkt */
pkt->pkt_ip->ip_dst, /* orig dst becomes new src addr */
pkt->pkt_ip->ip_src);
new->pkt_udp_data = (u_char *)(new->pkt_ip_data + UDP_HDR_LEN);
new->pkt_end = (u_char *)new->pkt_eth_data + ntohs(new->pkt_ip->ip_len);
udp_pack_hdr(new->pkt_udp,
htons(pkt->pkt_udp->uh_dport),
htons(pkt->pkt_udp->uh_sport),
UDP_HDR_LEN + pdu_len);
memcpy(new->pkt_udp_data, buf, pdu_len);
free(buf);
ip_checksum(new->pkt_ip, new->pkt_end - new->pkt_eth_data);
int ret_code = eth_send(io.eth, new->pkt_eth, new->pkt_end - (u_char *)new->pkt_eth);
if (ret_code < 0)
printf("*** Problem sending packet\n");
}
snmp_free_pdu_vars(&p);
snmp_free_pdu(&p);
}
Какая длинная функция получилась, попробую объяснить (если захотите, нарубите её на кусочки самостоятельно).
Вначале объявляем переменную
p
типа snmp_pdu_t
и парсим в неё информацию из полученного пакета. Делаем это как раз функцией snmp_dec_pdu
спрятанной от пользователей csnmp.Далее на консоль показываем — что именно мы получили и приступаем непосредственно к формированию ответа. Ответ мы будем формировать корректируя уже готовую переменную p — почти также, как мы ранее обращались с пакетом icmp echo request.
Условный оператор switch на основании значений
p.command
раскидывает логику обработки запроса по нескольким веткам. Допустимые варианты данном контексте: SNMP_CMD_GET, SNMP_CMD_GET_NEXT, SNMP_CMD_SET, SNMP_CMD_GET_BULK
. Мы пока что реализуем SNMP_CMD_GET
, а также оставим заготовку для SNMP_CMD_GET_NEXT
— всё остальное идёт в default
.Как уже писал выше, сначала сменим тип snmp-пакета — он теперь должен стать
SNMP_CMD_RESPONSE
. Далее в цикле пробежимся по переменным в данном запросе, почистим их значения, и если запрашиваемый oid равен .1.3.6.1.2.1.1.1.0
(system.sysDescr.0) то готовим ответ типа Octet String. Как видно исходники — это всё вставляется в поля type
и value
.Значения
OID
, их типы, описания можно посмотреть в вашей локальной системе (/usr/share/snmp/mibs/SNMPv2-MIB.txt
) или поискать в интернете по ключам: «SNMPv2 MIB», «RFC 3418:12/2002».Цикл с пробежкой по переменным я честно скопипастил из демо-кода csnmp. На мой взгляд, было бы достаточно поработать только с переменной имеющей индекс 0, но, кажется, автор csnmp более продвинут в этом вопросе, поэтому пока оставил так.
Сама переменная типа
snmp_var_t
это структура из 3-х полей: oid
— структура типа snmp_oid_t
(точнее, ans1_oid_t
) хранящая «имя» snmp-переменной; type
— целое число, определяющее тип значения; value
— указатель на само значение, т.е. адрес в памяти, где оно располагается.В случае если желаемый
oid
не совпал с нашими возможностями, мы просто ставим тип переменной SNMP_TP_NO_SUCH_OBJ
и следуем к формированию пакета и возврату его обратно.Отправку пакета завернул в условие
if (p.command == SNMP_CMD_RESPONSE)
. Это логично и правильно — если мы не заинтересовались пакетом, то не сменили его тип на ответ, а следовательно — и отвечать на него не считаем нужным.Формирование пакета также производим с помощью «скрытой» функции
snmp_enc_pdu
. Далее кропотливая сборка пакета, вычисление размеров на каждом уровне и отправка — всё это было описано в предыдущей статье, здесь только отличие в протоколе UDP.Ну и в финале мы очищаем переменные — если глянуть декларацию
asn1_new_str
(мы с её помощью формировали значение для отправки ответа), то становится понятно, что внутри этой функции выделяется память, которую нужно в итоге освободить и, кстати, в следующем разделе это будет видно более явно. Также необходимо очистить и саму pdu — там тоже достаточно компонентов с динамически выделяемой памятью.Код написан, пора пробовать.
Отлично! Всё работает как и ожидалось. Было точное совпадение OID — получите искомое, не совпало — ну нет, значит нет. Переходим на следующий этап.
▍ Обрабатываем snmpgetnext
Обработка запросов типа snmpgetnext будет также вписана в нашу мегафункцию
void send_snmp_reply(struct pkt *pkt)
— помнится, для этого там было зарезервировано местечко.Вставляем:
case SNMP_CMD_GET_NEXT:
p.command = SNMP_CMD_RESPONSE;
v = &p.vars[0];
// спрашивают, что у нас идёт за system.sysDescr.0
if (asn1_cmp_oids(v->oid, asn1_crt_oid((int[9]){1,3,6,1,2,1,1,1,0}, 9)) == 0) {
snmp_free_var(v);
// возвращаем system.sysObjectID.0
v->oid = asn1_crt_oid((int[9]){1,3,6,1,2,1,1,2,0}, 9);
v->type = SNMP_TP_OID;
v->value = asn1_new_oid((int[10]){1,3,6,1,4,1,318,1,3,7}, 10);
}
// запрос system.sysObjectID.0
else if (asn1_cmp_oids(v->oid, asn1_crt_oid((int[9]){1,3,6,1,2,1,1,2,0}, 9)) == 0) {
snmp_free_var(v);
// возвращаем system.sysUpTime.0
v->oid = asn1_crt_oid((int[9]){1,3,6,1,2,1,1,3,0}, 9);
v->type = SNMP_TP_TIMETICKS;
v->value = malloc(sizeof(int));
*(int *)v->value = ((((5*24) + 11)*60 + 35)*60 + 24)*100 + 22; // 5 days, 11:35:24.22
}
// запрос system.sysUpTime.0
else if (asn1_cmp_oids(v->oid, asn1_crt_oid((int[9]){1,3,6,1,2,1,1,3,0}, 9)) == 0) {
snmp_free_var(v);
// возвращаем system.sysContact.0
v->oid = asn1_crt_oid((int[9]){1,3,6,1,2,1,1,4,0}, 9);
v->type = SNMP_TP_OCT_STR;
v->value = asn1_new_str("Comparitech", 0);
}
// запрос system.sysContact.0
else if (asn1_cmp_oids(v->oid, asn1_crt_oid((int[9]){1,3,6,1,2,1,1,4,0}, 9)) == 0) {
snmp_free_var(v);
// возвращаем system.sysName.0
v->oid = asn1_crt_oid((int[9]){1,3,6,1,2,1,1,5,0}, 9);
v->type = SNMP_TP_OCT_STR;
v->value = asn1_new_str("APC-3425", 0);
}
// запрос system.sysName.0
else if (asn1_cmp_oids(v->oid, asn1_crt_oid((int[9]){1,3,6,1,2,1,1,5,0}, 9)) == 0) {
snmp_free_var(v);
// возвращаем system.sysLocation.0
v->oid = asn1_crt_oid((int[9]){1,3,6,1,2,1,1,6,0}, 9);
v->type = SNMP_TP_OCT_STR;
v->value = asn1_new_str("3425EDISON", 0);
}
// запрос system.sysLocation.0
else if (asn1_cmp_oids(v->oid, asn1_crt_oid((int[9]){1,3,6,1,2,1,1,6,0}, 9)) == 0) {
snmp_free_var(v);
// возвращаем system.sysServices.0
v->oid = asn1_crt_oid((int[9]){1,3,6,1,2,1,1,7,0}, 9);
v->type = SNMP_TP_INT;
v->value = malloc(sizeof(int));
*(int *)v->value = 72;
}
// запрос system.sysServices.0
else if (asn1_cmp_oids(v->oid, asn1_crt_oid((int[9]){1,3,6,1,2,1,1,7,0}, 9)) == 0) {
snmp_free_var(v);
// возвращаем inerfaces.ifTable.ifEntry.ifIndex.1
v->oid = asn1_crt_oid((int[9]){1,3,6,1,2,1,2,1,0}, 9);
v->type = SNMP_TP_INT;
v->value = malloc(sizeof(int));
*(int *)v->value = 1;
}
// запрос что-нибудь начинающееся с system
else if (asn1_oid_has_prefix(v->oid, asn1_crt_oid((int[7]){1,3,6,1,2,1,1}, 7))) {
snmp_free_var(v);
// возвращаем system.sysDescr.0
v->oid = asn1_crt_oid((int[9]){1,3,6,1,2,1,1,1,0}, 9);
v->type = SNMP_TP_OCT_STR;
v->value = asn1_new_str("APC Web/SNMP Management Card", 0);
}
else
v->type = SNMP_TP_NO_SUCH_OBJ;
break;
default:
Пробежимся по коду. С ходу меняем значение
p.command
на SNMP_CMD_RESPONSE
— это уже знакомо. Далее берём только значение переменной с индексом 0 — решил тут сильно не загромождать код, думаю для тестового прототипа это допустимо.Ну а дальше муторная и монотонная (конкретно в данной демонстрашке — разумеется, боевой код так писать нельзя) работа по сверке запрошенного
oid
и подготовке ответа: освобождение памяти от переменной, формирование oid
следующего элемента, его тип и значение. Тут хорошо видно как обрабатывать значения различных типов, а также фигурирует более явное выделение памяти посредством malloc
.На освобождение памяти хочу обратить особое внимание. Если помните, в прошлый раз чистили только значения (
snmp_free_var_value
), а сейчас переменную целиком (snmp_free_var
). Всё дело в протоколе snmp — при вызове snmpget мы просим дать значение конкретного oid
, т.е. он не меняется, а при snmpgetnext нужно дать значение следующего элемента. Соответственно, в ответе у нас будет изменены не только тип и значение, но также будет совсем другой oid
. Именно это и требует от нас тотальной зачистки.Также особого внимания заслуживают два последних условия:
Когда мы обрабатываем последний
oid
на нашем уровне, в данном случае .1.3.6.1.2.1.1.7.0
, мы не возвращаем в качестве следующего элемента SNMP_TP_NO_SUCH_OBJ
, что кажется вполне логичным, а возвращаем первый элемент из соседней веточки. На самом деле мы с помощью snmpwalk можем пройтись по любому уровню дерева snmp как ближе к корню, так и почти на конце какой-либо ветки — он не будет выходить за пределы запроса — при получении ответа с oid
вне запрошенного уровня он завершает опрос. А вот если бы мы вернули SNMP_TP_NO_SUCH_OBJ
, то запрос snmpwalk с более высокого уровня прервался на нашей веточке и не пробежался по соседним. Также этот oid
должен вернуться при обращении к oid большим чем .1.3.6.1.2.1.1.7.0
В последнем условии производится сравнение запроса только на префикс. Это тоже особое поведение snmp-демона — при обращении в начальные области нашей ветки должен вернуться
oid
первого элемента — это .1.3.6.1.2.1.1.1.0
. Другими словами, все запросы к system(.1.3.6.1.2.1.1)
или system.sysDescr(.1.3.6.1.2.1.1.1)
должны вернуть нам ссылку и значение system.sysDescr.0(.1.3.6.1.2.1.1.1.0)
, а вот обращение реально существующей system.sysDescr.0(.1.3.6.1.2.1.1.1.0)
вернёт следующий элемент — system.SysObjectID.0(.1.3.6.1.2.1.1.2.0)
Приступаем к тестированию:
Длинный вывод я подрезал, но уже видно, что snmpwalk ничего не заподозрил!
Следующая проверка
Результат, на первый взгляд, не совсем корректный, однако если сравнить
oid
запроса и oid
элемента, который был возвращён в последний раз, то становится понятно — элемент вышел за уровень запроса и snmpwalk остальное перестало интересовать.Ну а напоследок один каверзный запрос:
Внимательно изучаем отладочный вывод: snmpwalk умный парень — хоть и получил сразу ответ идти в соседнюю ветку, но на этом не успокоился, а произвёл контрольный вопрос через snmpget — так есть кто живой с этим
oid
или нет? Кстати, если сейчас попробуете на других oid
, то получите совсем другой результат, это потому что в первой части статьи написана обработка snmpget только для system.sysDescr.0
:)▍ Финал
На этом достаточно знакомства с богатым внутренним миром такого «простого» протокола как SNMP. А для решения моей основной задачи осталось совсем немного — определится с различными структурами хранения oid и их значений, привязка к базе ip-адресов, сделать чтобы всё это извлекалось и обрабатывалось в разумные временные рамки. Это реализуется простыми и понятными алгоритмами — хэши/деревья/ключи-значения и пр. но уже не в рамках данной статьи.
Всем приятных pet-проектов :)