Удаленный мониторинг и управление устройств на базе Linux/OpenWrt/Lede через 80-ый порт…

Удаленный мониторинг и управление устройств на базе Linux/OpenWrt/Lede через 80-ый порт…

Это дополняемая копия моей статьи на Хабре:
Часть 1
Часть 2

Как нестандартно управлять сетевым оборудованием во внешней сети.

Что значит нестандартно: в большинстве случаев, для управления оборудованием во внешней сети Вам необходимо:

1) Публичный IP-адрес. Ну, или если оборудование находится за чьим-то NAT-ом, то публичный IP и «проброшенный» порт.
2) Туннель (PPTP/OpenVPN/L2TP+IPSec и т.д.) до центрального узла, через который была бы доступна.

Поэтому «мой велосипед» потребуется Вам, когда стандартные методы Вам не подходят, например:
1) Оборудование находится за NAT-ом и кроме обычного http (80-го порта) — все закрыто. Вполне нормальная ситуация для крупных федеральных корпоративных сетей. Прописать порты — могут, но не сразу, не быстро и не Вам.
2) Нестабильный и/или «узкий» канал связи. Маленькая скорость, постоянные потери. Боль и разочарование при попытке организовать туннель.
3) Дорогой канал связи, где буквально каждый мегабайт на счету. Например спутниковая связь. Плюс большие задержки и «узкая» полоса.
4) Ситуация, когда Вам надо «жонглировать» большим количеством маленьких роутеров, на которых с одной стороны установлена OpenWrt/Lede для расширения возможностей, а с другой стороны ресурсов (памяти) роутера хватает далеко не на все.

Примечание номер раз: А что мешает в USB-порт роутера установить «флэшку» и расширить память роутера ?
Чаще всего требования к стоимости решения в целом, но иногда ключевую роль играет и форм-фактор. Например, на объекте стоит TP-Link ML3020, его единственный USB-порт используется под 2G/3G модем, все это завернуто в какой-нить небольшой пластиковый корпус и размещено где-то высоко-высоко (на мачте), далеко-далеко (в поле, в 30 км. от ближайшей базовой станции мобильного оператора). Да, можно воткнуть USB-hub и расширить число портов, но опыт показывает что это громоздко и ненадежно.

Итак, я постарался описать Вам мою типовую ситуацию: «где-то далеко-далеко, стоит очень важный, одинокий и маленький роутер под управлением Linux. Важно знать хотя бы раз в день, что он «жив» и при необходимости отсылась ему команды, например «солнышко, перезагрузись!»

Перейдем к реализации:
1) На стороне роутера по cron-у каждые 5/10/1440 минут, или когда угодно необходимо отсылать http-запрос на сервер с помощью wget, результат запроса сохранять в файл, файл делать исполняемым, и исполнять его.
У меня строчка в cron-е выглядит примерно так:

Файл /etc/crontabs/root:
*/5 * * * * wget "http://xn--80abgfbdwanb2akugdrd3a2e5gsbj.xn--p1ai/a.php?u=user&p=password" -O /tmp/wa.sh && chmod 777 /tmp/wa.sh && /tmp/wa.sh

,где:
xn--80abgfbdwanb2akugdrd3a2e5gsbj.xn--p1ai — домен моего сервера. Сразу замечу: да, можно указать и конкретный ip-адрес сервера, мы так раньше делали, пока наше государство, в праведном порыве борьбы нескажусчемнезнаю — не закрыло доступ к львиной доле «облаков» DigitalOcean и Amazon. В случае использования символьного домена, при возникновении подобного казуса, вы спокойно сможете поднять резервное облако, перенаправить на него домен и восстановить мониторинг устройств.
a.php — имя скрипта на стороне сервера. Да, я знаю, что это неправильно, называть переменные и имена файлов одной буквой…предлагаю считать, что так мы экономим несколько байт при отправке запроса 🙂
u — имя пользователя, логин железки
p — пароль
"-O /tmp/wa.sh" — файл на удаленном роутере, куда будет сохранятся ответ сервера, например команда reboot.

Сразу примечание: Аааа, почему мы используем wget, а не curl, ведь через curl можно отправлять https запросы и не GET-ом, а POST-ом. Аааа потому, что как в старом анекдоте «В крынку нЭ лезет!». В состав curl входят библиотеки шифрования размером около 2МБ и в силу этого врятли Вам удастся собрать образ для маленького TP-LINK ML3020 к примеру. А с wget — пожалуйста.

