惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

量子位
Stack Overflow Blog
Stack Overflow Blog
人人都是产品经理
人人都是产品经理
The GitHub Blog
The GitHub Blog
Engineering at Meta
Engineering at Meta
Vercel News
Vercel News
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Y
Y Combinator Blog
The Cloudflare Blog
Last Week in AI
Last Week in AI
B
Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
T
Tailwind CSS Blog
V
Visual Studio Blog
博客园 - 三生石上(FineUI控件)
小众软件
小众软件
Google DeepMind News
Google DeepMind News
D
DataBreaches.Net
博客园 - 司徒正美
B
Blog RSS Feed
Microsoft Azure Blog
Microsoft Azure Blog
罗磊的独立博客
Hugging Face - Blog
Hugging Face - Blog
L
LangChain Blog

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания
Yggdrasil-ng: как я переписал Yggdrasil на Rust за 3.5 дн...
Revertis · 2026-04-25 · via Все публикации подряд на Хабре

Уровень сложностиСредний

Время на прочтение9 мин

Охват и читатели0

Обзор

Это опять 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-ы не очень.