Это опять Revertron, в прошлой статье я разобрал, как устроена сеть Yggdrasil: криптоадресация, spanning tree, жадная маршрутизация, bloom-фильтры. Я обещал, что это первая статья цикла - и обещал рассказать новости. А новостей много: я переписал Yggdrasil с Go на Rust. Это и есть Yggdrasil-ng (next generation).
Сразу спойлер: рерайт у меня занял 3.5 дня. После этого я неделю чинил один баг. И на этом проекте я уже собрал Android-мессенджер. Обо всём по порядку.
Откуда вообще взялась эта идея
У меня есть свой проект - мессенджер Mimir для Android, основанный на Yggdrasil. До недавнего времени сетевая часть в нём работала через Go-библиотеку yggquic (Yggdrasil с QUIC внутри). Я туда регулярно вносил правки, но… делал это через веб-интерфейсы разных нейросетей - ChatGPT, Kimi и других. Кидаешь файл, просишь поменять, копируешь обратно. Каждое изменение - отдельный ритуал.
С каждым месяцем мне становилось всё сложнее: код Go-библиотеки рос, держать его в голове целиком я не мог, нейронкам приходилось давать всё больше контекста, а ошибок всё равно становилось больше. Проект развивался медленно, я выгорал.
Потом по работе я попробовал Claude Code - это CLI-инструмент от Anthropic, который интегрируется с IDE (у меня RustRover от JetBrains плюс Android Studio). Принципиальное отличие - Claude видит весь проект сам, читает файлы, запускает тесты, правит код прямо в репозитории. Не “скопируй мне функцию, я её перепишу”, а “вот задача, разберись и сделай”. Я вдохновился разработкой с LLM как таковой, причём на новом уровне - не для генерации отдельных функций, а для реальной работы над проектами.
А в русскоязычном сообществе Yggdrasil давно ходила шутка: “надо бы переписать Yggdrasil на Rust”. Несколько лет об этом смеялись. Браться за такой крупный проект без знания Go было страшно - Yggdrasil это не “hello world”, это сетевой стек с криптографией, async I/O, CRDT-маршрутизацией и особенно с каким-то кастомным актор-фреймворком.
И в один вечер опять зашёл разговор на эту тему, я прикинул то, насколько легче мне будет разрабатывать мессенджер, и психанул! Склонировал три репозитория - yggdrasil-go (сам демон), ironwood (библиотека маршрутизации) и phony (actor framework, на котором построен Go-код). Открыл Claude Code в этой папке. Запустил команду /init, и понеслось!
3,5 дня на рерайт
Это не опечатка. От git init Rust-проекта до работающего демона, который пингует Go-узлы по сети - три с половиной дня.
Конечно, “работающий” - это не “production-ready”. Тестов было мало, фич меньше, чем в Go-версии, кое-где вместо нормальной обработки ошибок стояло unwrap() (этот пассаж написала нейронка, на самом деле она такого не допускала ;) ). Но базовая сеть работала: Rust-узел подключался к публичным пирам, строил spanning tree, искал маршруты через bloom-фильтры, шифровал трафик и обменивался пакетами с Go-узлами.
Что важно понять: я сам не писал код. Я давал задания. Всё началось с анализа кода на Го. Я не помню точный промпт, но он был примерно из трёх предложений. Claude долго жевал гошные исходники и выдал некий обширный план того, как будет переписывать проект на Rust. Я очень внимательно читал план, чтобы нейронка не пошла по неверному пути прямо с самого начала, ибо дальше будет сложнее развернуться. Упирался в лимиты Claude Pro, приходилось делать перерывы на кофе, еду и даже какие-то дела по дому. Ложился спать в шесть утра. Да, наверное именно поэтому получилось 3,5 суток на перевод.
Получилось 7 фаз работы:
Фаза 1. Фундамент: типы, криптография, wire-протокол, конфиг.
Фаза 2. Маршрутизация: spanning tree, bloom-фильтры, pathfinder, роутер.
Фаза 3. Сеть: peers, ядро, публичный API.
Фаза 4. Шифрование: сессии, рэтчетинг ключей, подписи.
Фаза 5. Интеграционные тесты.
Фаза 6. Yggdrasil-демон поверх ironwood: TCP-транспорт, TUN, админ-сокет.
Фаза 7. Критические фичи: backpressure очередей, оптимизация keepalive.
(Это подсказала нейронка, я уже точно не помню.)
Местами Claude Code справлялся блестяще. Например, он сам обнаружил, что в моей реализации bloom-фильтра нужен тот же алгоритм хеширования (FNV-128), что и в Go-библиотеке bits-and-blooms, иначе Rust-узел не сможет договориться с Go-узлом. Растовые крейты почему-то использовали другие реализации.
Местами было тяжелее. Особенно с конвертацией ключей Ed25519 → Curve25519: в Go-коде использовался кастомный bilinear map (u = (1+y)/(1-y) mod p), и стандартные Rust-крейты так не умеют (так подумал Клод). Пришлось реализовать big-integer арифметику вручную и проверять побайтово, что результат совпадает с Go. Но потом стало понятно, что реализация дичайше медленная! Пришлось хорошо поискать и найти стандартный curve25519-dalek, который это делает быстро.
Через 3,5 дня я подключился к публичной сети Yggdrasil с Rust-узла. Пинговал известные серверы, открывал сайты внутри сети - всё работало. Установил ноду на один из домашних серверов.
Неделя на один баг
А потом начался ад.
Симптом был издевательский: первый час всё работало нормально. Дальше - постепенная деградация. Сначала отваливались один-два узла, потом часть сети, потом маршруты начинало плющить совсем, и сеть шатало (извините). Перезапустишь - снова час всё в порядке, потом опять хрень. На маленьких тестовых сетях (3–5 узлов) баг не воспроизводился вообще: всё чисто. Только в реальной публичной сети с сотнями пиров (на моём публичном пире), и только когда пир проработает достаточно долго.
Я неделю ковырялся по этому поводу вместе с Claude. Перерывали Go-исходники, сравнивали логи, добавляли отладочные сообщения, гоняли захваты пакетов. Несколько раз казалось, что нашли - патч применили, узел подержали час, баг вернулся. Это было особенно обидно, потому что весь остальной код, переписанный с Go, работал правильно - handshake, шифрование, bloom-фильтры, pathfinder.
В итоге всё свелось к пяти строчкам в обработчике пира.
В Yggdrasil узлы периодически обмениваются “подписями” (SigReq/SigRes) - это часть протокола построения spanning tree. Заодно по разнице между отправкой запроса и получением ответа измеряется RTT (время до пира). Этот RTT потом используется при выборе родителя в дереве и вычислении наилучшего пути: при прочих равных предпочитается пир с меньшей задержкой.
Так вот, в моей реализации время отправки засекалось так:
async fn peer_reader(...) { let mut reader = BufReader::new(conn_read); let sig_req_send_time = Instant::now(); // ← один раз на весь reader loop { //... // на каждый входящий SigRes: let rtt = sig_req_send_time.elapsed(); router.handle_response(peer_id, &peer_key, &res, rtt); } }
Заметили? sig_req_send_time создавалась один раз, при старте обработчика пира. И дальше elapsed() от неё измерял не “время с момента отправки запроса”, а время с момента подключения к этому пиру.
Что это означало на практике:
Только подключились - RTT 0 секунд, всё норм.
Через минуту - RTT каждого ответа от этого пира 60 секунд.
Через 10 минут - 600 секунд.
Через час - 3600 секунд.
Алгоритм Spanning tree смотрел на эти цифры и принимал решения: “у пира A задержка час, у пира B - два часа, выберу A”. Потом тот же узел через минуту: “у A - час и одна минута, у B - две и одна минута”. Дерево начинало плыть, координаты узлов разъезжались, и маршруты ломались - причём тем сильнее, чем дольше узел работал. На свежем подключении всё было ок, поэтому на коротких тестах баг не ловился.
Фикс - буквально хранить время отправки по каждому SigReq отдельно, в HashMap<PeerId, Instant> внутри роутера. Когда отправляем запрос - записываем время. Когда приходит ответ - берём оттуда. Дельта получается осмысленная.
Мораль простая. LLM сегодня может за несколько дней переписать сетевой стек средней сложности - это правда, я это видел своими глазами. Но это не значит, что человек больше не нужен. Большой проект Claude Code тащит на себе уверенно. А на тонком семантическом нюансе в пяти строках - где Instant::now() стоит не в той функции - может споткнуться так, что баг будет ловиться неделю. Особенно если он не воспроизводится на коротких прогонах, и обнаружить его можно только в живой сети.
И ещё одна вещь, важная для понимания. Это не баг “Claude написал плохо”. В Go-коде это решено правильно (там время отправки хранится в структуре пира). Но при переписывании логики на Rust LLM “оптимизировал”: подумал, что один раз засечь время хватит, ведь reader всё равно читает только этого пира.
Что нового в Yggdrasil-ng
Раз уж я переписывал, я не стал слепо копировать Go-версию. Несколько вещей в Rust-варианте сделаны иначе или добавлены с нуля.
Единый бинарник. В оригинале два исполняемых файла: yggdrasil (демон) и yggdrasilctl (управление). В Rust-версии это один бинарник. Запускаешь без аргументов - это демон. Запускаешь с yggdrasil getPeers - это команда управления. Удобнее ставить, удобнее писать скрипты.
Конфиг в формате TOML. Для проектов на Rust это идиоматичный формат, и я помню тысячи раз за эти семь лет как люди совершали простейшие ошибки в HJSON, который использует гошная версия.
Параметры запуска. Их я тоже переделал, сделал двойные минусы для длинных версий параметров и тому подобное.
Бан-лист по IP. Если с какого-то IP-адреса три раза подряд приходит битый handshake - этот IP временно банится. Защита от шумных или некорректно настроенных пиров, которые иначе забивают логи.
И главное - CKR.
CKR: VPN через Yggdrasil
CKR - Crypto-Key Routing. Идея простая: вы прокидываете обычные IPv4 или IPv6-подсети поверх mesh-сети Yggdrasil, маршрутизируя их по ключу через конкретный узел.
Зачем это нужно - несколько типичных сценариев:
Многоузловой VPN. Произвольное число узлов, можно создать свою IPv4-локалку внутри Yggdrasil, о которой знают только ваши узлы. Можно играть в старые сетевые игры, не поддерживающие IPv6, как Hamachi - помните такой?
Site-to-site. Соединить две локальные сети (например, домашнюю и офисную) - устройства в обеих сетях видят друг друга по обычным локальным IP, как будто они в одной подсети, а под капотом трафик идёт через Yggdrasil.
Exit-node. Один узел в сети раздаёт через себя “обычный” интернет - например, у вас сервер в неавторитарной стране, и весь ваш трафик идёт через Yggdrasil до сервера, а оттуда уже в интернет.
Технически это работает так: в конфиге клиента вы пишете соответствие “такая-то подсеть → такой-то ключ” (подсетью может быть 10.0.0.0/24 или даже 0.0.0.0/0). Когда на TUN-интерфейс приходит пакет с адресом из этой подсети, Yggdrasil не дропает его, а заворачивает в свой собственный зашифрованный туннель и отправляет узлу-владельцу подсети. Тот разворачивает и пускает в свою локальную сеть (или в интернет, если это exit-node).
Это полноценная альтернатива WireGuard или OpenVPN (никому не говорите!). И намного легче делать Multi-hop VPN (или Double VPN), когда вы входите в сеть через один IP, а в интрнет выходите через другой. Пакеты летят через всю сеть.
CKR работает на Linux, Windows и macOS (и как клиент на Android). Маршруты в системе устанавливаются автоматически через кросс-платформенную библиотеку route_manager - никаких ручных ip route add.
Приложения на новом стеке
После того как Rust-демон стал стабильным, я вернулся к мессенджеру Mimir. Старая Go-библиотека yggquic (QUIC-стек внутри Yggdrasil) пошла на свалку. Вместо неё теперь - отдельный Rust-крейт ygg_stream, построенный поверх Yggdrasil-ng.
ygg_stream - это транспортный слой поверх Yggdrasil. Он реализует TCP/KEY: TCP-подобный протокол, в котором вместо IPv4/IPv6-адресов используются 32-байтные ed25519-ключи. Полная семантика TCP (3-way handshake, надёжная упорядоченная доставка, FIN/RST, congestion control по Reno, RTT по Jacobson/Karels), плюс UDP-подобные датаграммы - fire-and-forget, без рукопожатия. Адресацию обеспечивает сам Yggdrasil, шифрование - тоже его, поверх ничего лишнего не накручивается (а в случае с QUIC добавлялся ненужный оверхед с сертификатами и шифрованием).
Mimir получает из этого крейта обычные “соединения” и “датаграммы”, и вся прикладная логика мессенджера работает с ними так же, как раньше с QUIC-соединениями из Go. API получился практически идентичный старому: Node.connect(key, port), Conn.read/write, send_datagram.
В Android-проект ygg_stream экспортируется через UniFFI - инструмент Mozilla (используется даже в Firefox for Android), который из публичного API Rust-крейта генерирует биндинги под Kotlin/Swift/Python. Один раз пишешь Rust - получаешь готовые обёртки. Сборка идёт через cargo ndk - нативные .so-файлы плюс сгенерированные .kt-обёртки.
Mimir - это “лёгкий” сценарий: приложение использует только клиентскую часть стека, без TUN-интерфейса. Полный узел внутрь мобильного приложения с TUN-ом - это уже Yggdrasil-ng для Android. Это второе приложение, которое я сейчас разрабатываю - аналог официального Android-клиента Yggdrasil (я тоже его мейнтейнер, и у которого внутри Go-демон), только с Rust-сердцем. Здесь уже доступны все “взрослые” фичи: TUN-интерфейс, внутрисетевые DNS-серверы с блокировкой рекламы и аналитики, и главное - CKR. То есть с телефона можно поднять туннель к серверу и пустить через него весь трафик устройства, или объединить телефон с домашней сетью site-to-site.
В опенсорс это приложение я пока не выложил - думаю, как его правильно распространять. На сегодня оно живёт в виде бесплатных APK-сборок, которые я выкладываю в Telegram-чат сообщества.
И буквально сегодня я доделал ещё одну важную вещь, которая стоит отдельного упоминания. Раньше у меня TUN-интерфейс читался и писался с Kotlin-стороны: Android поднимает TUN, Kotlin-код в цикле читает из него пакеты, передаёт в Rust-библиотеку, получает обратно пакеты, отправляет в TUN. Каждый пакет совершал минимум четыре пересечения JNI-границы и проходил через Java-аллокаторы.
Сегодня я перенёс работу с TUN целиком внутрь Rust-крейта: FileDescriptor отдаётся в Rust один раз, дальше всё (чтение, шифрование, маршрутизация, расшифровка, запись) живёт на Rust-стороне без участия JVM. Результат - прирост скорости в 9–10 раз (пропускная способность через CKR-туннель), резко меньшее потребление памяти и почти полное снятие нагрузки с GC.
Что дальше
Это вторая статья цикла. В следующих хочу разобрать конкретные технические темы поглубже - на выбор:
Шифрование сессий - как именно работает сессионный handshake, рэтчетинг ключей, почему Ed25519 приходится конвертировать в Curve25519 и как это устроено.
Pathfinder изнутри - bloom-фильтры, path lookup, broken paths.
CKR на практике - отдельная статья с конкретными конфигами: как поднять exit-node, как сделать site-to-site, какие подводные камни.
Android-разработка с UniFFI - для тех, кто хочет встраивать Rust-код в мобильные приложения.
Что больше интересно - пишите в комментариях, расставлю приоритеты по запросам.
Спасибо, что дочитали. Yggdrasil-ng - открытый проект, исходники здесь, баг-репорты приветствуются, PR-ы не очень.
Комментарии (23)

Dhwtj
25.04.2026 03:09Если сделали новые крейты то опубликуйте их в git и на crates.io
Дело, похоже, полезное. Ну не люблю я Go.
P.S. ночью надо спать

Xelld
25.04.2026 03:09Интересно, пишите ещё :)
Я бы почитал про pathfinder и вообще про сходимость сети подробнее.

casper_por
25.04.2026 03:09Отличная работа! С точки зрения выбирания что дальше - интересно всё, но в данный момент больше интересует CKR на практике ввиду новых ограничений, ну и так же очень рассчитываю на полную транспортную совместимость в перспективе с go версией

NickUkoloff
25.04.2026 03:09ох уж это неутомимое желание переписать всё на раст...
особенно с помощью ллмки

nitro80
Автор, похоже в https://t.me/Yggdrasil_ru меня забанили, как туда попасть?
Revertis Автор
Писать мне в личку. Да, надо быть осторожным, мой бот на входе даёт только 30 секунд на ввод цифр.