2) На стороне сервера (у меня это Ubuntu) мы будем использовать Zabbix. Почему: хочу чтобы было красиво (с графиками) и удобно (отправлять команды через контекстное меню). У Заббикса есть такая прелестная вещь, как zabbix-агент. Через агента мы будем вызывать php-скрипт на сервере, который будет возвращать информацию о том, регистрировался ли наш роутер в требуемый период времени. Для хранения информации о времени регистрации, командах для устройств, я использую MySQL, отдельную таблицу users примерно с такими полями:

CREATE TABLE `users` (
`id` varchar(25) NOT NULL,
`passwd` varchar(25) NOT NULL,
`description` varchar(150) NOT NULL,
`category` varchar(30) NOT NULL,
`status` varchar(10) NOT NULL,
`last_time` varchar(20) NOT NULL, // время последнего соединения
`last_ip` varchar(20) NOT NULL, // IP последнего соединения
`last_port` int(11) NOT NULL, // порт последнего соединения
`task` text NOT NULL, // задача которую получает роутер
`reg_task` varchar(150) NOT NULL, // "регулярная" задача, если мы захотим чтобы задача выполнялась всегда при регистрации
`last_task` text NOT NULL, // лог задач
`response` text NOT NULL, // сюда пишется ответ устройства
`seq` int(11) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

Все исходники можно забрать с Git-репозитория, по адресу: https://github.com/BazDen/iotnet.online.git
Теперь PHP-скрипты, размещаемые на стороне сервера(для удобства их можно положить в папку /usr/share/zabbix/):

Файл a.php:

// здесь ищем наш роутер в таблице базы данных
$sql_users=$conn->prepare("SELECT task, reg_task, response, last_time FROM users WHERE id=? AND passwd=? AND status='active';");
$sql_users->bind_param('ss', $user, $password);
$sql_users->bind_result($task, $reg_task, $response, $last_time);
$sql_users->execute();
$sql_users->store_result();


if (($sql_users->num_rows)==1){
$sql_users->fetch();
// здесь мы роутеру отправляем его задачи
echo $task;
echo "\n";
echo $reg_task;
// вот здесь мы пишем время ответа и сам ответ роутера
$response_history="[".date("Y-m-d H:i")."] ".$message;
// задачу отправили, теперь надо ее удалить,а после удаления отметить в логах, что такая-то задача выполнена
$last_ip=$_SERVER["REMOTE_ADDR"];
$last_port=$_SERVER["REMOTE_PORT"];
$ts_last_conn_time=$last_time;
$sql_users=$conn->prepare("UPDATE users SET task='', seq=1 WHERE (id=?);");
$sql_users->bind_param('s', $user);
$sql_users->execute();
if (strlen($message)>1){
$sql_users=$conn->prepare("UPDATE users SET response=?, seq=1 WHERE (id=?);");
$sql_users->bind_param('ss', $response_history, $user);
$sql_users->execute();
}
// теперь надо сохранить время регистрации пользователя, его айпи и сообщение от него. Пока только сообщение
$ts_now=time();
$sql_users=$conn->prepare("UPDATE users SET last_time=?, last_ip=?, last_port=? WHERE (id=?);");
$sql_users->bind_param('ssss', $ts_now, $last_ip, $last_port, $user);
$sql_users->execute();
}
// если мы не нашли роутер в нашей базе данных, или его статус "неактивный", то ему ... будет отправлена команда reboot....
// Почему так жестоко ? Потому что роутеры иногда пропадают, а это маленький способ проучить новых владельцев.
else
{
echo "reboot";
}
$sql_users->close();
?>

Файл agent.php (это скрипт вызываемого zabbix-агента):
$sql_users=$conn->prepare("SELECT seq FROM users WHERE id=? AND passwd=? AND status='active';");
$sql_users->bind_param('ss', $user, $password);
$sql_users->bind_result($seq);
$sql_users->execute();
$sql_users->store_result();


// обмен данными происходит через поле seq. При регистрации железка ставит данное поле в "1"
if (($sql_users->num_rows)==1){
$sql_users->fetch();
echo $seq;
}


// обнуляем $seq.
$sql_users=$conn->prepare("UPDATE users SET seq=0 WHERE id=? AND passwd=? AND status='active';");
$sql_users->bind_param('ss', $user, $password);
$sql_users->execute();
$sql_users->close();
?>

Ну и заключительный этап: прописание агента и добавление графиков.

Если у Вас еще не установлен zabbix-агент, то:
apt-get install zabbix-agent

Редактируем файл /etc/zabbix/zabbix_agentd.conf:
Добавляем строку:
UserParameter=test,php /usr/share/zabbix/agent.php user password

,где:
test — имя нашего агента
"php /usr/share/zabbix/agent.php user password" — вызываемый скрипт с указанием регистрационных данных устройства.

Добавление графиков: открываем web-интерфейс zabbix, в меню выбираем:
Настройка -> Узлы сети -> Создать узел сети. Здесь достаточно указать имя узла сети, его группу, интерфейс агента по умолчанию

Теперь нам для данного узла сети надо добавить элемент данных. Обратите внимание на два поля: «ключ» — это как раз тот параметр, что мы прописывали в файле /etc/zabbix/zabbix_agentd.conf (в нашем случае это test), и «интервал обновления» — я ставлю 5 минут, потому как и оборудование регистрируется на сервере тоже один раз в пять минут.

Ну и добавляем график. Рекомендую в качестве стиля отрисовки выбрать «Заполнение».

На выходе получается нечто очень лаконичное, например вот так:

На резонный вопрос: «и это того стоило ?», отвечу: ну конечно, смотрите «причины создания велосипеда» в начале статьи.
В дискуссиях с «технарями» со стороны Заказчика я часто встречаюсь с ограниченным восприятием возможностей таких маленьких устройств (с невысокими ресурсами памяти и производительностью), многие считают что «максимум что нам потребуется это отправить reboot, для чего-то более серьезного — отправим бригаду».
Но практика показывает, что это не совсем так. Вот небольшой перечень частых типичных задач:
1) Сетевая диагностика и устранение. За ethernet-портом Вашего роутера обычно «живет» другая железка, у которой свой внутренний ip-адрес. Иногда, ее можно(нужно) «попингать». Или управление туннелем — если на роутере, работающем через 3G-модем вдруг не поднимается туннель, но сам роутер мы видим.
2) Системное обслуживание. Обновление прошивки, апгрейд служебных скриптов.
3) Эквилибристика. Это можно было бы назвать «извращениями», но понятие «эквилибристка» как, цитирую, «способность циркового артиста удерживать равновесие при неустойчивом положении тела» — подходит больше. Подобные ситуации возникают по причине ограниченности бюджета заказчика.

