Доброго времени суток.
Интерпретатор ESPB временно пока отложил, интерес к нему оказался скромным. Но подошел к глобальной задаче с другой стороны и на этот раз все крутится вокруг Zerotier. Давно обратил внимание на данную платформу, когда то ковырял libzt - sockets поверх сети Zerotier. Это удобно для приложений, которые находятся вне единой локальной сети и должны общаться сквозь NAT. Думаю, многие хотели бы использовать микроконтроллеры (например, ESP32) для IoT-задач, не заморачиваясь со способами доставки информации на сервер и не завися от того, спрятан ли этот сервер за NAT.
Из этой идеи выросли три связанных проекта:
Проект | Назначение |
|---|---|
ZtSharp | Полностью managed ZeroTier на C#/.NET, собственный TCP/IP поверх VL2 |
ZtLiteESP | Минимальный VL1-клиент для ESP32: messaging между peer |
ZtGenStorage | Подготовка identity и |
ZeroTier: два уровня
Условно архитектуру ZeroTier удобно разделить на два уровня.
VL1 (Virtual Layer 1) — криптографически адресуемый P2P-слой: identity, discovery, криптография, управление путями, NAT traversal и обмен служебными и пользовательскими сообщениями.
VL2 (Virtual Layer 2) — виртуализация Ethernet поверх VL1: виртуальная сеть, Ethernet-кадры, сетевые настройки и дальнейшая работа с IP-трафиком.
Упрощённо:
ZeroTier ├── VL1 │ ├── identity │ ├── discovery & crypto │ ├── paths & NAT traversal │ └── USER_MESSAGE │ └── VL2 ├── virtual Ethernet & frames ├── ARP / NDP └── IP connectivity
ZtSharp: ZeroTier целиком на C#
Изначально хотелось получить managed-аналог libzt без native-бинарников и P/Invoke-обёрток под каждую ОС. Собственно, с этого всё и началось.
ZtSharp — полностью managed реализация ZeroTier node на C#/.NET. За основу взято ядро ZeroTier One 1.16.2, а поверх protocol layer реализована собственная managed networking часть.
В проекте реализованы:
Логика протоколов VL1/VL2;
Управление identity и peer-ами, discovery и path establishment;
Криптографическая часть, relay и multipath/bonding;
Поддержка
USER_MESSAGE, planet/moon;Встроенный network controller и pluggable storage;
Socket API и логирование через
Microsoft.Extensions.Logging.
Отдельный слой ZtSharp — собственный TCP/IP-стек, работающий непосредственно поверх виртуального Ethernet ZeroTier, без участия системных сокетов ОС:
ARP, NDP, ICMP/ICMPv6;
IPv4, IPv6, UDP;
TCP (с полноценным конечным автоматом состояний).
.NET application │ ▼ ZtSocket │ ▼ managed TCP/IP │ ▼ virtual Ethernet │ ▼ ZeroTier node VL1 + VL2
Сохранена совместимость с оригинальным libzt: это проверено тестированием через пример socket поверх VL2, где managed-нода ZtSharp успешно общается с native libzt.
USER_MESSAGE: связь без виртуальной сети
В процессе портирования выяснил, что в ZeroTier есть штатный механизм USER_MESSAGE — прикладное VL1-сообщение, предназначенное конкретному peer.
На уровне API у него есть:
адрес назначения;
64-битный
typeId;произвольный payload.
typeId фактически можно использовать как прикладной идентификатор типа сообщения — например, отделить телеметрию от команд или разные версии прикладного протокола.
Упрощённо:
Application │ ▼ USER_MESSAGE (typeId + payload) │ ▼ ZeroTier VL1 (identity / path / crypto) │ ▼ Peer
При этом не нужны:
join в виртуальную VL2-сеть;
ZeroTier IP-адрес;
TAP/TUN;
ARP/NDP;
IP-маршрутизация.
Есть и важная практическая оговорка.
USER_MESSAGE не является полноценной очередью сообщений с гарантированной доставкой. Отправка означает, что сообщение поставлено в механизм передачи; подтверждение доставки и прикладная семантика ACK должны быть реализованы самим приложением.
Именно так устроен и мой тест: hello from esp32:0, hello from esp32:1 и остальные ответы — это прикладные подтверждения, а не встроенная гарантия доставки USER_MESSAGE.
Это хорошо подходит для control/telemetry-сценариев, где размер сообщений небольшой и приложение само определяет, нужна ли повторная передача.
Почему на ESP32 только VL1
Для сервера вполне разумна архитектура:
VL1 ↓ VL2 ↓ Virtual Ethernet ↓ ARP / IPv4 / IPv6 ↓ UDP / TCP
Но для сценария:
«устройство передаёт статус серверу и иногда получает команду»
это избыточно.
Поэтому в ZtLiteESP оставлено только необходимое:
ESP32 │ ├── Identity ├── Discovery ├── WHOIS ├── Path ├── NAT traversal ├── Crypto └── USER_MESSAGE │ ▼ Application
Чего в ZtLite НЕТ:
VL2 TAP ARP/NDP ZeroTier IP IP routing network rules
Это не «свой велосипедный ZeroTier». Внутри используется platform-independent ядро node/ из ZeroTier One. Снаружи находится ESP-IDF-обвязка: lifecycle node, FreeRTOS task и абстракция UDP-транспорта.
Транспорт отделён от protocol core, что позволяет отлаживать один и тот же профиль даже без физической платы:
ZtLite core │ ┌──────────┼──────────┐ ▼ ▼ ▼ ESP32 QEMU Win32 (lwIP) (OpenEth) (Winsock)
Identity и planet: не на чипе
Генерация ZeroTier identity использует memory-hard proof-of-work (композиция SHA-512 и Salsa20). Это делает создание новых identity дорогим, повышая стоимость массового создания адресов (защита от спама).
Для обычного ПК эта операция незаметна. Для MCU выполнять её при первом старте особого смысла нет, да и слишком затратно.
Поэтому появился ZtGenStorage.
Он решает задачу генерации Identity в формате ZeroTier.
identity.public identity.secret
Создание identity дорогое, а проверка уже существующей identity значительно дешевле. Поэтому генерация на build-хосте — естественное и единственно верное решение для embedded. MCU не занимается подготовкой node storage при запуске, файлы просто встраиваются в прошивку.
Важно: одна identity = одно устройство. Если прошить несколько плат одним
identity.secret, получится коллизия в сети. Для серийной прошивки ZtGenStorage должен генерировать уникальную identity для каждой платы.
Реальный ESP32-C6 и ZtSharp
После отладки в QEMU проверил всё уже на реальном ESP32-C6.
В тесте использовались ESP-IDF 6.0.2, Wi-Fi и UDP-транспорт на порту 9995. После подключения к Wi-Fi устройство получило IP адрес, запустило ZeroTier node и перешло в состояние ONLINE.
Дальше ESP32 начал искать peer ddab75351e- пример непосредственно ZtSharp запускаемый через usermsg-live-server.bat.
Сначала через WHOIS была получена его identity, затем ZeroTier подтвердил рабочий path:
ONLINE ↓ WHOIS → identity peer ↓ PATH up ↓ USER_MESSAGE
После установки соединения ESP32 отправил несколько USER_MESSAGE и получил ответы от ZtSharp:
hello from esp32:0 hello from esp32:1 hello from esp32:2 hello from esp32:3 hello from esp32:4 hello from esp32:5
То есть в упрощённом виде всё выглядит так:
ESP32-C6 │ ▼ Wi-Fi / IP │ ▼ ZeroTier VL1 │ ├── WHOIS ├── discovery └── path │ ▼ USER_MESSAGE │ ▼ ZtSharp
При этом ESP32 не подключается к виртуальной ZeroTier-сети и не получает ZeroTier IP через VL2. Для обмена с peer достаточно самого VL1 и USER_MESSAGE.Отдельно интересно, что для bootstrap используются штатные "планеты" ZeroTier, при этом ZtLite не требует регистрации виртуальной сети через ZeroTier Central.
Память
Для ESP32 особенно интересен footprint. Динамика потребления heap-памяти в ходе прогона:
После инициализации Wi-Fi: ~303 KiB свободно.
После инициализации transport: ~222 KiB.
После создания Node: ~192 KiB.
После выхода в ONLINE: ~183 KiB.
Минимум за весь прогон: 180 664 байта.
После завершения 5 обменов: 228 816 байт свободно (память высвобождается).
Создание самого ZeroTier node не уничтожает запас RAM, хотя основная нагрузка по-прежнему приходится на сетевую инфраструктуру ESP-IDF (lwIP, Wi-Fi driver).
Утилита sizeprobe показывает размеры отдельных C++ объектов через sizeof:
Node = 4208 bytes World = 192 bytes Identity = 80 bytes Salsa20 = 64 bytes InetAddress = 28 bytes Peer = 1344 bytes Path = 376 bytes
Для выведенных runtime-структур:
Trace = 48 bytes Switch = 12592 bytes Topology = 1824 bytes SelfAwareness = 32 bytes -------------------------------- total = 14512 bytes
Примечание: это именно sizeof структур, а не полный фактический footprint с учётом фрагментации heap. Самой заметной структурой является Switch, что ожидаемо, так как это центральный компонент обработки VL1-трафика.
По итогам теста у основной FreeRTOS-задачи осталось 5680 bytes free из 28672 (High-Water Mark), а у UDP-задачи ZeroTier — почти 10 KB свободного stack.
Размер firmware
Есть смысл отдельно посмотреть и на Flash.
Для текущей конфигурации итоговый image имеет размер:
1 177 048 байт, то есть примерно 1.12 MiB.
Из них:
Flash Code 1 063 670 B .text 777 450 B .rodata 247 096 B .eh_frame 38 584 B
По DIRAM:
Used 140 830 B Total 452 112 B Usage 31.15 % Free 311 282 B
LP SRAM практически не используется:
Used 24 B Total 16 384 B Usage 0.15 %
Эти цифры относятся ко всему firmware, а не только к ZeroTier: сюда входят ESP-IDF, Wi-Fi, lwIP, FreeRTOS, диагностический код и сама ESP32-обвязка ZtLite.
Поэтому размер .bin нельзя напрямую считать размером VL1 core.
Зато вместе с runtime-измерениями эти цифры показывают главное: VL1-only ZeroTier node реально помещается на ESP32-C6 с заметным запасом по RAM.
Что получилось в итоге
В результате получился не «очередной порт всего ZeroTier на ESP». Получилось грамотное разделение ролей:
ZtSharp использует ZeroTier как основу полноценной managed-сети: VL1 + VL2 + собственный TCP/IP stack + socket API.
ZtLiteESP использует только ту часть ZeroTier, которая действительно нужна маленькому устройству: identity, discovery, path establishment, криптография и USER_MESSAGE.
А ZtGenStorage выносит подготовку identity и bootstrap-world за пределы микроконтроллера.
И главный вывод всей работы, пожалуй, такой:
ZeroTier можно использовать не только как виртуальный Ethernet, но и как готовую защищённую P2P-транспортную основу. Причем в нашем случае используются официальные "Планеты" Zerotier.
Для сервера поверх неё можно построить полноценный managed TCP/IP stack.
Для микроконтроллера достаточно оставить VL1 и обмениваться небольшими сообщениями между peer. Один и тот же механизм при этом работает на обоих концах — от полноценного managed node на .NET до небольшого ESP32-C6.
Именно такой вариант для IoT мне кажется гораздо интереснее попытки механически притащить весь ZeroTier на каждый класс устройств.
Проекты
Исходный код и инструменты доступны на GitHub:
ZtSharp — полностью managed реализация ZeroTier (libzt) на C#/.NET с VL1/VL2, собственным TCP/IP stack и API сокетов
ZtLiteESP — минимальный VL1-only клиент для ESP32 с
USER_MESSAGEZtGenStorage — утилита для подготовки ZeroTier identity и
planetперед прошивкой устройства
