Написанное ниже — мои заметки по протоколу Reticulum. Я решил, что они могут оказаться кому-то полезными и публикую их по этой причине. Особенности работы протокола рассматриваются только с позиции клиента ввиду моих крайне скудных знаний по этой теме.
Reticulum — радиопротокол для mesh-сети, по словам автора — попытка создать альтернативный протокол базового уровня для сетей передачи данных.
К сожалению основная документация под названием «Reticulum network stack», написанная Mark Qvist — хороший пример того, как писать документацию не нужно. По большей частью этот текст — водяной раствор крупиц информации о протоколе. Более того, даже если вы продеретесь через философские рассуждения о приватности и инновационности Reticulum, полного понимания о протоколе у вас не будет. Автор совершенно бесстыдно предлагает понимать детали через чтение исходного кода эталонной реализации Reticulum. И к сожалению, для сбора большей части информации мне пришлось прибегнуть к LLM, который этот исходный код анализировал. К слову, все попробованные мною LLM по-настоящему не знают протокол и без очень большой помощи со стороны пользователя не способны писать программы с его использованием. В определенной мере помимо отсутствия полноценной документации это связано также с тем, что у протокола было несколько версий и детали его реализации менялись.
Дополнение: после написания этих строк один добрый человек написал полноценную спецификацию протокола и выложил её на гитхаб.
Идея Reticulum в корне проста — все ее участники по своему желанию могут быть как просто конечными клиентами, так и узлами-ретрансляторами. Все передающееся в сети можно поделить на 2 вида данных — незашифрованное распространение так называемых анонсов и broadcast пакетов с информацией всем участникам сети и зашифрованная передача данных кому-то конкретному.
Анонсы - пакеты данных в виде идентификатора сети с некими структурированными данными и цифровой подписью. В структуре этого пакета данных второй байт представляет собой счетчик перемещений (hops) между ретрансляторами.
Выглядит это следующим образом: допустим мессенджер на смартфоне шлет анонс на ретранслятор с 0 hops, ретранслятор пересылает сообщение другим подключенным к нему ретрансляторам и клиентам меняя счетчик на 1. При этом ретранслятор запоминает, от кого к нему пришел анонс. Остальные по цепочке передают анонс дальше меняя счетчик и запоминая откуда пришел анонс. Таким образом анонс достигает всех участников сети, кого-то даже несколько раз, но в этом случае такой пакет обычно игнорируется, если с момента получения предыдущего пакета прошло мало времени или величина hops больше.
Важно учитывать, что если Reticulum работает через TCP соединение, то это именно одно соединение без прерываний, через которое туда и обратно шлются пакеты Reticulum в виде непрерывного потока бинарных данных без особой последовательности и организации.
Уже на этом этапе можно понять, что фоновая активность сети у всех ее участников весьма высока даже при участии в сети только в качестве конечного клиента (за минуту приходят десятки, а то и сотни одних только анонсов). При этом следует отметить, что так как анонсы передаются в незашифрованном виде, то сеть reticulum не является цензуроустойчивой. Стоит порезать незашифрованную часть протокола - и маршрутизация развалится. Понимание устройства пакета у Reticulum осложняется весьма активным использованием криптографии.
Схема пакета в байтах:
Заголовок Hops Адрес Контекст Данные
[1 ] [1 ] [16/32 ] [1 ] [До 465 ]
В самом первом байте пакета каждый бит является флагом:
1) - IFAC: Если равен 1, то после hops от 1 до 64 байта выделяются для кода доступа
2) - Тип заголовка: Если равен 0, то в поле адресов указан один адрес (16 байт), если 1 - два адреса (32 байта)
3) Контекст
4) Передача: 0 - broadcast, 1 - транспортная
5) Тип пункта назначения: 00 - одиночный, 01- группа, 10 - plain, 11- link
6) Типы пакетов: 00 - данные, 01 - анонс, 10 - запрос на связь, 11 - proof
Во второй байте подсчитывается количество
ретрансляций, те самые hops.
Затем следует опциональный сегмент IFAC
Как именно он реализован в Reticulum я не разбирался, на публичных серверах он не используется.
После следуют одно или два поля с адресами. Они представляют собой первые 128 бит SHA256, о том, как именно получается эти хэши напишу ниже. В пакете таких адресов может бы один или два. В зависимости от типа пакета, поля с адресами могут использоваться для разных целей, универсального принципа работы здесь нет.
Контекст - еще одно однобайтовое поле, которое иногда несет в себе дополнительный параметр, если первого байта пакета не хватило. Сценариев использования контекста много, но в моем случае крайне примитивного понимания протокола это не важно.
И наконец, блок данных. У данных также предусмотрена некая определенная структура, которая очень сильно зависит от типа пакета, универсальной схемы нет.
Identity Reticulum достаточно активно использует криптографию как для формирования пакетов, так и для создания идентификатора. Перед началом использования сети необходимо произвести следующие действия: Сгенерировать закрытый ключ X25519 и на основе его открытый. Сгенерировать закрытый ключ Ed25519 и на основе его открытый. Затем 32 байтный открытый ключ X25519 объединяется с 32 байтами открытого ключа Ed25519 - получаем составной открытый ключ длиной в 64 байта. Одним из компонентов для получения уникального идентификатора в сети служит хэш sha256 из составного открытого ключа, точнее, первые 128 бит от этого хэша. Эти 128 бит будут примешиваться к другим хэшам, чтобы получать адреса. Но об этом позже.
Фреймирование пакетов производится в основном с помощью HDLC. Фреймирование через байт 7E. Этот байт стоит в начале и конце пакета, а если он встречается в самих передаваемых данных, то он заменяется на 2 байта: 7D 5E. А если в данных встречается 7D, то он заменяется на 7D 5D. Соответственно, для приема данных преобразовывать байты нужно в обратном порядке.
С чего начинается работа в сети? С отправки анонса чтобы 1) Узлы маршрутизации знали, какими путями слать нам пакеты 2) желающие отправить нам пакет знали наш публичный ключ шифрования.
Анонсы
Итак, мы решили послать анонс на публичный сервер. Перед отправкой нам потребуется идентификатор приложения, для которого будет отослан пакет. В Reticulum нет классической схемы «адрес устройства + порт». Вместо этого у каждого приложения на конкретном устройстве есть свой отдельный адрес в виде хэша.
Как это работает? Mark Qvist предлагает следующую схему. Нам потребуется имя приложения (допустим, mysuperapp). И некие «аспекты», которые расширяют его. Допустим, это v0005 и phone (пусть это будут версия приложения и указание на вид устройства, на котором оно запущено). Соединяем их через точку в «mysuperapp.v0005.phone», в единую строку. Берем первые 10 байт хэша sha256 от этой строки. И вот у нас получается так называемый хэш имени приложения. Эти самые 10 байт позволяют любому в сети, знающем о программе mysuperapp и его возможных «аспектах» понять, что пакет с таким идентификатором внутри предназначен для такой-то программы mysuperapp, а для незнающих это просто набор непонятных 10 байт. Теперь мы соединяем эти 10 байт с 16 байтами sha256 от публичных ключей, о которых писалось выше и берем от этих 26 байт еще один sha256, причем только первые 16 байт от него. Получаем адрес, по которому можно слать пакеты. Немного сложно. Однако, все указанное выше можно проделать даже и не владея никакими языками программирования с помощью разных криптографических утилит. Таким образом, у нашего устройства может быть столько же адресов, сколько на нем запущено приложений.
Анонс состоит из следующих частей:
1 Байт: 00010001 (возможны вариации по схеме выше)
2 байт: 00000000 (потому что мы посылаем анонс от себя и он прошел 0 ретрансляторов)
С 3 по 18 байт - адрес,который получили в предыдущем абзаце
19 байт - 00000000 (байт контекста)
Затем 64 байта нашего объединенного публичного ключа.
Потом 10 байт хэша от названия приложения
Затем блок из 10 байт, из которых первые 5 байт случайные, а другие 5 байт - текущего UnixTime с конца в формате BigEndian (служит временной меткой).
Затем n количество байт, для так называемого Ratchet - механизма динамической смены ключей шифрования при пересылке нескольких пакетов, в котором я разбираться буду в одну из последних очередей. Его использование активируется байтом контекста. При байте контекста 0 никаких данных у Ratchet нет, никаких байтов в пакете соответственно тоже.
Затем 64 байта подписи с помощью Ed25519 (о том, что конкретно подписывается, напишу немного ниже).
И, наконец, следует набор произвольных данных, можно написать сюда что угодно. Главное, чтобы размер пакета не превысил 500 байт.
Подписью подписывается: адрес+составной открытый ключ+хэш имени приложения+случайные 10 байт(которые были ранее в этом анонсе)+данные Ratchet (только при условии его использования)+произвольные данные в конце пакета.
Если вы где-то ошибетесь - пакет будет молча проигнорирован.
При успешном принятии пакета, ВОЗМОЖНО, следующий в цепочке пришлет пакет типа proof. А может быть и не пришлет - зависит от настроек. Этот ответный пакет редкость и не следует при создании приложений на него полагаться.
Следует добавить, что анонс распространяется по сети не в том виде, в котором мы отправляем его туда.
Получатель получит от ретранслятора пакет, преобразованный из одно- в двухадресный.
Первый адрес - ретранслятора, а второй - тот, что был указан в изначальном анонсе.
Отправка пакетов с информацией Существует 4 типа пакетов с информацией: Single — один пакет одному адресату Group — один пакет множеству адресатов Link — множество пакетов одному адресату с динамически меняющимися ключами шифрования. Инициализация такого соединения осуществляется в несколько этапов с использованием специальных пакетов. Broadcast — один незашифрованный пакет всем в сети. Однако, пакет не ретранслируется, то есть передать его кому-то вне прямого соединения нельзя.
Рассмотрим только отправку Single-пакетов, так как Link сильно мудреный, а все остальное применимо лишь в очень специфических случаях.
В первую очередь мы генерируем эфемерный ключ x25519. По протоколу Диффи-Хеллмана из нашего приватного x25519 ключа и публичного x25519 адресата получаем общий секретный ключ. Применяем к этому общему ключу HMAC Key Derivation Function с SHA256. Параметр info должен быть пуст. Длина хэша - 64 байта. Соль -- хэш-адрес получателя. Берем первые 32 байта для шифрования методом AES-256-CBC, для IV генерируем случайные 16 байт. Применяем к шифруемым данным PKCS7 паддинг и шифруем их.
Теперь подготавливаем hmac подпись.
Алгоритм sha256, в качестве ключа для подписи берем оставшиеся 32 байта хэша HKDF, подписываем IV+шифрованные данные.
Теперь собираем пакет:
1 байт -- заголовок типа Data и Destination type Single
2 байт -- 00000000
Затем 16 байт хэш-адреса
Затем наш публичный эфемерный ключ x25519 (32 байта)
После следуют 16 байт IV.
Затем шифрованные данные
В конце - HMAC подпись.
Все не более 500 байт.
Принимающая сторона получает пакет в том виде, в котором он был отправлен кроме измененного счетчика ретрансляций. Методом Диффи-Хелмана из нашего публичного эфемерного ключа и своего приватного x25519 получается общий секретный ключ.
Он используется с HKDF с указанными ранее параметрами.
Затем используя 32 последних байта из полученных 64 проверяется подпись.
И, если все нормально, можно дешифровывать, используя первые 32 байта хэша HKDF, полученный IV и не забыть убрать PKCS7 паддинг.
А как же узнать, что пакет дошел до цели? При желании получатель может прислать (а может и не прислать) пакет типа proof, который содержит в себе хэш принятого пакета и подпись. Ретрансляторы, теоретически, пересылают такой пакет обратно по цепочке до приславшего, ориентируясь на указанный хэш пакета. То есть какое-то непродолжительное время ретрансляторы хранят хэши всех пересланных пакетов типа data и откуда они пришли. Мои эксперементы с пересылкой этого пакета оканчивались неудачей, так что я ничего о нем рассказать не могу. Буду рад, если кто-нибудь укажет на ошибки или оставит свои мысли о протоколе в комментариях.