Вот пара примеров:

Wi-Fi мониторинг. Модная последние пять лет тема в основном среди федеральных сетей ритейла. Вы неспешно прогуливаетесь по торговым залам, а Ваш мобильник с включенным Wi-Fi в попытках «присосатся» к какой-нить сети регулярно рассылает пакеты Probe Request, которые можно анализировать, с целью Вас посчитать: как часто Вы приходите в этот магазин, по каким траекториям гуляете и так далее. Дальше данные собираются, анализируются, рисуются тепловые карты и менеджеры за такие картинки «выбивают» деньги у руководства или инвесторов. Ну а пока….»денег нет, но вы держитесь…», а результат(реальный) уже надо показать, включается старая добрая песня «Да-да, потом мы конечно поставим циски и все что пожелаете, но сейчас надо показать Заказчику результат! Кстати забыли сказать, Заказчик разрешил наше оборудование подключать к своему хотспоту через вайфай, но на общих началах, просто как будто мы гостевые клиенты». И вот приходится делать роутеров-эквилибристов — поднимается несколько WiFi сабинтерфейсов, одним из которых он цепляется за хотспот, а вторым мониторит окружающую среду, судорожно выгружает результат tcpdump-а в себя же, далее содержимое файла пакует в архив и рискуя помереть от «переедания» пытается выплюнуть содержимое на фтп-сервер. Неудивительно, что роутер-эквилибрист часто «срывается» и его как-то приходится удаленно «реанимировать».
Radius. Здесь описать ситуацию проще: «Мы хотим децентрализованную сеть хотспотов, которые бы работали на оборудовании модель которого заранее не известна, через каналы, но какие мы еще не знаем. Ах, забыли сказать, мы не только хотим показывать рекламу клиентам, но и анализировать все вокруг места установки хотспота. Нет, мы пока не знаем зачем, но придумаем, не сомневайтесь, мы же смогли придумать эту идею»

