Что больше всего бесит при первом запуске устройств с управлением по Ethernet? Необходимость его искать в сети с использованием зоопарка из подходов. Тут используются программы автопоиска (например Winbox для MikroTik), дефолтные IP адреса (все эти 192.168.1.1, 192.168.100.1, 192.168.2.1 — кто во что горазд). Иногда надо со смартфона показывать QR коды в камеру устройства или передавать настройки тональными сигналами в микрофон. Мы задались целью найти стандарт для поиска устройств в сети и внедрить его в свои устройства на основе микроконтроллеров и/или одноплатных компьютеров. Это статья о стандартах, их особенностях, преодолённых трудностях и об открытом коде, который мы написали для себя и считаем лучшей в мире открытой реализацией SSDP сервера и клиента.
Почему Ethernet?
Кабельный Ethernet это прекрасный стандарт для индустриальных применений. Кабельное подключение надежнее беспроводного, кабели бывают с экраном, выпускаются индустриальные разъёмы RJ-45, соединение гальванически развязано (чрезмерно высокое напряжение может привести к поломке одного устройства в сети, но на другие не распространится). Соединение легко транслируется в оптоволокно медиаконвертерами, либо передаётся через коммутаторы, что обеспечивает большую дальность в интрасети, а при желании позволяет соединяться и на любой дальности по Интернету. Немаловажно, что сеть Ethernet скорее всего уже существует на объекте внедрения, так что затраты на создание канала управления минимальны. Про скорости передачи данных даже говорить не стоит. RS-232 и рядом не стоял. Ethernet почти всегда есть в одноплатных компьютерах, да и в микроконтроллеры его частенько встраивают, а иногда даже со встроенным PHY (https://www.ti.com/product/TM4C1294NCPDT).
Минусы у Ethernet тоже есть. Если в протоколах RS-232, RS-485, USB пакет данных всегда будет доставлен, то в Ethernet доставка пакетов негарантированная. Либо, в случае использования поверх Ethernet протоколов гарантированной доставки (TCP), будет непредсказуемая задержка. С потерей пакетов можно бороться, используя оборудование с заведомо большей пропускной способностью, чем теоретическая пиковая нагрузка. А еще одним минусом Ethernet является плохая предсказуемость адресов устройств (что MAC, что IP) и сложная диагностика отказов связи (тут RS-232 сильно выигрывает).
-
Начнём с MAC адреса. Например, у каждого устройства должен быть уникальный MAC адрес для приёма и передачи пакетов в пределах подсети. Встречал я китайские устройства с одним MAC адресом на всю серию. Это не создаёт проблем, пока вы используете одно такое устройство, но после включения второго, как-то всё перестаёт работать. Простого пользователя такая история ставит в тупик.
-
Если с MAC адресами производитель не накосячил и хотя бы сгенерировал их для каждого устройства случайными в рамках стандарта, то следующий уровень адресации это IP адрес (здесь и далее я буду говорить только об IPv4). И тут снова не предсказать условия эксплуатации. В обычных пользовательских сетях принято использовать DHCP сервер, который выдаст адрес автоматически (а также маршруты, сервер времени NTP, сервер DNS и т.п.). Это очень удобно, но узнать-то адрес как? Мы, например, раньше ходили на сервер к dhcpd и смотрели там недавно выданные lease. Еще сканировали сеть с помощью nmap, а потом включали и выключали устройство и смотрели какой адрес появляется и исчезает. Так себе методы.
-
Но если сеть индустриальная, то часто DHCP сервер не используется и IP адреса нужно назначить каждому устройству вручную. Заранее производителю оборудования совершенно непонятно, будет ли устройство в сети 192.168/16, 10/8, 172.16/20 или любой более узкой подсети. Поэтому без DHCP любой статический адрес, заданный по-умолчанию в устройстве, маршрутизироваться почти наверняка не будет.
В итоге самым распространённым способом уже 20 лет было назначать дефолтный IP адрес, писать его в инструкции, заставлять пользователя менять настройки своей сети (IP, маска подсети), через админку менять настройки сети устройства, восстанавливать настройки пользователя, profit. Однако, поскольку задача поиска устройств в сети довольно распространенная, то она была решена многими производителями самостоятельно. Например, в ПО Winbox для MikroTik есть механизм поиска устройств даже в другой подсети; в протоколе GigЕ Vision также есть механизм поиска устройств в сети, но только в той же; и т.д. Мы тоже в своё время сделали свой примитивный протокол обнаружения устройств через broadcast запрос на определённый порт и ожидание unicast ответа.
Напрашивается мысль, что должен существовать относительно универсальный протокол поиска и настройки сетевых устройств. И лучше использовать его, чем строить очередной велосипед.
Выбор протокола обнаружения устройств
Сразу скажу, что мы хотим охватывать мир Windows, Linux и Mac. Поэтому нам не подходят Mac only решения, где в рамках замкнутой экосистемы всё работает само. Не подходят решения, основанные на закрытом коде, чтобы не получить vendor lock. Нужно легковесное решение, которое реально реализовать в микроконтроллере. Сам поиск нужно уметь встраивать в наш софт, но будет хорошо, если операционная система тоже умеет устройство обнаруживать.
Удалось найти несколько относительно стандартных и распространенных протоколов:
-
mDNS (multicast, zeroconf, avahi): https://datatracker.ietf.org/doc/html/rfc6762, https://datatracker.ietf.org/doc/html/rfc6763
-
SSDP как часть UPnP (multicast, HTTP): https://web.archive.org/web/20151107123618/, http://upnp.org/specs/arch/UPnP-arch-DeviceArchitecture-v2.0.pdf
-
SLP (UDP multicast): https://docs.oracle.com/cd/E19455-01/806-1412/806-1412.pdf
Было решено попробовать найти и запустить примеры для каждой из перечисленных выше технологий обнаружения.
С SSDP/UPnP всё пошло неплохо. Достаточно быстро нашёлся пример сервера, который запускаются и обнаруживаются стандартными средствами Windows (https://github.com/ZeWaren/python-upnp-ssdp-example), пример с клиентом и сервером, которые можно запустить, как на Windows, так и на Linux и они находят друг друга (https://github.com/MoshiBin/ssdpy), готовые библиотеки с примерами для C (http://miniupnp.free.fr) и Python (https://pypi.org/project/ssdpy/), а самое главное стандарт, написанный понятным языком с пояснениями и примерами (http://upnp.org/specs/arch/UPnP-arch-DeviceArchitecture-v1.1.pdf).
Для поиска на базе mDNS тоже нашлись какие-то примеры. Но работали они менее стабильно. В нашей сети есть несколько сетевых принтеров. И с помощью найденных примеров их обнаружение носило вероятностный характер. С чем было связано обнаружение или необнаружение устройств, быстро понять не удалось. Также выяснилось, что встроенная поддержка mDNS появилась только в Windows 10. Для Windows 7 придётся вручную ставить Apple Bonjour или какие-то другие дополнительные компоненты (https://www.chiefdelphi.com/t/enable-mdns-on-windows/155295). Но даже в Windows 10 в зависимости от наличия обновлений поддержка может быть не полной и нацелена в основном на поиск сетевых принтеров (https://en.wikipedia.org/wiki/Multicast_DNS, https://learn.microsoft.com/en-us/answers/questions/266761/does-windows-10-support-mdns, https://superuser.com/questions/1330027/how-to-enable-mdns-on-windows-10-build-17134). Вдобавок ко всему описание mDNS в виде «машинописных» RFC (https://datatracker.ietf.org/doc/html/rfc6763) субъективно менее приятно в работе по сравнению с описанием UPnP.
Первые две технологии достаточно популярны. В интернете для них можно легко найти примеры, библиотеки и обсуждения проблем. SLP на их фоне выглядит гораздо скромнее. В частности, по запросу «Service Location Protocol library» первой ссылкой у нас вышел пакет pyslp для python с полностью отсутствующим описанием: https://pypi.org/project/pyslp/ Документация потом нашлась https://pythonhosted.org/pyslp/ Но при попытке запуска примера он упал с ошибкой `local variable ‘error_code’ referenced before assignment`. После некоторых доработок примеры запустились, но сервера, расположенный дальше localhost всё-равно найти не получилось. Стоит отметить что для SLP есть и другие библиотеки с примерами и относительно неплохое описание стандарта (https://docs.oracle.com/cd/E19455-01/806-1412/806-1412.pdf). Но время на исследование у нас было ограничено. Поэтому мы решили остановить свой выбор на SSDP / UPnP.
SSDP
SSDP это Simple Service Discovery Protocol, но есть и другая расшифровка: Stupidly Simple DDoS Protocol (https://habr.com/ru/articles/332812/) из-за его подверженности amplification DDoS attack.
Обнаружение устройств делается в 2 этапа (M-SEARCH и HTTP):
Сначала клиент SSDP посылает специальный поисковый UDP-запрос M-SEARCH выделенному мультикастному адресу на конкретный SSDP-порт 239.255.255.250:1900 с заданным форматом пакета, в который входят следующие строки:
-
M-SEARCH * HTTP/1.1 — обязательная строка в начале запроса
-
HOST:239.255.255.250:1900 — поле содержит мультикастный адрес и порт, зарегистрированные в Internet Assigned Numbers Authority
-
MAN:»ssdp:discover» — должен иметь такое значение
-
MX:2 — время ожидания ответа в секундах; может принимать целочисленные значения между 1 и 5 включительно; ответ устройства должен иметь задержку на случайное время от 0 до MX секунд
-
ST:upnp:rootdevice — цель поиска; в протоколе UPnP указаны варианты значений этого поля, от которых зависит процесс формирования ответа устройств и то, какие устройства в принципе будут на него отвечать
-
в конце запроса должна быть пустая строка
Этот M-SEARCH запрос пересылается всем участникам мультикастной группы, которые слушают адрес 239.255.255.250 на 1900 порту, после чего все устройства, которые подпадают под условия поиска (входят в группу устройств, запрошенных в поле ST), должны сформировать ответ и отправить его юникастной посылкой UDP-пакета на адрес и порт устройства клиента, который делал изначальный M-SEARCH запрос. Формат пакета с ответом с учетом только обязательных полей будет состоять из следующих строк:
-
HTTP/1.1 200 OK — обязательное поле; обязательная строка в начале
-
CACHE-CONTROL:max-age=1800 — обязательное поле; максимальное время валидности ответа в секундах; минимум 1800 секунд
-
DATE: дата формирования ответа — необязательное поле; дата формирования ответа на запрос
-
EXT: — пустое поле для обратной совместимости с версией протокола UPnP 1.0
-
LOCATION: http://192.168.4.207:8088/jambon-3000.xml — обязательное поле; URL расположения описания этого устройства
-
SERVER:unix/5.1 UPnP/2.0 MyProduct/1.0 — обязательное поле; строка в формате: OS name/OS version UPnP/2.0 product name/product version
-
ST:upnp:rootdevice — обязательное поле; ответ устройства на цель поиска; ответ зависит от запроса, на upnp:rootdevice однократный upnp:rootdevice
-
USN:uuid:device-UUID::upnp:rootdevice — обязательное поле; Unique Service Name, задаёт уникальное имя устройства на основе его UUID, которое может быть сгенерировано любым подходящим алгоритмом
-
BOOTID.UPNP.ORG:0 обязательное поле; содержит в себе число, которое должно увеличиваться каждый раз, когда устройство повторно подключается к сети и отправляет начальное уведомление — «перезагрузка» в терминах UPnP
Следующим этапом для получения более полной информации об устройстве может идти обычный HTTP-запрос XML-файла из поля LOCATION, в котором должна содержаться более развернутая информация об устройстве. Основные поля в данном XML-описании следующие:
-
<root> раздел — обязателен:
-
<device> раздел — обязателен:
-
<deviceType> — обязательное поле; тип UPnP устройства в формате urn::schemas-upnp-org:device:deviceType:ver, где deviceType — стандартизированное название типа устройства (как правило, для обычных устройств это Basic, а некоторые другие значениями можно посмотреть здесь), ver — максимальная поддерживаемая версия устройства
-
<friendlyName> — обязательно поле; понятное имя устройства
-
<manufacturer> — обязательное поле; название производителя
-
<manufacturerURL> — разрешенное поле; веб-страница производителя
-
<modelDescription> — рекомендованное поле; описание модели устройства; может быть длиной < 128 символов
-
<modelName> — обязательное поле; название модели
-
<modelNumber> — рекомендованное поле; номер модели
-
<modelURL> — разрешенное поле; веб-страница модели
-
<serialNumber> — рекомендованное поле; серийный номер устройства
-
<UDN> — обязательное поле; Unique Device Name, уникальное имя устройства, uuid:device-UUID; device-UUID должен совпадать с переданным в поле USN в ответах на M-SEARCH и NOTIFY-уведомлениях
-
-
Альтернативным способом для первого этапа является рассылка уведомлений устройством о своём присутствии в сети (NOTIFY). Однако вместо мультикаст запроса M-SEARCH с клиента и юникаст ответа от сервера в данном случае предполагается периодическая отправка NOTIFY-пакетов самим SSDP-устройством на мультикастный SSDP-адрес 239.255.255.250:1900 и постоянное прослушивание клиентом рассылки в мультикастной группе. Формат NOTIFY-пакета состоит из следующих строк:
-
NOTIFY * HTTP/1.1 — обязательное поле; строка должна быть в начале каждого NOTIFY-уведомления
-
HOST: 239.255.255.250:1900 — обязательное поле; SSDP мультикастный адрес и порт
-
CACHE-CONTROL: 1800 — обязательное поле; максимальное время валидности ответа в секундах; минимум 1800 секунд
-
LOCATION: — обязательное поле; URL расположения описания этого устройства
-
NT: — обязательное поле; тип устройства, которое посылает NOTIFY-уведомление
-
NTS: ssdp:alive — обязательное поле; тип NOTIFY-объявления; может быть ssdp:alive (при добавлении в сеть и при истечении времени в CACHE-CONTROL), ssdp:byebye (при отключении из сети), ssdp:update (при изменении свойств)
-
SERVER: unix/5.1 UPnP/2.0 MyProduct/1.0 — обязательное поле; строка в формате: OS name/OS version UPnP/2.0 product name/product version
-
USN:uuid:device-UUID::upnp:rootdevice — обязательное поле; Unique Service Name, задаёт уникальное имя устройства на основе его UUID, которое может быть сгенерировано любым подходящим алгоритмом
-
BOOTID.UPNP.ORG: — обязательное поле; содержит в себе число, которое должно увеличиваться каждый раз, когда устройство повторно подключается к сети и отправляет начальное уведомление — «перезагрузка» в терминах UPnP
-
CONFIGID.UPNP.ORG: — обязательное поле; число, определяющее номер конфигурации
-
SEARCHPORT.UPNP.ORG: — необязательное поле; номер порта, с которого устройство будет отвечать на юникастный M-SEARCH; может быть отличным от 1900 только если 1900 порт недоступен
Может взять готовые инструменты?
Выбрав SSDP протокол, мы продолжили тестирование готовых инструментов для SSDP, которые делятся на программы поиска по SSDP (клиент), а также программ эмуляции устройств, отвечающих по SSDP (сервер). Стало сюрпризом, что чаще всего они несовместимы между собой. Многие авторы воспринимают SSDP, как HTTP notify поверх UDP Multicast на 239.255.255.250:1900, а дальше – кто во что горазд. Ниже идёт сводная таблица наших экспериментов с клиентами, где мы отбирали только те, что написаны на С/С++ и Python, имеют открытый код, а еще добавили встроенный инструмент Windows. Внизу добавили сравнение с тем инструментом, который написали мы:
Название |
Язык |
Утилита или библиотека |
ОС |
Сетевые интерфейсы |
Отображение ответа на M-SEARCH |
Отображение ответа по HTTP |
---|---|---|---|---|---|---|
сетевое обнаружение Windows |
— |
графическая утилита |
Windows |
Все: автоматически |
Нет |
Да |
gssdp-discover (https://helpmanual.io/help/gssdp-discover/) |
C |
консольная утилита |
Linux |
Один: автоматически или можно указать конкретный |
Да |
Нет |
miniupnpc из библиотеки miniupnp (https://github.com/miniupnp/miniupnp) |
С |
библиотека с готовым примером |
Windows, Linux, MacOS, *BSD |
Один: автоматически или можно указать конкретный |
Нет |
Да |
Python |
пакет python и утилита |
Linux |
Все: автоматически или можно указать конкретный |
Да |
Нет |
|
UPnP Scanner (https://alax.info/blog/257) |
C++ |
графическая утилита |
Windows |
Все: автоматически |
Нет |
Да |
ssdpy-discover из библиотеки ssdpy (https://github.com/MoshiBin/ssdpy/) |
Python |
библиотека с готовым примером |
Windows, Linux |
Один: автоматически или можно указать конкретный |
Да |
Нет |
upnpy (https://pypi.org/project/UPnPy/) |
Python |
пакет python |
Windows, Linux |
Один: автоматически, нельзя выбрать |
Да |
Да |
ssdp-scan из ssdp-responder (https://github.com/troglobit/ssdp-responder) |
C |
консольная утилита |
Linux |
Все: автоматически |
Нет |
Да |
Наша реализация (https://github.com/EPC-MSU/revealer) |
Python |
графическая утилита |
Windows, Linux, MacOS |
Все: автоматически |
Да |
Да |
Во-первых, почти все клиенты делятся на 2 группы. Те, кто останавливается на стадии M-SEARCH и не запрашивает XML конфигурацию на стадии HTTP. И те, кто наоборот игнорирует устройство, если на M-SEARCH оно ответило, а по HTTP — нет. Исключением оказался пакет upnpy поверх которого мы быстро написали клиентскую утилиту. Но функций для доступа ко всем полям XML описания там нет. Чтобы это исправить придётся самостоятельно парсить XML.
Отдельной проблемой оказался выбор сетевого интерфейса, на котором работает сервер. Счастливы неведующие, у которых такой интерфейс один. Но в случае нескольких интерфейсов (Ethernet, WiFi, …) логично сканировать их все. Кстати Windows так и делает. Вместо этого часто предлагалось выбирать конкретный интерфейс или довериться автовыбору. В пакете upnpy выбрать интерфейс вообще нельзя (мы не нашли как).
Кроссплатформенность встречалась тоже редко. Тут лидером является библиотека miniupnp и их клиент miniupnpc. Они единственные охватили большую тройку Win/Lin/Mac. Но в своей серверной части Windows они уже не поддерживают.
Кроссплатформенность и выбор сетевых интерфейсов это не так просто. Если сервер подписывается на мультикастную группу со всех адаптеров компьютера, подобный код не получается автоматически кроссплатформенным, так как процедура подписи в Linux должна проводиться с помощью использования группы 0.0.0.0, а в Windows с помощью явного перебора всех сетевых адаптеров и подписи с их реальным IP-адресом. В своём коде сервера мы сделали ответ по всем интерфейсам, а вот в доступных в интернете примерах это не всегда поддерживалось.
А вообще, чего хочется от клиента поиска устройств по сети? Чтобы нашел по-максимуму. Чтобы искал быстро. Чтобы был доступ ко всей информации, а не только IP адрес и имя. Но и тонуть в информации о разных uuid:device-UUID не хочется. Кроссплатформенности хочется, чтобы можно было эту утилиту предлагать широкому кругу пользователей для поиска сделанных тобой устройств. Ну и тогда уж графический режим нужен. И тут лучше посмотреть своими глазами, кто как реализовал UX.
Windows тут стоит поставить на первое место. Microsoft прорабатывает свои решения. Но средств диагностики обычно не закладывает. В итоге сетевое окружение Windows, выступая, как клиент, показывает устройство в сетевом окружении, только если пройдены оба этапа обнаружения и XML файл содержит поле <deviceType>
, а отсутствие остальных обязательных полей прощает. Зато показывает и иконку, и подробную информацию по запросу, и информация структурирована.
gssdp-discover это утилита линуксоида. Минимум информации, консоль. HTTP предлагается запрашивать самостоятельно.
miniupnpc недалеко ушло от gssdp-discover.
ssdp гораздо лучше для диагностики, так как выводит всю информацию. Тоже без HTTP уровня. Но нужно учесть, что без установленного в системе Python 3.8 или выше, последняя версия ssdp работать не будет. А более ранние версии сильно отличаются по функциям. Не надо думать, что это не проблема. Сейчас 2025 год, а клиенты продолжают спрашивать работает ли наш софт на WinXP и на Python 3.4. Так что наша реализация клиента, например, сделана для Python 3.6 и выше.
В UPnP Scanner совсем другой подход. Это Windows решение. Графический интерфейс. Выводится вся информация. Очень похоже на встроенное сетевое окружение Windows с GUI начального уровня, но зато доступны исходные коды. К сожалению есть еще отдельная проблема с версиями UPnP. В версиях UPnP 2.0 в XML описании не должно присутствовать поле URLBase, но для предыдущих версий UPnP оно могло присутствовать и использовалось, как абсолютный URL, к которому добавляется относительный путь presentationURL. В итоге UPnP Scanner считает невалидными и не показывает все устройства, которые соответствуют UPnP 2.0 и выше.
ssdpy-discover. Мы тестировали на Windows и запускали клиента через client.py. Вывод похож на другие подобные консольные утилиты. Но основные поля есть, а второстепенные отфильтрованы.
upnpy. Про эту библиотеку я уже писал выше. Она найдёт устройства на обоих уровнях поиска, но не позволит выбрать интерфейс поиска. Также клиент придётся писать самостоятельно, а доступ ко всем информационным полям устройства можно сделать через парсинг XML. Мы этого делать не стали. На скриншотах видны поля, где доступ был встроенный в библиотеку.
ssdp-scan отличается тем, что умеет собирать NOTIFY сообщения. Правда занимать это может много времени. M-SEARCH он тоже умеет и быстро строит список в консоли Linux. Информация в нём скудная.
Серверов мы нашли меньше, чем клиентов. Это неудивительно, ведь они больше нужны производителям оборудования, как мы. Ниже сводная таблица наших экспериментов с серверами и наша реализация для сравнения:
Название |
Язык |
Утилита или библиотека |
ОС |
Сетевые интерфейсы |
Ответы по HTTP |
---|---|---|---|---|---|
miniupnpd из библиотеки miniupnp (https://github.com/miniupnp/miniupnp) |
С |
библиотека с готовым примером |
Linux, MacOS, *BSD |
Один: задаётся пользователем в конфиге или ключом запуска |
Да |
python-upnp-ssdp-example (https://github.com/ZeWaren/python-upnp-ssdp-example) |
Python |
утилита |
Windows, Linux |
Один: задаётся пользователем в коде |
Да |
ssdpy-server из библиотеки ssdpy (https://github.com/MoshiBin/ssdpy/) |
Python |
библиотека с готовым примером |
Windows, Linux |
Можно указать конкретный интерфейс параметром при создании сервера |
Нет |
ssdpd из репозитория ssdp-responder (https://github.com/troglobit/ssdp-responder) |
C |
утилита |
Linux |
Все автоматически |
Да |
Наша реализация (https://github.com/EPC-MSU/pyssdp_server) |
Python |
утилита |
Windows, Linux, MacOS |
Все автоматически с автоподхватом |
Да |
Все сервера умеют отвечать на M-SEARCH. Все сервера, кроме ssdpy-server, умеют отвечать на HTTP стадии и обнаружились у нас средствами Windows. А ssdpy-server у нас обнаруживался только тем клиентом, что входит с ним в одну библиотеку. Несмотря на обнаружение в Windows, чаще всего реализация стандарта полей XML описания в серверах была неполной. С кросплатформенностью еще хуже. Нужной нам тройки Win/Lin/Mac никто не собрал.
Найти всегда
Итак, есть много частично работающих готовых реализаций сервера и клиента, написанных под разные ОС, написанные на разных языках (нас волновал и Си, и Python). Если их скомбинировать, поотлаживать сниффером Ethernet, например Wireshark, а также почитать различные RFC, то можно довести клиент и сервер до совершенства, а дальше встраивать сервер в одноплатники и микроконтроллеры, а клиент — выдавать пользователям.
Однако одна из наших целей, чтобы устройство обнаруживалось всегда. Даже, когда в нём неизвестные статические настройки сети. А описанный выше способ поиска принципиально не может работать для случая, когда маски подсети на клиенте и на сервере разные. Сервер сможет получить от клиента мультикаст M-SEARCH запрос благодаря магии мультикаста, но ответ будет через UDP unicast, который не способен дойти до клиента в другой подсети. Однако эта проблема оказалась решаема в рамках стандарта SSDP. Мы используем вторую часть SSDP — advertisement с помощью NOTIFY сообщений. Стандарт не запрещает добавлять собственные поля, где мы передаём информацию о текущих сетевых настройках. Сообщения NOTIFY идут на мультикастный SSDP-адрес, поэтому они доходят от сервера до клиента вне зависимости от того, в какой подсети они находятся. Однако вместо периодической отсылки NOTIFY мы делаем их отсылку вслед за получением M-SEARCH. То есть запрос по multicast и ответ по multicast. Таким образом, мы гарантируем, что полный список найденных устройств будет готов через 2 секунды после запроса на клиенте, а не отловом случайных нотифаев раз в 1800+ секунд. Мы не знаем кого-то еще, кто додумался до такого приёма с SSDP.
DHCP и AutoIP
Отдельно нужно рассмотреть случай, когда в устройстве стояло автоматическое определение настроек сети (DHCP). Если DHCP сервер недоступен или отсутствует, то устройство окажется вообще без подсети и без сетевых настроек. И тут нам на помощь приходит AutoIP.
AutoIP базируется на стандарте RFC 3927 и присваивает устройству случайный, равновероятный IP-адрес из выделенной подсети 169.254.1.0-169.254.254.255. Мы генерируем его на основе MAC-адреса устройства, что тоже допустимо. AutoIP включается после нескольких неудачных попыток получить сетевые настройки от DHCP сервера. Диапазон адресов AutoIP считается всегда входящим в подсеть IPv4. Таким образом, пакеты на эти адреса направляются адресату в локальной сети напрямую, а не на шлюз, который вообще может отсутствовать. По-простому это означает, что два компьютера с поддержкой AutoIP (Windows, например), могут включить автоматическое определение настроек, не получить их от DHCP сервера, но, тем не менее, присвоить себе 2 разных IP адреса и быть связанными на L3 уровне. Всё это важно понимать для реализации в микроконтроллере, так как в Windows/Linux эти механизмы включены по-умолчанию. В микроконтроллерной библиотеке lwIP уже поддерживался механизм AutoIP, поэтому главное было включить его и понимать почему и как он работает.
Beyond SSDP
Теперь мы можем находить свои устройства вне зависимости от их сетевых настроек, но только на стадии M-SEARCH/NOTIFY. Стадию HTTP провести невозможно, если устройства находятся в разных подсетях. Поэтому сменить IP адрес стандартным способом через админку не получится. Как же установить новые настройки найденному устройству? Для этого мы снова использовали свойство M-SEARCH и NOTIFY пакетов, что в них можно дописывать новые поля. Неизвестные поля по стандарту отбрасываются. Мы же добавляем поддержку этих полей и прописываем туда IP/маску подсети, шлюз и флаг включения DHCP. Если наше устройство получает через мультикаст сетевые настройки, то оно их применяет. Теперь мы можем не только всегда найти своё устройство, но и сразу вбить ему новые настройки, несмотря на то, что устройство может быть в другой подсети и недоступно по UDP/TCP. Да, это функция будет работать только для нашей реализации сервера в микроконтроллере и для одноплатников (pyssdp_server) и только через клиент (revealer), который мы написали. Но по всем остальным функциям SSDP совместимость сохраняется.
Тут еще возникает вопрос безопасности. Коллега может по незнанию, неосторожности или ради шутки поменять сетевые настройки и устройство перестанет быть доступно из управляющей программы, что выведет из строя всю систему, куда устройство интегрировано. Придётся звать настройщика, что обычно означает задержку в работе на часы или на дни. Поэтому добавляем защиту паролем с возможностью его смены. Добавляем альтернативный сервисный пароль, который вычисляется секретным алгоритмом на основе серийника, неизвестен пользователю, и является fallback вариантом, когда пользователь пароль поменял, но забыл. Тогда пароль под конкретный серийник можно запросить в техподдержке. Всё это работает без шифрования и не претендует на секьюрность, так как располагается, как правило, в закрытой сети.
Сделали ли мы неубиваемую схему? Почти. Какой IP нельзя ставить на интерфейсе, чтобы не сломать связь с ним? Это вообще хороший вопрос для собеседования на сисадмина. Кто-то вспомнит про популярный IP localhost (127.0.0.1). Но вряд ли многие знают про диапазоны 0.0.0.0/8 и 127.0.0.0/8. Также к запретным IP был добавлен весь мультикаст 224.0.0.0/4. Вот теперь не сломаешь.
Реализация в микроконтроллере
Мы используем микроконтроллер Texas Instruments TM4C1294KPDT. Довольно уникальным свойством этого семейства является встроенный Ethernet PHY, что позволяет подключать микроконтроллер ножками напрямую на разъём Ethernet (со встроенными трансформаторами). Для этого микроконтроллера производитель даёт примеры, реализующие UDP сокет, а также HTTP сервер. Работает это всё поверх стека lwIP (lightweight Internet Protocol) версии 1.4.1, адаптированного для TI контроллеров с использованием FreeRTOS или без него. Этих примеров достаточно, чтобы написать свою небольшую реализацию SSDP сервера с ответом на M-SEARCH запросы, на запрос XML-файла и посылку NOTIFY пакетов. Писать реализацию с нуля было бы очень накладно.
Забавно, что Texas Instruments предлагает в SDK свою утилиту поиска микроконтроллеров в сети, не являющейся реализацией одного из стандартных протокола поиска. То есть это очередной велосипед, основанный на broadcast, в ответ на который присылается IP адрес, MAC адрес, идентификатор платы, тип платы, название приложения, версия прошивки.
С микроконтроллерами всё не так просто. Поэтому пришлось решить несколько проблем интеграции lwIP в нашу прошивку.
Например, сначала мы вызывали методы lwIP из тех прерываний, где нам было это удобно, но иногда получали сбои. А затем мы прочитали, что библиотека lwIP не является потокобезопасной. То есть все вызовы её методов должны идти последовательно, быть сериализованы.
А еще у нас не получалось сменить настройки сети на автоматические (DHCP), если текущий шлюз был не из той подсети, откуда приходит запрос на смену настроек. Комбинация странная. А причина оказалась в нехватке памяти микроконтроллера на то, чтобы одновременно ответить через несуществующий шлюз и создать нужное количество запросов к DHCP. AutoIP в такой ситуации тоже не срабатывал. Увеличили память — проблема ушла.
Сервер на ПК pyssdp_server
Микроконтроллерную реализацию сервера показывать довольно бессмысленно. Она завязана на конкретный чип и FreeRTOS. А вот реализацию сервера для компьютера мы вывесили в открытый доступ: https://github.com/EPC-MSU/pyssdp_server Это кроссплатформенное Win/Lin/Mac приложение для Python 3.6+, распространяемое в исходных кодах. За основу мы взяли https://github.com/ZeWaren/python-upnp-ssdp-example, который был существенно переработан, чтобы он обнаруживался в сети более стабильно и для всех сетевых интерфейсов, поддерживал обнаружение из другой подсети, поддерживал смену настроек сети через наше расширение SSDP, поддерживал HTTP сервер с переадресацией, выделение настроек в конфиг, скрипты инсталляции для systemd и т.п.
Что в итоге получилось? Получилось серверное приложение, которое через релиз в виде zip архива или через git копируется на любой одноплатник (а можно компьютер с Windows или macOS), в нём меняется файл конфига на вашу информацию (название устройства, ссылки на компанию и на продукт, имя производителя и т.д.), далее сервер запускается и ваше устройство появляется в сетевом окружении Windows, ищется через любую программу поиска из таблицы выше, ищется через наш клиент (https://github.com/EPC-MSU/revealer) в других подсетях. При клике на найденное устройство обычно открывается его HTML страница, которая обслуживается встроенным HTTP сервером, использующимся и для отсылки XML описания устройства. Файлы HTML можно поменять на свои и получить полноценный статический встроенный сайт (серверные скрипты не поддерживаются). Для того чтобы серверное приложение работало и после перезагрузки, есть готовый скрипт инсталляция сервера в виде systemd сервиса.
Кто регулярно превращает свои программы в сервисы, стартующие при загрузке компьютера, знает, как много может пойти не так. Наш pyssdp_server оказался не исключением. Проблему того, что сетевой интерфейс может быть не готов к открытию в нём соединений, мы предусмотрели. Сервер ожидал хотя бы одного активного сетевого интерфейса. Но когда интерфейсов 2, то сервер норовил стартовать, как только поднимется хотя бы один из них. В итоге все остальные сетевые интерфейсы не обнаруживались, пока не перезапустишь pyssdp_server. Пришлось делать фоновое отслеживание сетевых интерфейсов. Теперь, если к проводному Ethernet вдруг добавится WiFi или VPN, то обнаружение будет работать и на нём.
Неожиданный нюанс, с которым мы столкнулись, что иногда на наших одноплатниках уже работает встроенная админка на 80 порту. А HTTP сервер, встроенный в SDDP, выделен на порт 5050. И хотелось бы, чтобы при клике на устройство, например в сетевом окружении Windows, открывалась именно встроенная админка. То есть хочется, чтобы presentationURL указывал на один порт, а Location — на другой. Однако в стандарте UPnP считается, что presentationURL должен быть на том же адресе и порту, что и Location поле. Пришлось это обходить через включаемый в конфиге Redirect со встроенного HTTP сервера на нужный порт главной админки.
Во время тестов мы запускали сервер и клиент на одном компьютере и получали ошибку, что порт 1900 уже занят. Логично, ведь и сервер, и клиент слушают порт 1900, чтобы получать M-SEARCH и NOTIFY пакеты. И тут мы видим, что Windows прекрасно находит наш сервер на этом же компьютере, слушая тот же порт 1900. Как? А так — оказывается можно слушать один порт несколькими приложениями. Для этого есть флаги сокета SO_REUSEADDR
и SO_REUSEPORT
(их различия описаны вот тут сразу после слов «Welcome to the wonderful world of portability…» https://stackoverflow.com/questions/14388706/how-do-so-reuseaddr-and-so-reuseport-differ). Да, поведение флагов не кроссплатформенное. В Windows SO_REUSEADDR включает в себя поведение SO_REUSEPORT (за исключением старых версий до 2003), а в Linux флаг SO_REUSEPORT, без которого одновременные подключения не возможны, был добавлен на версиях >=3.9. Причем в линуксе приходящие requests будут распределены между открытыми подключениями, а в Windows первое подключение будет основным, а остальные ждут в резерве.
В итоге мы сделали, чтобы несколько экземпляров клиентского приложения могли работать одновременно на одном компьютере, да еще и с запущенным там же сервером SSDP. Но вот 2 сервера одновременно запретили как раз через флаги SO_REUSEADDR
и SO_REUSEPORT
, так как компьютер не должен идентифицировать себя двумя способами одновременно. Несмотря на то, что все клиенты слушают один порт, каждый из них получает свой список устройств и не получает пакеты, предназначенные для другого клиента. Дело в том, что HTTP ответ же идёт через unicast, где указывается порт, а unicast порты у клиентских приложений различаются. Порт 1900 используется в SSDP только для мультикаста.
Естественно мы добавили не только поиск из другой подсети, но и возможность смены настроек, чтобы работало не хуже микроконтроллеров. И тут две проблемы. Во-первых, нужно знать на каком сетевом интерфейсе меняем настройки, если их несколько. У микроконтроллера интерфейс-то один. С этим оказалось всё легко. Сервер слушает и отвечает сразу по всем интерфейсам. При этом он использует разные UUID. Поэтому запрос на смену настроек мы привязали к интерфейсу. А во-вторых, чудесный мир кроссплатформенности… Мы же хотим менять настройки на Win/Lin/Mac, а одних видов Linux существует порядка 1000 (https://habr.com/ru/companies/lanit/articles/562484/). И для каждого есть еще разные по времени версии релизов. Как тут написать единый код смены сетевых настроек? Поэтому мы пошли следующим путём: полученные настройки передаются в консольный скрипт параметрами. Скрипт, работающий на более-менее всех современных Линуксах мы написали. А если нужен скрипт для Windows, хитрого Linux или Mac, то его может написать пользователь сам. Да и вообще он сможет в скрипте кастомизировать реакцию на такое важное событие, как смена сетевых настроек.
В Windows вскрылся еще ряд странностей. Во-первых, нужно отключить брандмауэр или не будет работать отсылка и получение мультикаст пакетов из другой подсети. А ведь это важное и ценное свойство SSDP, чтобы можно было всегда найти устройство. Но внутренняя жизнь сети в Windows оказалась еще сложнее. Если запустить pyssdp_server на компьютере с WiFi и проводным адаптером Ethernet, то обнаружение по проводу работает всегда. А по WiFi оно работает только первые 10 секунд после вставления или вынимания кабеля Ethernet. То есть, изменение линка на одном из адаптеров прочищает WiFi адаптер на 10 секунд и он ловит поисковые запросы. А потом перестаёт. И Wireshark не показывает больше приходящие multicast запросы. Поиском нашлась похожая проблема (https://community.intel.com/t5/Wireless/Intel-WiFi-chips-are-blocking-multicast-after-resuming-from/td-p/684849):
I believe that once the NIC/Windows is in this deprecated state the issue lies with the NIC/Windows not properly using IGMP to indicate the client is interested in receiving traffic from the mDNS multicast groups.
IGMP is used by networking hardware (APs/Switches) to figure out which clients are interested in the multicast traffic being broadcasted.
If the AP that sits between the clients does not see the IGMP join message coming from the client it won’t forward multicast traffic for that IGMP group to that client, since it doesn’t know that the client is «interested» in receiving that multicast traffic.
То есть точка доступа WiFi может считать, что мы не хотим получать мультикасты по WiFi, так как на низкоуровневом протоколе IGMP, используемом для управления мультикастными подписками, мы не сообщили ей такой трафик нам пересылать. И причина может быть в некорректном использовании IGMP в Windows или в сетевом адаптере. Дальше я решил не копать.
Наш клиент Revealer
В клиентском приложении хотелось достичь кроссплатформенности Win/Lin/Mac, которой нет ни у кого из готовых приложений. Хотелось соответствия стандарту SSDP, которая в примерах из интернета встречается редко. Хотелось выводить все информационные поля из стандарта на обеих стадиях обнаружения устройств (M-SEARCH и HTTP), что тоже делают нечасто. Для отладки нужно видеть все данные, а не их обрывочные представления. Например, в нашем приложении мы увидели, что одно из устройств Zyxel на M-SEARCH стадии передаёт строку SERVER с пробелом в одном из полей, что приводит к её некорректному распознаванию и в Windows устройство не показывается. Мы, кстати, про проблему с пробелами указываем в комментариях конфига, идущего в комплекте с pyssdp_server. Хотелось графического режима для того, чтобы приложение было удобно обычным пользователям. В итоге получилось вот так:
Приложение с одной кнопкой и найденными устройствами, на которые можно кликать. Если нажать на значок информации, то откроется окно с полями структур описания устройства:
Вверху поля HTTP стадии. Внизу поля M-SEARCH стадии. Если нажать на значок шестерёнки, который появляется только для устройств с поддержкой наших расширений протокола SSDP, то появляется окно с установкой сетевых настроек:
Итог
Я считаю, что у нас получилась лучшая в мире открытая кроссплатформенная реализация SSDP клиента и сервера. У неё низкий порог входа. Поэтому я мимоходом поставил pyssdp_server и на свой компьютер для домашнего кинотеатра. Самое долгое было заполнять конфиг желаемыми строчками текста. Теперь я не буду вспоминать его IP, когда захочу зайти на него по ssh.
Клиентское приложение Revealer не только используется для поиска наших устройств, но и показывает некоторые принтеры, IP камеры, виртуальные машины, в которых поддерживается SSDP. Некоторые наши клиенты начинают волноваться, когда понимают, что их сетевая инфраструктура себя неплохо анонсирует в сети. Статья написана для того, чтобы сберечь время тем, кто хочет сделать свои устройства обнаружимыми в сети. Для этого можно пользоваться нашими готовыми реализациями. Статья написана для тех, кто хочет реализовывать свои велосипеды и может быть передумает. Поэтому разобраны многие подводные камни, дан обзор текущих инструментов. Статья написана для получения критики и обмена опытом, поэтому наш путь описан подробно и с обоснованием принятых решений. В мире поиска сетевых устройств слишком много хаоса. Надо что-то с этим делать.
Авторы статьи: Запуниди Сергей, Надежда Тарабрина.
ссылка на оригинал статьи https://habr.com/ru/articles/872216/
Добавить комментарий