Модифицирующий MQTT Proxy
Janus MQTT Proxy — это сервис, который я написал на Go в качестве хобби-проекта. Он подключается к MQTT-брокеру и подписывается на все события, а клиенты, в свою очередь, подключаются к proxy и общаются с ним как с MQTT-брокером.
Он позволяет:
- ограничивать доступ клиентов к разным топикам. В том числе раздельно ограничивать доступ на чтение и запись;
- подменять названия топиков и содержимое событий с помощью regexp-ов.
В MQTT нет стандарта для структуры топиков и содержимого пакетов. Например, в каких-то сервисах включение лампочки выполняется отправкой 1 в топик /my/lamp/on, а где-то нужно отправить On в топик /my/lamp. Чтобы корректно связать между собой два таких сервиса, нужно явно указывать одному из них, что нужно отсылать и куда. Если топиков много, тогда конфиг будет огромным и совершенно нечитаемым.
Janus MQTT позволяет оставить сервисы как есть: каждый работает с топиками так, как ему удобно, а весь конфиг конвертации MQTT-пакетов сосредоточен в одном единственном месте. Причём чем больше разных сервисов/устройств вы используете, тем такой подход удобнее.
В идеале каждый сервис должен брать свою конфигурацию просто из структуры топиков. Допустим, среди них есть топик /light/main. «Ага, — должен подумать сервис, — значит, я могу включать и выключать свет, отправляя сообщения в этот топик». К сожалению, таких сообразительных сервисов я пока не встречал.
Что касается безопасности, то любая конфигурация умного дома состоит из модулей, как физических, так и программных. С помощью Janus MQTT мы можем дать этим устройствам/модулям доступ только туда, куда нужно. Это, кстати, не только улучшит безопасность, но и снизит уровень хаоса — никаких публикаций в топики с неизвестных клиентов.
Чтобы было понятнее, как использовать сервис, я разберу небольшую часть моего конфига — ту, которая описывает управление освещением.
У Janus MQTT есть основной конфиг, который содержит основные настройки и список пользователей. Для каждого пользователя задаётся пароль и отдельный конфиг преобразований — самое интересное происходит именно в нём:
broker_to_client: # настройка преобразования пакетов от брокера к клиенту
# описание устройств для MQTT discovery.
- topic: ^/devices/wb-gpio/controls/LIGHT_([^/]*)/meta/type$
template: /homeassistant/light/{{.f1}}/config
val_map:
switch: >-
{
"command_topic":"/light/{{.f1}}/state",
"state_topic":"/light/{{.f1}}/state",
"name":"{{.f1}}"
}
- topic: ^/devices/wb-gpio/controls/LIGHT_([^/]*)$
template: /light/{{.f1}}/state
val_map: {0: OFF, 1: ON}
client_to_broker: # настройка преобразования пакетов от клиента к брокеру
- topic: ^/light/([^/]*)/state$
template: /devices/wb-gpio/controls/LIGHT_{{.f1}}/on
val_map: {OFF: 0, ON: 1}
Каждое правило содержит регулярное выражение, которое извлекает данные из топика. Затем эти данные могут быть подставлены в шаблон названия нового топика и в шаблон пакета.
Любопытно, что в этой конфигурации для отправки команд и чтения состояния мой Home Assistant использует один и тот же топик, а уже внутри прокси эти топики разделяются на два.
Хак для MQTT discovery работает так: среди топиков Wiren Board есть специальные сервисные топики, которые описывают тип устройства. Насколько я понимаю, они используются только в стандартном интерфейсе управления. Они есть у каждой лампы и все равны switch. Все эти события retained и приходят один раз — в момент подключения. Я просто беру и подменяю все такие пакеты JSON-структурой, которая нужна для того, чтобы MQTT discovery подхватил эти устройства.
Сервис написан на Go и построен на базе библиотеки paho.mqtt.golang. Эта библиотека реализует MQTT-клиент для работы с брокером. Использовать её в качестве сервера никто не предполагал, поэтому его пришлось писать самому, используя части MQTT-клиента.
Принцип работы очевидный: выдаём себя за брокера, принимаем пакеты от клиентов, изменяем их и отправляем настоящему брокеру. То же самое в обратную сторону. Т.е. получаем такой MITM.
Самые интересные штуки:
- поддержка различных уровней QoS — за счёт того что доставка на уровнях 2 и 1 осуществляется с подтверждением, получаем диалог клиента и сервера по каждому отправляемому сообщению;
- из-за тех же QoS нужно генерить различные message_id, которые закодированы в uint16 и должны переиспользоваться;
- Janus MQTT держит одно единственное подключение к брокеру и подписан на все сообщения. Уже внутри себя он разбирает подписки клиентов и присылает им только то, что нужно каждому из них;
- не хотелось делать хранилище сообщений внутри сервиса, однако пришлось сделать in-memory хранилище retained-сообщений.
Обработка MQTT-пакетов от клиентов описана в функции client.serveIncoming, она запускается в горутине и читает из TCP-сокета. В этой функции описана высокоуровневая логика обработки MQTT-пакетов от клиентов:
- ConnectPacket — аутентифицировать пользователя и вернуть Connack с подтверждением или ошибкой;
- SubscribePacket — подтвердить подписку отправкой Suback и отправить retained-сообщения;
- UnsubscribePacket — подтвердить отмену подписки отправкой Unsuback и отменить подписку;
- PingreqPacket — отправить обратно Pingresp;
- PublishPacket — запустить автомат отправки сообщения в брокер;
- PubackPacket, PubrelPacket, PubcompPacket передаются в автоматы отправки сообщений;
- DisconnectPacket — дисконнект.
Автоматы отправки сообщений в клиент и от клиента нужны только для поддержки различных уровней QoS. Всего в MQTT три уровня QoS:
- QoS 0: отправка без подтверждения доставки;
- QoS 1: отправка с подтверждением доставки;
- QoS 2: отправка с подтверждением доставки один и только один раз.
Как мне кажется, уровень 1 и тем более уровень 2 не имеют большого смысла дома, но, тем не менее, реализовать их было интересно (уровень 2 я немного не доделал, поэтому пока что обрезаю весь QoS уровнем 1, протокол это позволяет).
Обработка publish-пакета от клиента с QOS=2
Обработка publish-пакета от брокера с QOS=2
Janus MQTT я использую для организации взаимодействия между тремя компонентами:
- Wiren Board — основной контроллер;
- Home Assistant — фронтенд (у него удобное приложение);
- Yandex2mqtt — голосовое управление.
Wiren Board
Содержит кучу разных реле и датчиков. В нём же крутятся основные скрипты управления всем. На нём работает MQTT-брокер.
Yandex2mqtt
Реализует oAuth для аутентификации и шлюз для взаимодействия с API умного дома Яндекса. Я решил не тратить на это время и взял готовый компонент. Изначально его написал munrexio, потом bawdiest обернул в докер, а я немножко поправил.
Пока что в конфиге одна единственная лампочка, но зато самая важная:
$ mosquitto_sub -h localhost -u yandex2mqtt -P yandex2mqtt -t '#' -v
/light/LIVING_TABLE/state 1
Home Assistant
Фронтенд. Собирает статку и рисует разные графики. Позволяет управлять как светом, так и отоплением.
Заниматься настройкой Home Assistant мне не хочется, мне нужно, чтобы я запустил его и там сразу появились все мои устройства.
К счастью, в Home Assistant есть MQTT Discovery — это специальный режим, когда Home Assistant получает конфиг устройств прямо из MQTT. Там нужно создать специальные топики с JSON-структурами, описывающими устройства.
В итоге весь конфиг Home Assistant сводится к:
mqtt:
username: !env_var MQTT_USER
password: !env_var MQTT_PASS
broker: !env_var MQTT_HOST
discovery: true
discovery_prefix: /homeassistant
Janus MQTT
Конфиг Janus MQTT для Home Assistant самый сложный. Однако он уложился в 100 строчек, при том что описывает 17 групп освещения, 6 термостатов с обратной связью по состоянию (вкл/выкл) и по температуре в комнате и 5 тёплых полов. Часть этого конфига приведена выше. Полный конфиг можно посмотреть тут.
Docker
Все сервисы работают в докере на NanoPi и описаны в docker-compose.yml.
У меня сервис безостановочно работает с января, была пара мелких багов, которые я поправил, но в целом всё хорошо.
Весь докеробраз сервиса весит порядка 10 Mб. Слава Golang!
Сервис можно скачать/посмотреть тут :
https://github.com/phoenix-mstu/janus-mqtt-proxy.
Файл docker-compose можно посмотреть тут:
https://github.com/phoenix-mstu/smart_home/tree/master/raspberry.
И самое интересное, сервис можно пощупать вот тут: 52.59.242.204:26927, логин/пароль — habr/hrabr, протокол — MQTT, конечно же. Там настроен проброс порта в мою локалку. Специально для статьи запущен отдельный инстанс сервиса в докере со специальным конфигом, там можно:
- Управлять светом в моей кладовке (любопытно будет посмотреть на светомузыку).
- Оставить сообщение.
В качестве брокера выступает мой основной брокер-сервер — посмотрим, сломается или нет. Я, конечно, предпринял ряд мер для безопасности. Понятно, что DoS-атаку оно не выдержит — вы просто забъёте узкий канал. Будьте разумны.