И надо не забывать, что в силу массу неопределенных заранее обстоятельств, управление должно осуществлять в нестандартных условиях, когда мы не можем подключится к роутеру напрямую через ip:порт и вынуждены просто ждать проявления активности от него. Если абстрагироваться, то диалог между сервером и роутером можно представить вот так:
Роутер: привет. я роутер такой-то, есть ли для меня задачи ?
Сервер: роутер такой-то я тебя зарегистрировал, что ты живой. Вот задача: покажи мне результат команды ifconfig ?
Роутер: привет. я роутер такой-то, в прошлый раз ты просил показать результат ifconfig, вот он. Есть ли для меня задачи ?
Сервер: роутер такой-то я тебя зарегистрировал, что ты живой. Задач для тебя нет.

Самый интересный вопрос: а каким образом удаленный роутер может прислать определенный объем информации ? В прошлой части я описывал, что на роутере из-за ограниченности ресурсов стоит только «урезанный» wget, который работает только через GET и ничего больше, нет ни фтп-клиента, ни curl. Точнее, нам нужен универсальный способ, независимо от особенностей сборки образа. Я остановился на использовании wget. Точнее, ну как «остановился» — у меня просто не было выбора 🙂

Сразу оговорка. Мое решение по управлению рабочее, ни сильно ограниченное и я уверен — кривое, даже если оно устраивает большинство моих заказчиков. Как БЫ можно было сделать по уму — написать небольшую утилиту, которая через 80-ый порт шлет POST-ом бинарные данные. Включить ее(утилиту) в состав прошивки роутера и уже с помощью bash-а обращаться к ней. Но реальность такова, что: а) надо быстро б) возможно надо делать все на существующем «зоопарке роутеров» в) «не навреди!» — если роутер работает и выполняет другие задачи, старайся вносить изменения, которые на затронут существующий функционал.

Перейдем к реализации. Допустим ваш заказчик хочет из заббикса, перезагружать роутер легко и непринужденно, «щелчком мыши». Сегодня мы начнем описание с заббикса.

В меню «Администрирование» -> «Скрипты» добавляем новый скрипт. Называем его «Reboot», в качестве команды прописываем <code> «php /usr/share/zabbix/reboot.php {HOST.HOST}»</code>

Далее: Меню «Мониторинг» -> «Последние данные» -> «Щелчок правой кнопкой мыши на нужно узле сети». Вот так вот будет выглядеть меню после добавления скрипта.

Соответственно в директорию (у Вас она может быть другая, я использую корневую директорию zabbixa).

Оговорка по безопасности: для наглядности объяснения в скрипте я использую только id роутера, но не пользуюсь паролем. В рабочей версии так делать не рекомендуется! Почему так сделал я: потому что большой вопрос — где хранить пароли к роутерам ? В самом zabbixe в «инвентарных данных» ? Противоречивая практика. Как вариант: ограничить доступ извне к самому файлу reboot.php

Файл reboot.php


$sql_users=$conn->prepare("UPDATE users SET task='reboot' WHERE id=? AND status='active';");
$sql_users->bind_param('s', $user);
$sql_users->execute();
$sql_users->close();
?>

 

Собственно все. Открытым остается вопрос «как получать результат выполнения команды со стороны устройства». Рассмотрим задачу на примере с командой ifconfig. Вот такую команду можно отправить устройству:


message=`ifconfig`; wget "http://xn--80abgfbdwanb2akugdrd3a2e5gsbj.xn--p1ai/a.php?u=user&p=password!&m=$message" -O /tmp/out.txt

, где:
message=`ifconfig` мы переменной $message присваем результат вывода команды ifconfig
wget "http://xn--80abgfbdwanb2akugdrd3a2e5gsbj.xn--p1ai/a.php наш скрипт a.php, регистрирующий роутеры и принимающий сообщения от них
u=user&p=password!&m=$message учетные данные и значению переменной запроса m — присваивает содержимое переменной $message
-O /tmp/out.txt // вывод в файл /tmp/out.txt нам в данном случае не нужен, но если не указывать данный параметр, wget не срабатывает

Почему это криво работает: потому что это потенциальная дыра в безопасности. самая безобидная ошибка, которая может случится — это если в выводе вашей команды к примеру будет символ «&». Поэтому надо фильтровать и все что отправляется с роутеров и все что приходит на сервер. Дааа, мне стыдно, правда. В свое оправдание могу лишь написать — что вся статья посвящена тому, как управлять роутерами с неопределенной заранее прошивкой, с неопределенными заранее каналами связи.

Напоминаю, что все исходники можно забрать с Git-репозитория, по адресу: https://github.com/BazDen/iotnet.online.git