[Перевод] Warpnet — Twitter без сервера

Warpnet — одноранговая социальная сеть нанаписанная на Go, в которой каждый пользователь владелец собственной ноды — и клиента и сервера в одном приложении. Здесь я рассказываю, как она устроена, что два года коммитов говорят о моих ошибках в собственном проекте и какие задачи до сих пор не решены.

https://github.com/Warp-net/warpnet
1. Аккаунт, который платит за аренду
Я знаю людей, которых кормит только реклама в одном-единственном аккаунте с большой аудиторией. Платформа может удалить этот аккаунт хоть завтра — не объясняя причин и без права на обжалование, — и в следующем месяце человеку будет нечем платить за квартиру. В таком положении несколько моих знакомых, и именно это в конце концов разозлило меня настолько, что я взялся за дело.
Остальное в этом списке хорошо знакомо: вечная угроза бана, в том числе тихого, когда вы продолжаете публиковать, а вас никто не видит; слежка за каждым вашим действием и перепродажа этих сведений рекламодателям; то, что вашими личными данными, вплоть до номера телефона, владеет платформа. Сеть зарабатывает на вашем контенте, а вы — её источник дохода.
В сентябре 2024 года я решил, что мне хватит умения что-то с этим сделать. Мне нужна была не альтернатива Твиттеру, а другая модель: сеть, где всё хранится на вашем собственном компьютере, где вы сами себе администратор и где никто не может вас ни забанить, ни продать — потому что сделать это просто некому.
Прошло два года, и у этого решения есть осязаемый итог — работающая сеть. Эта статья — рассказ о ней. Кое-какие мои представления о собственном проекте оказались ошибочными, а кое-что из того, что я называл готовым, тогда ещё не существовало.
Поначалу я писал всё это вручную и раз за разом ошибался. Целые подсистемы строились, встраивались, работали — и удалялись. Не одно архитектурное решение я бросил после нескольких месяцев работы: не потому, что появилось лучшее, а потому, что выбранное не выдерживало. Поэтому почти всё, о чём здесь пойдёт речь, — второй или третий ответ на вопрос, на который я сначала ответил плохо; такие места я буду отмечать по ходу. Так честно выглядит эта работа, и именно поэтому мои рассказы о проекте постоянно забегали вперёд самого проекта.
2. Зачем
В следующих главах владению аккаунтом уделено не меньше места, чем форматам передачи данных. Когда я сравниваю Warpnet с Mastodon, Bluesky, Nostr и Farcaster, вопрос не в том, какой протокол они используют, а в том, кто в состоянии что-то у вас отнять и во что это ему обойдётся.
Я пользовался ВКонтакте, Instagram, YouTube и X и видел, как сообщества вырастают на системах, которые даже минимально не контролируют: между вами и вашими читателями сидит посредник со своей бухгалтерией и всё остальное вытекает из одного этого факта.
3. Что на самом деле значит «децентрализованный»
3.1 Три вопроса, которые нужно задать любой сети
Каждая социальная сеть отвечает на три вопроса, признаёт она это или нет: где живут данные, кто может заставить вас исчезнуть и как вас находят незнакомые люди. За словом «децентрализованный» скрываются очень разные ответы на все три.
Почти весь интернет работает по клиент-серверной модели: x.com — центральный сервер, ваше приложение — клиент, который спрашивает у него разрешения на всё, а сервер решает, что можно каждому клиенту. Альтернатива — одноранговая сеть (peer-to-peer, P2P), где каждый участник одновременно и клиент, и сервер. Большого Брата в ней нет, потому что ему негде «ткнуться».
3.2 Четыре формы одного слова
Клиент-сервер. Один оператор держит у себя аккаунты, посты, индекс и правила. X — чистый случай, Bluesky — тоже, если не по замыслу, то по развёртыванию.
Федерация. Много серверов, каждый — маленький клиент-серверный мир, и все говорят на общем протоколе. Архетип — электронная почта, социальная версия — Mastodon. Хозяина можно сменить, но какой-то хозяин есть всегда.
Сети на реле или хабах. Ваша личность — это ключ, а не аккаунт, поэтому серверы становятся взаимозаменяемыми, а ваши подписанные посты живут на тех ретрансляторах, которые вы выберете. Здесь Nostr и Farcaster, а мягкий центр смещается от «админа» к «тем ретранслятором, которыми по факту пользуются все».
Настоящий P2P. Каждый участник и хранит, и раздаёт, а общая инфраструктура тонкая: в основном она нужна, чтобы находить друг друга, плюс ретранслаторы, которые перебрасывают трафик для пиров, застрявших за NAT. NAT — трансляция адресов на уровне роутера: она прячет домашние машины за одним публичным адресом и не даёт посторонним подключиться к ним снаружи. Сюда относятся Secure Scuttlebutt, BitTorrent и Warpnet.
3.3 Торренты, если говорить точно
Российское «правительство» не смогло остановить торренты, потому что rutracker.org — не сервер. Рутрекер никогда не хранит сам контент: у него есть хеш и — исторически — адрес трекера, то есть телефонная книга, которая подсказывает вашему клиенту, у каких пиров лежат куски файла. Куски передаются от пира к пиру, а с 2005 года у BitTorrent есть распределённая хеш-таблица (DHT) — справочник, разложенный по всем пирам: каждый держит свой кусочек, у которого нет владельца, так что трекером может выступить любой пир. Закройте индексный сайт, закройте трекер — рой продолжит работать.
Если перенести это на социальные сети, Mastodon — федерация файлохостингов, Nostr — набор арендованных сидбоксов, Bluesky — один хостинг-провайдер плюс один гигантский краулер. Secure Scuttlebutt и Warpnet — это рой, а ретранслятор Warpnet — трекеры с одной дополнительной обязанностью, которой у трекера никогда не было: перебрасывать трафик для пиров, до которых нельзя дозвониться напрямую.
3.4 Один вопрос
Честная проверка — не вопрос «децентрализована ли сеть?», а другой: если самый популярный сервер этой сети сегодня ночью исчезнет, останутся ли ваши посты? И смогут ли подписчики вас найти?
Mastodon: только если исчез не ваш сервер, а если ваш — то нет и нет.
Bluesky: в теории да, на практике нет.
Nostr: да, если вы пользовались больше чем одним реле.
Farcaster: только пока оплачена аренда хранилища и валидаторы Snapchain продолжают включать ваши сообщения в цепочку; к тому же, в отличие от остальных в этой группе, ваша личность — это платная регистрация в блокчейне, а не просто пара ключей.
Secure Scuttlebutt и Warpnet: ваши посты уцелеют, потому что никогда и не лежали где-то ещё.
Теперь о цене, потому что настоящий P2P расплачивается за отсутствие сервера. Доступность ваших данных зависит от того, в сети ли вы или те, кто их реплицирует — раз. Обход NAT и заряд батареи телефона — два. Для обнаружения по-прежнему нужны bootstrap-узлы, а глобального поиска без индекса не бывает — индекс же и есть сервер. У модерации нет оператора, к которому можно апеллировать - три. Warpnet отвечает на это узлами-модераторами, которые голосуют по каждой жалобе, но платить всё равно приходится: вердикт действует на всю сеть, и попросить кого-то посмотреть ещё раз некого.
4. Честный взгляд на соседей
Прежде чем писать код, я изучил, что уже существует. Альтернативы — хорошие проекты, просто они идут на другие компромиссы, чем я. ActivityPub — раз уж всё дальнейшее опирается на это слово — протокол, которым серверы наподобие Mastodon обмениваются постами, подписками и лайками; серверы, говорящие на нём, и составляют Федиверс.
4.1 Mastodon
Мой вердикт Mastodon звучал так: его претензия на децентрализацию — полуправда, это просто Твиттер, нарезанный на мелкие кусочки, а админ инстанса — царь и бог.
«Админ — царь» верно для одного сервера, но не для сети. Администратор вашего инстанса может вас заблокировать или просто выдернуть вилку из розетки; чего он не может — так это распоряжаться всей платформой, а подписчиков можно перенести на другой инстанс, хотя посты — нельзя. Ещё я утверждал, что скрытие аккаунтов работает через централизованный DNS или через список публичных адресов. Первая половина неверна; вторая, по сути, и есть реальный механизм — блокировки доменов: список на уровне приложения в базе данных админа, где перечислены имена хостов других серверов, которые нужно ограничить или отключить.
Стек — Ruby on Rails, лицензия — AGPLv3 или более поздняя, а не просто GPL, как я упорно её называл. Именно из-за этой лицензии Mastodon не мог запретить Gab или Truth Social использовать свой код; заблокировать их могли только отдельные администраторы, и большинство инстансов закрыли домены Gab в считаные дни после его форка в июле 2019 года. Отсюда ирония, которую мне придётся признать: инструмент дефедерации, который я критиковал, — именно тот, что изолировал Gab.
4.2 Bluesky / AT Protocol
Это единственное суждение, в котором я ошибся начисто. Я был убеждён, что Bluesky даже не претендует на децентрализацию. На самом деле претендует, и очень громко: протокол описывает себя как открытые стандарты децентрализованной публикации, рабочую группу IETF (ATP) учредили в начале 2026 года и объявили о ней в апреле, а её устав лежит в IETF Datatracker.
Формулировка, которую не стыдно опубликовать: децентрализован на бумаге, централизован на деле. По подсчётам самой компании на октябрь 2025 года, 99,99% пользователей сидят на инфраструктуре Bluesky PBC. Хэндлы — это обычный DNS, а центральное звено — каталог идентификаторов did:plc, который ведёт компания. О передаче его швейцарской ассоциации компания объявила в сентябре 2025 года, но ассоциация пока только формируется и ничем не управляет. Personal Data Server, хранящий репозиторий одного пользователя, недорого держать у себя. Дорогие уровни — ретрансляторы, которое собирает всё, что публикует каждый сервер, и AppView, превращающий этот поток в ленты, поиск и счётчики. Единственный AppView на всю сеть компания по-прежнему держит сама. У Blacksky есть независимые PDS, реле и стек модерации, а также, по собственному объявлению проекта, AppView, открытый в публичной бете в июле 2026 года, — но он индексирует не всю сеть. Личные сообщения идут через один центральный сервис и не защищены сквозным шифрованием.
4.3 Nostr
Мой итог по Nostr был таким: он хранит ваш контент на сервере, этот сервер его модерирует, а без сервера вы не подключитесь.
Первые три утверждения верны, но формулировки нужны точнее. Ретранслятор не может подделать подписанную заметку, зато может отказаться её хранить или отдавать, и каждое реле модерирует только то, что хранит само. «Нет ретранслятора — нет подключения» — правда, но «ретранслятор» здесь — любое из нескольких сотен публичных, включая VPS за пять долларов, которую вы поднимете сами, а в модели outbox ваш охват не зависит от одного ретранслятора: ни один в одиночку не способен отрезать вас от подписчиков. Личные сообщения в Nostr шифровались всегда (NIP-04), но выдавали, кто, с кем и когда переписывался; теперь это скрывает gift wrap из NIP-17, хотя внедрён он неравномерно.
4.4 Farcaster
Farcaster я описывал как полудецентрализованную сеть на Ethereum плюс хабы, которая имеет смысл только внутри Ethereum и которую обычный человек запустить у себя не может.
На самом деле, когда я остановился на этом описании, хабов уже не было, и об этом надо сказать прямо, а не поправить втихую. В апреле 2025 года их заменил Snapchain: контент живёт в цепочке, которую синхронизирует небольшой закрытый набор валидаторов (по документации Snapchain, на июль 2026 года их семь, пятью управляет Neynar); личность и аренда хранилища — контракты в OP Mainnet, которых обычный пользователь никогда не касается. Но узлу нужен сервер с 16 ГБ оперативной памяти, четырьмя ядрами и одним-двумя терабайтами диска (в README указан 1 ТБ, в документации — 2 ТБ), а чтобы стать валидатором, требуется согласие действующих операторов. В январе 2026 года, если верить объявлениям обеих компаний, протокол и доминирующий клиент были проданы Neynar. Так что теперь на мягком центре написано название компании.
4.5 Secure Scuttlebutt
SSB я называл по-настоящему децентрализованным и в то же время косной устаревшей технологией, потому что вместо базы данных там журнал, в который можно только дописывать (append-only).
SSB — единственная, кроме нашей, по-настоящему одноранговая архитектура в этом ряду и ближайший предок того, что пытается сделать Warpnet. «Медленный» — справедливо: именно таким был однословный вердикт LWN в статье за апрель 2026 года. Но «устаревший» я бы заменил конкретикой: удаления нет, одна личность на устройство, охват — два рукопожатия, протокол-преемник переименован в PZP и до сих пор не готов для обычных пользователей, а флагманский клиент Manyverse фактически заброшен. И «не поддаётся цензуре» значит ещё и «не поддаётся усилению»: ни одно сообщение не доходит до всей сети.
4.6 Сравнение, коротко
Данные актуальны на сентябрь 2026 года, и у каждой цифры в этом разделе указаны дата и источник — собственный пост или документация проекта, апрельская статья LWN 2026 года о Secure Scuttlebutt или запись рабочей группы ATP в IETF Datatracker, — чтобы читатель, проверяя цифру, сам видел, не устарела ли она с тех пор.
Mastodon. Нужен ли сервер: да (инстанс); где живут ваши данные: в базе данных администратора инстанса; кто может вас выкинуть: администратор инстанса; обнаружение: в пределах инстанса, федеративная подгрузка; модерация: на уровне инстанса, живые администраторы.
Bluesky. Нужен ли сервер: да (PDS, реле, AppView); где живут ваши данные: у вашего провайдера PDS (в 99,99% случаев — у компании); кто может вас выкинуть: провайдер, AppView компании, каталог PLC; обнаружение: реле + AppView; модерация: лейблеры компании, подключаемые лейблеры.
Nostr. Нужен ли сервер: да (хотя бы одно реле); где живут ваши данные: на реле, куда вы публикуете; кто может вас выкинуть: реле может забанить, но ключ у вас никто не отзовёт; обнаружение: списки реле, NIP-05, сеть доверия; модерация: на уровне реле + на уровне клиента.
Farcaster. Нужен ли сервер: да (узлы Snapchain); где живут ваши данные: в Snapchain, удаляются, когда аренда просрочена; кто может вас выкинуть: валидаторы решают, что включать в цепочку, а FID не удалит никто; обнаружение: клиент и API; модерация: оператор клиента.
SSB. Нужен ли сервер: нет (pubs и rooms необязательны); где живут ваши данные: у каждого реплицирующего пира; кто может вас выкинуть: никто; обнаружение: gossip по графу друзей, pubs, rooms; модерация: блокировки по графу друзей.
Warpnet. Нужен ли сервер: нет (реле без состояния); где живут ваши данные: на вашем узле плюс копии у подписчиков; кто может вас выкинуть: админа нет; кворум модераторов может наложить теневой бан на контент, а рейтинг на уровне floor выбрасывает вас из DHT; обнаружение: DHT, gossip, mDNS, реле; модерация: автоматический кворум модераторов, блокировки на каждом узле.
5. Чего я хотел и как это писалось
5.1 Шесть желаний, тринадцать принципов
Я хотел шести вещей: полной автономии без серверов, локального хранения без облаков, отсутствия регистрации, анонимности по желанию, криптографической изоляции и управления сетью без участия людей. Формулировка, под которой я подписываюсь до сих пор: приватность должна быть архитектурной, а не декларативной. Обещание в политике конфиденциальности — всего лишь обещание; архитектура, в которой данные попросту некому украсть или удалить, — факт.
Эти желания отлились в тринадцать общих принципов сети:
Warpnet не может никому принадлежать.
Warpnet должен работать независимо от любых сторонних сервисов.
Warpnet не должен зависеть ни от каких проприетарных или сторонних технологий.
Узел Warpnet должен распространяться одним исполняемым файлом.
Warpnet должен быть кроссплатформенным решением.
На одной машине может работать только один узел участника Warpnet, но у него может быть несколько псевдонимов.
Узлы участников Warpnet должны управляться исключительно консенсусом сети.
Бизнес-узлы Warpnet могут допускать централизованное управление.
Бизнес-узлы Warpnet обязаны ответственно поддерживать хотя бы один bootstrap-узел и один узел-модератор.
Узел Warpnet должен хранить приватные данные только на локальной машине.
Сеть узлов Warpnet не должна разрабатываться или контролироваться одним человеком.
Контент в Warpnet должен модерироваться автоматически, без участия человека; узлы-модераторы должны быть защищены от несанкционированного изменения.
Владельцы узлов несут полную личную ответственность за любой контент, который они загружают в Warpnet.
На четырёх из них держится эта статья: узел (нода) — один исполняемый файл (4); узел хранит приватные данные только на локальной машине (10); контент модерируется автоматически, без участия человека (12); владельцы узлов несут полную личную ответственность за то, что загружают (13). Остальным противоречит реальность. Принципы 8 и 9 описывают «бизнес-узлы» для рынка внутри самой сети — это не реализовано. Принцип 11 гласит, что сеть не должна разрабатываться одним человеком, а три четверти коммитов в этом репозитории — мои. Под принцип 12 код появился рано: ветку с модерацией слили в июле 2025 года. Но понадобилось ещё пятнадцать месяцев, чтобы сделать модерацию автоматической и общесетевой: только в августе 2026 года три модератора начали голосовать подписанными вердиктами.
5.2 Оговорка перед кодом
Одно хочу сказать прямо, потому что об этом спрашивают. Warpnet — не плод вайбкодинга. Основу я написал сам, руками. Android-приложение — сильно переработанный форк Tusky, клиента Mastodon, и его основа — работа авторов Tusky. ИИ-инструменты я использую только для тривиального шаблонного кода, чтобы не выгореть, пока пишу всё в одиночку.
Впрочем, проверить это можно и по git log, и он не так лестен, как эта оговорка. С марта по август 2026 года 557 коммитов — чуть больше четверти всего репозитория — помечены как сделанные с участием ИИ. Бо́льшая часть из них — шаблонный код и рефакторинг. Архитектура и исходная реализация — мои.
Архивный прототип, январь 2025 года: https://github.com/filinvadim/warpnet
6. Стек: один файл
6.1 Продукт — это один бинарник
С самого начала я описывал стек так: написан на Go, фронтенд на Vue 3, все данные — в зашифрованной BadgerDB у вас на диске, весь узел — один исполняемый файл без зависимостей, за которым не стоит ни сервер, ни магазин приложений. В целом это описание по-прежнему верно, с одной оговоркой: десктопная сборка линкуется с системным WebView (на Linux это libwebkit2gtk), и именно поэтому snap-пакет везёт его с собой. Файл embedded.go встраивает в бинарник файл версии, иконку и закоммиченный frontend/dist (147 файлов). Wails — фреймворк на Go, который заворачивает веб-интерфейс в нативное окно и отрисовывает Vue-бандл, так что десктопное приложение — это просто узел, к которому приделано окно.
Данные лежат в ~/.warpdata/<network>/storage/v0 (на Windows — в %LOCALAPPDATA%\warpdata\<network>\storage\v0). Есть две сети со встроенными списками bootstrap-узлов, testnet и warpnet, и у каждой своя база данных и своя версия вашей личности. Почему один файл? Потому что, чтобы получить от него что-то, не нужны ни магазин приложений, ни установщик, ни сервер.

6.2 Четыре роли из одного списка опций
Из одной кодовой базы собираются четыре вида узлов: member (десктопное приложение), remote (-tags remote — тот же узел в headless-режиме, отдающий тот же Vue-бандл браузеру), relay (без состояния, публично доступный, самообновляющийся) и moderator (со своим тегом сборки; локально запускает классификатор модерации и голосует по жалобам). Каждый из них строит свой хост из одного и того же списка опций libp2p — открытого сетевого инструментария, общего для множества одноранговых проектов: транспорты, шифрование, маршрутизация между пирами.
Транспорт ровно один, и протокол защиты тоже один, поэтому умолчания libp2p (голый TCP, QUIC, WebSocket, WebTransport, TLS) не подключаются никогда. Транспорт — TCP, обёрнутый в маскировочный слой, который будет разобран ниже; только TCP — потому что WebRTC и QUIC пока не поддерживают приватные сети. А каждый участник с публичным адресом заодно предлагает себя другим в качестве реле.
Одна из опций, зависящих от роли, заслуживает объяснения уже сейчас: PSK (pre-shared key, заранее распределённый ключ) — секрет, который каждое соединение libp2p обязано предъявить прежде всего остального. PSK в Warpnet — это sha256(network + major version), и в коде прямым текстом сказано, что он публичен: его задача — отделять одну сеть от другой, а несовместимые мажорные версии — друг от друга, вынуждая обновляться. Это метка, а не пропуск, — к этому вопросу мы ещё вернёмся в разделе о маскировке.
7. Ваш пароль и есть ваша личность
7.1 Вход — локальная операция

Регистрации нет — при первом запуске вы вводите имя и пароль. Пароль никуда не отправляется и нужен только для шифрования локальной базы данных. Потеряете его — потеряете аккаунт. Пароль — центральный факт в устройстве системы.
Сервера аутентификации и хранения аккаунтов нет, поэтому «вход» — это то, что ваш узел проделывает сам с собой: проверяет пароль, открывает базу данных и калькулирует криптографическую личность узла. При первом запуске он выпускает для вашего идентификатора пользователя ULID — сортируемый 128-битный идентификатор, который генерируется локально. Только после этого стартует узел libp2p, получая свой peer ID из открытой половины ключа. Ключ — Ed25519, схема подписи, чей 32-байтный открытый ключ достаточно мал, чтобы уместиться прямо в peer ID. Поэтому всякий, кто знает ваш peer ID, знает и ваш открытый ключ, и никому никогда не приходится запрашивать ключ, чтобы проверить вашу подпись.
7.2 Один пароль — три секрета
Каждый корневой секрет вырабатывается с помощью Argon2id — требовательной к памяти функции выработки ключа: одна попытка стоит около 20 миллисекунд и 64 МиБ оперативной памяти, и именно это лишает атакующего ускорения на GPU, которое делает перебор дешёвым. Из одного пароля получаются три секрета, разведённые солями; соль состоит из строки контекста, имени пользователя и — для двух из них — названия сети:
const (
identityKeyContext = "warpnet/kdf/v1/identity-key"
databaseKeyContext = "warpnet/kdf/v1/database-key"
walletSeedContext = "warpnet/kdf/v1/wallet-seed"
)
func DeriveIdentityKey(username, password, network string) (ed25519.PrivateKey, error) {
if password == "" {
return nil, ErrEmptyPassword
}
seed := deriveKey([]byte(password), derivationSalt(identityKeyContext, network, username))
defer Wipe(seed)
return GenerateKeyFromSeed(seed)
}
func DeriveDatabaseKey(username, password string) ([]byte, error) {
if password == "" {
return nil, ErrEmptyPassword
}
return deriveKey([]byte(password), derivationSalt(databaseKeyContext, username)), nil
}
func DeriveWalletSeed(identityKey ed25519.PrivateKey, network, username string) []byte {
return deriveKey(identityKey, derivationSalt(walletSeedContext, network, username))
}
func derivationSalt(parts ...string) []byte {
return ConvertToSHA256([]byte(strings.Join(parts, "\x00")))
}
Ключ личности зависит от сети, ключ базы данных — нет. Поэтому одни и те же учётные данные дают вам одинаковый peer ID на любой машине, в testnet — peer ID, отличный от того, что в warpnet, и один и тот же ключ базы данных в обеих сетях.
Замысел такой: «никакой случайности — закрытый ключ должен быть детерминированным». Сервера, который хранил бы вашу личность, нет, а значит, её должно быть можно воспроизвести из того, что вы знаете. На мой взгляд, файл ключа лишь перенёс бы единую точку отказа на хрупкий артефакт, который можно потерять или скопировать.
Последствия, если говорить прямо: восстановления нет (пока). Потеряли пароль — потеряли и данные, и личность разом; сменили его — и вы уже другой человек. Резервная копия — это копия папки storage плюс логин и пароль у вас в голове. И только один секрет из пароля не выводится: это сессионный токен из 32 случайных байт, который выпускается один раз за запуск и используется только для привязки телефона.
7.3 Неверный пароль — это несовпадение ключа расшифровки
Проверки пароля в привычном смысле, строго говоря, нет вовсе. Badger открывается выработанным ключом; если этот ключ не расшифровывает реестр ключей, Badger возвращает ErrEncryptionKeyMismatch, а узел превращает это в сообщение «wrong username or password». Неверный пароль — это буквально невозможность прочитать данные с диска. Никакой искусственной логики, только природа криптографии.
7.4 Как устроены данные
Ключи подчиняются грамматике /<NAMESPACE>/<root>/<range>/<id>, а сортируемая форма включает перевёрнутую временну́ю метку: math.MaxInt64 минус миллисекунды Unix, дополненные нулями до 19 цифр, чтобы прямой обход выдавал сначала самое новое. Не всё хранится вечно: уведомления истекают через 24 часа, недоставленные исходящие сообщения ждут в очереди 7 дней, привязанный телефон узел помнит 72 часа (не больше 10 устройств), кэшированные копии чужих медиа живут 7 дней, а чужие твиты — 30. Собственный контент узел хранит бессрочно.
8. Задача первая из четырёх: как найти друг друга
Четыре трудные задачи я для себя обозначил давно. Первая из них — дискавери (обнаружение): как участники вообще находят друг друга. Но сначала нужно описать слой, который лежит под ней.
8.1 Noise вместо TLS — а потом TLS как костюм
Решение, которое я принял и повторял всем, кто спрашивал, было таким. Трафик между узлами шифруется протоколом Noise, а от TLS я отказался, потому что он зависит от центров сертификации: представьте, что центр сертификации отзывает сертификат, и вся сеть гаснет (Россия, например, перешла на собственные сертификаты). Noise — это зашифрованное аутентифицированное рукопожатие между двумя «голыми» открытыми ключами, без сертификатов и без кого-либо, кто мог бы что-то отозвать.
А теперь поворот: каждое соединение между узлами обёрнуто в настоящий TLS-туннель, который выглядит как разговор браузера с сайтом.
Причина этого разворота не криптографическая, а политическая. В России, Иране и ряде других стран блокируют уже не по адресам, а сигнатурам протоколов (DPI). Оборудование провайдера изучает форму соединения — первые байты, тайминги, размеры пакетов — и отбрасывает то, чего не узнаёт, или то, что узнаёт слишком хорошо. Поэтому первыми под удар там попадают VPN-протоколы, Tor и вообще всё с необычным рукопожатием: никому не нужно знать, что внутри, достаточно увидеть, что трафик необычный. А для сети вроде моей такое обнаружение равносильно отключению. Отказ от сервера спасает от изъятия сервера, но ни от чего не спасает, когда ваши пакеты заявляют о себе на каждом промежуточном узле между вами и собеседником. То есть создан камуфляж: самое скучное, что есть сегодня в сети, — браузер, открывающий HTTPS к какому-нибудь крупному сайту, и именно под это Warpnet пытается маскироваться.
Но этот туннель — пустая обертка. Никто серты не проверяет, они вообще самоизданные, а рукопожатие Noise внутри остаётся единственным, что кого-либо аутентифицирует; PSK решает лишь, с какой сетью и какой мажорной версией вам разрешено разговаривать. Насколько хорошо этот камуфляж держится в реальной сети под цензурой — отдельный вопрос, отвечаю на него в 8.5.
8.2 Четыре источника, одна очередь
Gossip — схема, при которой пиры пересказывают услышанное нескольким соседям, пока не узнают все; gossipsub из libp2p делает это по именованным топикам. mDNS — это multicast DNS: он кричит на всю локальную сеть, чтобы найти пиров в том же доме.
Дискавери состоит из четырёх именованных источников, которые питают одну очередь: stream, gossip, mDNS и DHT. Ядром дискавери является DHT. Второй источник - gossip: каждые пять минут каждый узел публикует в /warpnet/discovery/1.0.0 свои адреса и до десяти подключённых пиров. Третий по важности - mDNS. Четвёртый источник - stream, работает взаимно: кто запрашивает у вашего узла сведения о нём, тот сам попадает в очередь.
8.3 Ретранслятор (релей)
Ретрансляторы не хранят состояния на диске, обслуживаемый маршрут — один, пользовательского контента — никакого. Тупы как пробка. У них закреплён максимальный приоритет в менеджере соединений, и логика “autorelay” рассматривает в качестве кандидатов только их. Если реле запущено с включённым самообновлением, оно раз в час опрашивает релизы на GitHub и подменяет собственный бинарник.
Ретранслятор начинает работать только после того, как AutoNAT сообщит о публичной достижимости, а до тех пор пиры отвергают его как «не недостижимое».Ретранслятор по определению должен быть поднят на статическом публичном адресе, поэтому он всегда объявляет себя публично достижимым, не дожидаясь проверки: пирам за NAT ретрансляторы нужны сразу - без доп проверок.
8.4 NAT: сбой, которого не видно
NAT (Network Address Translation) или трансляция сетевых адресов — это технология, которая позволяет множеству устройств в домашней или корпоративной сети выходить в интернет через один общий внешний IP-адрес.
По сути вы из дома можете посылать запросы в Интернет, но интернет не может вам посылать запросы - NAT блокирует.
Для сети, где каждый пользователь — сервер, NAT — главная инженерная проблема. Warpnet использует инструменты libp2p: AutoNAT v2 — чтобы узнать, достижимы ли вы; пробивку NAT, или hole punching (DCUtR: два пира за NAT одновременно дозваниваются друг до друга, а момент координирует реле); circuit relay v2 — как запасной вариант; UPnP — чтобы попросить у роутера порт. Отдельные трассировщики записывают в лог каждую попытку пробивки, потому что, как сказано в комментарии в том файле, узел, которому ни разу не удалось пробиться, продолжает работать — просто всё время через реле. Поэтому обход NAT — единственная подсистема, чьи сбои снаружи не видны.
8.5 Маскировка, честная оценка — 5 из 10
DPI (deep packet inspection, глубокая инспекция пакетов) — сетевое оборудование, которое классифицирует трафик по форме, а не по адресу. Тезис «блокировать нечего — сервера нет» верен лишь тогда, когда трафик тоже нельзя опознать по отпечатку и заблокировать. Трафик Warpnet я описывал как похожий на HTTPS из Chrome и оценивал эту защиту на пять из десяти. От этой пятёрки я не отказываюсь.
Что поставляется с тех пор, как этот транспорт стал транспортом по умолчанию: исходящая сторона использует uTLS с отпечатком ClientHello от браузера и фиксированным SNI (именем целевого хоста, которое TLS-клиент отправляет открытым текстом) www.googleapis.com; принимающая сторона предъявляет самодельную цепочку сертификатов на имя Cloudflare, с таймаутами и очередью приёма соединений против активного зондирования. Фрагментация скромнее, чем я её расписывал: один разрыв TCP посреди строки SNI, причём в поставляемой версии — с нулевой задержкой. Можно ли вообще назвать такое фрагментацией? Более того, внимательный цензор способен заметить SNI снаружи, сертификат Cloudflare внутри и сертификат, который не принял бы ни один настоящий TLS-клиент. Так что, на мой взгляд, пять из десяти побеждают дешёвые фильтры, но не дорогие.
9. Протокол: один обработчик, много дверей
9.1 Маршруты — это идентификаторы протоколов
HTTP API в Warpnet нет вовсе. Любая операция — это поток libp2p, будь то ваш собственный интерфейс, где нажали кнопку публикации, привязанный телефон или узел незнакомца, запрашивающий ваши твиты. Поток открывается на идентификаторе протокола, который заодно служит строкой маршрута: /<private|public>/<get|post|delete>/<entity>/0.0.0. Сейчас их 95. По каждому потоку идёт один JSON-конверт: тело, идентификатор сообщения, ID узла, путь, временная метка, версия, подпись; затем отправитель закрывает свою сторону потока, и конец файла служит разделителем кадров. Для простоты поддержки стримы libp2p сейчас эмулируют поведение «реквест-респонс».
9.2 Что покрывает подпись и как это покрытие росло
Раз доверенного сервера нет сообщение само должно доказывать, кто его отправил. Поэтому каждый конверт несёт подпись Ed25519:
// SigningBytes returns the bytes an ed25519 signature covers: the raw body, the
// timestamp as decimal Unix nanoseconds, then the destination route. Senders
// must set Timestamp and Destination first.
func (m Message) SigningBytes() []byte {
ts := strconv.FormatInt(m.Timestamp.UnixNano(), 10)
buf := make([]byte, 0, len(m.Body)+len(ts)+len(m.Destination))
buf = append(buf, m.Body...)
buf = append(buf, ts...)
buf = append(buf, m.Destination...)
return buf
}
Для проверки не нужен никакой обмен ключами, потому что peer ID — это встроенный в него открытый ключ Ed25519: проверяющий извлекает ключ из ID того, кто открыл поток, так что, выдав себя за другой узел, отправитель лишь сделает своё сообщение недействительным.
9.3 Middleware — и агностичность к UI
Каждый обработчик стоит за одной и той же цепочкой: Logging, RateLimiter, Auth, Idempotency. Лимитер нарочно работает до аутентификации, чтобы флуд отбрасывался прежде, чем кто-то потратит процессорное время на проверку подписей. Реализация - leaky bucket на каждый маршрут и каждого пира. Размер ведра зависит от рейтинга пира, но никогда не опускается ниже одного вызова: замедлить можно, вырубить — нет, сеть должна жить. Middleware аутентификации, в свою очередь, следит за разделением на публичное и личное-непубличное: приватные маршруты разрешены только самому узлу, роуту привязки и привязанному телефону.
А теперь трюк: UI не вызывает никакого локального API. Он собирает такой же конверт, какой отправил бы пир, подписывает его ключом владельца и вызывает SelfStream, а тот через внутрипроцессный канал выполняет тот же обработчик, обёрнутый в ту же цепочку middleware. Браузерная панель делает то же самое через WebSocket, а телефон — через настоящий поток libp2p. Итого три транспорта, один обработчик. Напоминает вызов localhost - просто loopback запрос.
9.4 Ленты: свой топик gossipsub у каждого и проверка на входе

У каждого пользователя есть топик gossipsub с именем user-update-<userId>. Подписка на человека подписывает ваш узел на этот топик; написанный твит публикуется в ваш собственный; подписчики проверяют, что известный им ID узла заявленного автора совпадает с подписантом, и только после этого твит попадает в их домашнюю ленту.
Стоит отметить, что ответы через gossip не ходят — для них механизм другой, чем для твитов. Ответ пересылается узлу автора родительского твита, который хранит всю ветку. Пока защиты от злонамеренного встраивания твитов в чужую ленту не предусмотрено.
9.5 Личные сообщения и исходящая очередь

Личные сообщения — это два публичных роута: один создаёт чат на обоих узлах, другой доставляет сообщенин. Если узел собеседника не в сети, сообщение попадает в исходящую очередь на 7 дней и будет отправлено, когда пир снова подключится.
Конфиденциальность при передаче обеспечивает сессия Noise между двумя узлами, а при хранении — шифрование базы данных с каждой стороны. Баннер в окне переписки гласит: «end-to-end encrypted and stored only on your device and theirs», то есть «сквозное шифрование, хранится только на вашем устройстве и на устройстве собеседника». «Два конца» здесь — две ноды с х ключами шифрования. Уведомлений о доставке пока нет.
9.6 Три клиента, коротко о каждом
Десктопный клиент — это Vue-бандл в окне Single Site Browser (SSB) Wails. Удалённый узел отдаёт тот же бандл по HTTP, а все запросы гоняет через один WebSocket, защищённый рукопожатием Noise XX с ключом узла, который браузер закрепляет при первом подключении. Android-клиент — тонкий узел libp2p, который на самом деле полноценным узлом не является; подробнее об этом — в следующих разделах.
10. Задачи со второй по четвёртую — и пятая, которой не было в списке
Обнаружение было первой из четырёх задач, и предыдущий раздел — ответ на неё. Для каждой из оставшихся я привожу одни и те же три вещи: что я себе обещал, что в итоге поставлено на прод и что за код есть.
10.1 Задача вторая: консенсус, который умер и превратился в счётчики
Второй задачей, которую я себе записал, было согласие без участия людей, и первым ответом, пришедшим в голову, стал консенсус — семейство алгоритмов (Raft, Paxos и их византийские родственники), которые заставляют набор машин договориться об одном упорядоченном журнале событий. Первый год он стоял в коде. Сегодня его там нет, и причина заслуживает отдельного объяснения, потому что это одновременно и причина, по которой большую социальную сеть на таких алгоритмах не построить вообще.
Консенсус покупает вам ровно одно: все соглашаются об одном и том же порядке одних и тех же событий. А плату берёт в четырёх валютах, и ни одной из них социальная сеть заплатить не может.
Лидер. Raft и Paxos фиксируют запись через одного избранного лидера, который реплицирует её на большинство. Значит, пропускная способность всего кластера — это пропускная способность одной машины, и эта машина — тот самый сервер, который я убрал. Вернуть лидера только ради подсчёта сердечек было бы анекдотом.
Круговая задержка. Фиксация не завершена, пока её не подтвердило большинство. Между домашними машинами в разных странах это в лучшем случае сотни миллисекунд. А реакция должна появляться мгновенно, и счётчик просмотров популярного поста увеличивает каждый, кто пролистывает мимо. Взаимодействия в социальной сети — мелкие и частые записи, худшая из возможных нагрузок для алгоритма, чья единица работы — путь через полпланеты и обратно.
Состав участников. Этим алгоритмам нужен известный и стабильный набор участников. Социальная сеть — ровно наоборот: ноутбуки закрываются, телефоны засыпают, люди приходят и уходят каждую минуту. Консенсус поверх такого набора — это вечная переконфигурация и кворум, который удаётся собрать редко. Когда половина участников спит, такая сеть не замедляется — она останавливается.
Личность. Между незнакомцами нужен не обычный консенсус, а византийский, который выдерживает f лжецов только среди 3f+1 участников и стоит квадратичного числа сообщений на каждое решение. Обе эти цифры предполагают, что личности — дефицит. В Warpnet личность бесплатна, так что кто-нибудь выпустит десять тысяч личностей и завладеет кворумом. Блокчейны решают это, делая личность дорогой — через доказательство работы или доказательство доли, — а я не хотел ни того, ни другого.
Есть и пятый довод, самый простой: консенсус отвечает на вопрос, который социальная сеть почти никогда не задаёт. Двое людей в разных странах реагируют на два не связанных между собой поста — в каком порядке это произошло? Этот ответ никому не нужен, и ничего от него не зависит. Единственное место, где мне действительно нужно, чтобы несколько машин договорились об одном исходе, — вердикт модерации, и там кворум — три, окно — 30 секунд, а предмет — одна жалоба. Такое голосование сеть может себе позволить, потому что оно случается редко и касается одного объекта.
На деле мой собственный консенсус охранял уникальность идентификатора пользователя — проблему, которую я сам себе придумал. Идентификатор — это ULID, и в нём восемьдесят случайных бит. Коллизию там с пользой не подстроишь, а украденный идентификатор без ключа, выработанного из стоящих за ним учётных данных, ничего не доказывает. Так что удаление всей подсистемы убрало заслон, который ничего не охранял, и потери никто не заметил.
А по-настоящему сходимость нужна была числам, потому что нет центрального табло, которое могло бы авторитетно сказать, сколько у твита реакций. Инструмент здесь — CRDT (conflict-free replicated data type), бесконфликтный реплицируемый тип данных: структура, у которой любые две копии можно слить в любом порядке, без координации, и получить один и тот же результат. Счётчики вовлечённости в Warpnet (реакции, ретвиты, ответы, просмотры, голоса в опросах) — это PN-счётчики, то есть счётчики с положительной и отрицательной частями, реплицируемые через топик gossipsub /warpnet/stats/1.0.0.
Воплощение этой схемы заняло три попытки за один день. Общей ячейки, которую все увеличивают, нет: каждый процесс владеет собственным ключом /STATS/{incr|decr}/{dataKey}/{nodeID}/{generation} и пишет туда свой нарастающий итог, а чтение суммирует все ключи под префиксом. Поколение (generation) — это 128-битный случайный nonce, который выпускается при каждом запуске процесса, чтобы две жизни одного узла никогда не делили один подсчётчик.
Две вещи, которые я об этом говорил, требуют поправки. Я называл это «CRDT-консенсусом». Это не консенсус: никто ни о чём не договаривается. Это сходимость без координации, а единственный настоящий консенсус в Warpnet — голосование модераторов. И ещё я говорил, что счётчики понадобятся для аналитики, — такого кода пока нет.
10.2 Задача третья: люди и запечатанный конверт в каждой фотографии
Третья задача, если одним словом, как я её тогда записал, — люди. Люди — слабое звено любой сети. Я не хотел детской порнографии в сети или подобного . Поэтому каждый медиафайл несёт зашифрованные метаданные о том, кто его загрузил, и шифр подобран так, что обычный человек их прочесть не может, а полиция, потратив пару недель вычислений, — может.
Сегодня каждое изображение и каждое видео, попадая в Warpnet, покидает узел загрузившего с запечатанным блобом. В блобе — полный NodeInfo узла (peer ID, псевдонимы привязанных телефонов, все адреса, которые узел знает за собой), полная запись пользователя и аппаратный адрес каждого сетевого интерфейса. Запечатан он случайной солью, Argon2id с той же стоимостью в 64 МиБ, что и у ключа личности, и AES-256-GCM. Пароль выбирается случайно из 110 000 000 000 значений — это примерно 2^36,7, — используется один раз на всю загрузку и затем стирается.
// Password space: 26^6 × 10^4 × 16 ~= 110 billion.
// Argon2id(t=1, m=64MiB, p=4) ~= 19–22 ms/guess/core.
// Expected brute force on 1000 cores: ~12–14 days.
Кроме того, блоб подписан узлом поверх хеша файла, причём идентификаторы узла и владельца в сам файл не попадают, так что утёкшая копия не раскрывает ничего лишнего. Причина записана в коммите: до этого метаданные ничего не доказывали. Их формат публичен, пароль ничей, а значит, кто угодно мог запечатать блоб с именем невиновного пользователя и приложить его к собственному контенту — и ничто на принимающей стороне его никогда не проверяло. Теперь узел, получающий чужое изображение, сверяет подпись с ID узла пользователя, который его отдаёт, и отказывается от медиа, которые нельзя атрибутировать. Исключение — только медиа, пришедшие через мост из Федиверса, потому что их атрибуция — исходный URL, по которому их индексирует шлюз. При этом изображения перекодируются в JPEG, что уничтожает исходный EXIF камеры, включая GPS; видео свои метаданные сохраняют.
10.3 Задача четвёртая: модерация без людей
Модерация тогда была лишь в планах, и насчёт её формы я был уверен: автоматическая, без людей и несложная. Ушло пятнадцать месяцев и три переделки. Модерация стала работать по жалобам: ничего не сканируется, и единственный спусковой крючок — пользователь, нажавший Report. Выбранная модель — классификатор безопасности на миллиард параметров с открытыми весами, обученный ровно для этого, и время вердикта сократилось примерно до двух секунд. Кроме того, есть кворум модераторов, подписанные вердикты и взаимные аудиты.

Вы жалуетесь на твит или профиль кнопкой Report (на ответы и изображения пожаловаться пока нельзя, на личные сообщения — никогда). Ваш узел ставит на жалобу ваш идентификатор и свой собственный и публикует её в /warpnet/reports/1.0.0. Кто будет судить, решается без единого сообщения: каждый модератор вычисляет sha256(reportID, moderatorID), и это выстраивает всех модераторов в порядке, который невозможно предсказать, пока жалобы не существует, — так что никто не сможет подстроить, чтобы ему достался пост друга. Примерно трое голосуют сразу: каждый оценивает своё место по тому же хешу и по модераторам, которых видел в последнее время; остальные ждут по восемь секунд за каждую позицию в оценке и отступаются, если три бюллетеня уже есть. Каждый голосующий забирает контент с собственного узла того, на кого пожаловались, и запускает модель локально, не видя чужих голосов. Окно — 30 секунд; чётное число бюллетеней урезается до нечётного: отбрасывается бюллетень, стоящий в порядке последним. Для FAIL нужно строгое большинство, а при равенстве голосов решение принимается в пользу контента.
Председателем становится голосующий, который стоит в порядке первым, и он объявляет вердикт; если он молчит десять секунд, его место занимает следующий по очереди. Председатель подписывает вердикт ключом Ed25519, и узел участника, прежде чем что-либо трогать, сверяет подпись с открытым ключом из peer ID председателя, потому что иначе кто угодно мог бы поддельным вердиктом наложить теневой бан на любого пользователя.
Наказание которое я выбрал - изоляция, а не удаление, потому что модератор Warpnet не может залезть в ваш компьютер и стереть файл. Вердикт FAIL публикуется в собственный топик фолловеров нарушителя. Узел нарушителя на свой топик никогда не подписан, поэтому вердикта не слышит; его подписчики слышат, и их клиенты скрывают контент. Тот, кто знает, что его поймали, перепостит то же самое другими словами; тот, кто не знает, продолжает кричать в комнату, которая тихо опустела. Пожаловавшийся получает ответ при любом раскладе.
Кроме того, модераторы выборочно проверяют друг друга контрольными заданиями, взятыми из текстов, по которым сеть уже вынесла решение, и ведут журнал совпадений. Журнал фиксирует репутацию, но никому ни в чём не отказывает: один аудитор, способный дисквалифицировать честных модераторов, был бы атакой хуже той, от которой мы защищаемся.
В приватной тестовой сети из одного участника и трёх модераторов пост с прямой угрозой скрылся примерно через 30 секунд после жалобы, а убитого председателя примерно через десять секунд сменил следующий по очереди. Когда в сети не было ни одного модератора, жалоба принималась, и ничего не происходило. В одном из тестов модель пометила профиль, в котором не было ничего предосудительного. Требование большинства нужно для того, чтобы для ошибки понадобилось несколько машин, а не для того, чтобы сделать ошибки невозможными.
Не решено пока Личность ничего не стоит, а значит, терпеливый атакующий с несколькими машинами и несколькими ключами модераторов — нерешённая проблема. Я отказался от зашитых белых списков, учётных данных, которые выдают bootstrap-узлы, и доказательства работы, потому что каждый из этих вариантов снова создает Большого Брата. Апелляций на решение модераторов пока нет.
И на момент написания статьи контейнер модератора занимает до 2 ГиБ памяти каждый.
10.4 Пятая задача: репутация, когда личность бесплатна

Задача, которую я не включил в те четыре, лежит под всеми остальными: если кто угодно может бесплатно выпустить себе личность, как сети запомнить плохое поведение? Ответ — рейтинги. Узел оценивают соседи, а не он сам: запись, в которой оцениваемый узел совпадает с её автором, отвергается при валидации, а не по договорённости. Рейтинг состоит из трёх измерений: сетевое поведение, социальное поведение и между модераторами. Все начинают с рейтинга 1000, потому что личность бесплатна, а испытательный срок лишь наказал бы честных новичков, а терпеливый атакующий его просто переждет.
Наблюдения складываются в часовые корзины, подписываются наблюдателем и реплицируются через собственный CRDT-топик, чтобы ретрансляторы и модераторы, у которых нет диска, переживали перезапуск. Штрафы затухают с периодом полураспада 12 часов в сетевом измерении и 7 дней в остальных. Собственные наблюдения узла учитываются без ограничений; чужие ограничены — не более 150 от одного наблюдателя и 400 в сумме, так что одна лишь клевета никогда не опустит никого ниже 600. Уровни такие: trusted — от 800, watched — от 500, degraded — от 200, floor — ниже; каждому соответствуют множитель лимита частоты (1; 0,5; 0,25; 0,1 — но не меньше одного вызова), тег в менеджере соединений и оценка в gossipsub. Низкий рейтинг замедляет узел, но никогда его не блокирует; исключения — пир уровня floor выпадает из таблицы маршрутизации DHT и опускается ниже порога graylist в gossipsub.
11. Мосты в другие сети
11.1 Узел не знает, что ActivityPub существует
У Warpnet есть мост в Mastodon и в Threads. Решение, благодаря которому это обошлось так дёшево, на самом деле простое: узел обо всём этом ничего не знает. Я создал шлюз, который может встраивать юзеров Мастодона и Theads в Warpnet как своих родных юзеров. Узел помечает пользователей с той стороны моста названием чужой сети и заводит два стартовых аккаунта, чей домашний узел — peer ID шлюза ActivityPub. Поэтому все уже существующие пути для удалённого пользователя продолжают работать без единого изменения: они открывают поток к домашнему узлу пользователя, а им просто оказывается шлюз.
Шлюз — программа без состояния из соседнего репозитория, которая входит в приватную сеть как обычный пир. Его peer ID задаётся в настройках каждого узла, так что можно запустить собственный. Он не хранит никакого состояния Warpnet, от которого зависел бы какой-либо узел, потому что мост, кэширующий социальный граф, превратился бы в мини-инстанс — тот самый мягкий центр, который Warpnet отвергает. Каждый пользователь может запустить свой шлюз и нацелить на него свой узел (в настройках)
11.2 Бюджетирование и контроль трафика в шлюз
Все лимиты частоты в узле рассчитаны на клиента одного человека, а шлюз — это один peer ID, через который идёт весь Федиверс. То есть шлюз потенциально легко заддосить.Он получает собственный уровень лимитов (всплеск до 600 запросов и 6000 в минуту). По той же причине лимиты relay-цепей выросли до 1 ГиБ и одного часа: шлюз принудительно считает себя недоступным извне, поэтому каждый узел добирается до него через реле. Это позволяет контролировать допуск к шлюзу и “размазать” нагрузку.
11.3 Слияние всех лент
Смешанная лента «Warpnet плюс Mastodon плюс Threads», которую вы видите в примечаниях к релизу, целиком сделана на стороне клиента: K-way merge на фронтенде Vue, по одному источнику на каждый хэндл, на который вы подписаны, и по курсору на каждый источник. В обработчике ленты на узле нет никакой логики бриджа в другие сети. Для Threads понадобился заранее заведённый стартовый аккаунт, потому что Threads не отдаёт ни поиска аккаунтов, ни графа подписок, по которому можно ходить. Отвечать на посты из Threads клиент не даёт: Threads показывает ответы из Федиверса только под постами, чьи авторы это разрешили, а по протоколу никак не узнать, что это за посты.
11.4 Предупреждающий баннер — моя собственная критика слово в слово

Откройте профиль с той стороны моста, и жёлтый баннер сообщит, что вы покидаете доверенную зону: данные этого профиля живут на его собственном сервере, проходят через шлюз открытым текстом и модерируются живыми администраторами. Это мои собственные претензии к иным сетям, превращённые в элемент интерфейса, только без эпитетов
12. Что Warpnet умеет сегодня
Вместо прогулки по журналу коммитов — программа такой, какая она есть. Оговорки я расставил честно, потому что кое-что работает только в одном клиенте, только на локальной машине или только в тестовой сети. В следующей главе перечислено, что слабо или отсутствует вовсе.
12.1 Аккаунт и личность

При первом запуске вы выбираете имя пользователя и пароль. Нет ни регистрации, ни электронной почты, ни номера телефона, ни сервера, который хранил бы ваш аккаунт. Пароль открывает вашу локальную базу данных и заново вырабатывает ключ подписи, поэтому одни и те же учётные данные воспроизводят одну и ту же личность на любой машине, а другой пароль — это просто другой человек. Восстановления нет: потеряете пароль — вместе с ним уйдут и данные, и личность.
12.2 Публикации и чтение
Можно публиковать, отвечать, редактировать, удалять и закреплять — в пределах 280 символов. Цитата — обычный пост со ссылкой на цитируемый; простой ретвит несёт идентификатор исходного с префиксом RT:. Домашняя лента собирается из тех, на кого вы подписаны, — каждый из них публикует в свой топик, — а ответы подтягиваются с узла автора корневого поста, так что разговор приходит целиком, а не обрывками.
Лайков больше нет. Вы реагируете эмодзи через панель выбора с категориями и видите, кто как отреагировал. По сети путешествует только общее число, а разбивка по эмодзи хранится на узле автора; реакции, записанные до этого изменения, читаются как сердечко. В опросе от двух до четырёх вариантов и обязательный срок окончания; за тем и другим следит узел автора, а итоги скрыты, пока вы не проголосуете или опрос не закроется. Просмотры подсчитываются.
12.3 Медиа

К посту можно приложить до четырёх изображений или одно видео размером до 36 мб, а на превью видео показывается кадр из самого файла. В личных сообщениях на десктопе медиа тоже можно отправлять. Входящие изображения перекодируются — при этом исходные метаданные камеры, включая GPS, уничтожаются, — и каждый файл покидает ваш узел с запечатанным блобом, указывающим на загрузившего, как уже было описано выше. Чужие медиа кэшируются на неделю.
12.4 Личные сообщения
В чатах видно превью последнего сообщения и отметки о непрочитанном, а сами чаты отсортированы по свежести. Разговор открывается сразу, даже если собеседник не в сети. В этом случае сообщение ждёт в исходящей очереди до семи дней и доставляется, когда он подключится. Уведомлений о доставке нет. Защита здесь — зашифрованная сессия между двумя узлами плюс шифрование при хранении с каждой стороны, а не сквозное шифрование на уровне сообщений: такого в сети нет.
12.5 Социальный граф и управление им
Можно подписываться и отписываться, смотреть подписчиков и подписки и получать рекомендации, на кого подписаться. У запросов на подписку есть маршруты, хранилище и страница в интерфейсе, но флаг, который делал бы аккаунт закрытым, нигде не читается, так что эта проверка пока ничего не делает.
Блокировки, скрытие пользователей и фильтры по словам — ваши и локальные. Блокировка вдобавок превращается в постоянную запись в чёрном списке пиров, хотя к этому списку узел обращается только тогда, когда выбирает, кого обнаруживать, поэтому входящее соединение он не отклоняет. Фильтры по словам хранит узел, а применяет клиент, и только те, что скрывают пост из домашней ленты. Закладки — просто локальный список.
12.6 Уведомления, поиск, профили
Уведомления приходят об упоминаниях, подписках, запросах на подписку и реакциях. Они сбрасываются, когда вы их открываете, и могут также приходить на электронную почту. Есть уведомление, что сеть обнаружила нового пользователя. Можно искать людей, а в профилях работают вкладки Media и «Tweets and threads», которые подгружаются лениво, поэтому чужой профиль открывается быстро.
12.7 Федиверс и деньги

Аккаунты Mastodon и Threads видны изнутри Warpnet через шлюз ActivityPub: на них можно подписываться и их можно читать, а их посты перемешаны с постами Warpnet в одной ленте, причём рекомендации разделены по источникам. На профилях с той стороны моста висит закрываемое предупреждение , что их контент живёт на чужом сервере и модерируется его администраторами. Подписки, ответы и реакции ходят в обе стороны. Личные сообщения аккаунтам с той стороны моста не принимаются, а отвечать на посты из Threads клиент не даёт — по причине, уже названной выше.

Кошелёк есть только в тестовой сети. Он работает в Nile — тестовой сети TRON, хранит USDT и TRX, выводит свой сид из ключа личности, а не хранит ключ для расходования средств, и находит контакты, спрашивая у тех, на кого вы подписаны, их выработанный адрес. Свежий адрес не может ничего отправить, пока на нём нет хоть сколько-нибудь TRX, потому что комиссии платятся в TRX.
12.8 Жалобы, модерация и рейтинги
С десктопа можно пожаловаться на пост или профиль и своими словами написать причину; с телефона — только на профили. На ответы и изображения жаловаться нельзя, а личные сообщения не модерируются вообще. Жалоба уходит узлам-модераторам, примерно трое из них независимо её рассматривают и голосуют, и при любом исходе вы получаете ответ. Ничего не удаляется. Контент, не прошедший проверку, скрывается от тех, кто иначе увидел бы его. Всё это зависит от того, в сети ли модераторы, а на момент написания статьи машины модераторов тестовой сети выключены.
Отдельно узлы оценивают друг друга по поведению в сети, в приложении и в модерации. Плохой рейтинг замедляет запросы узла и понижает его шансы при выборе пиров, но никогда не блокирует его полностью.
12.9 Как перенести архив Твиттера

Импорт архива Твиттера возможен. Клиент распаковывает его, оставляет ваши собственные посты с фотографиями (до четырёх в каждом) и по одному передаёт их вашему узлу, где фотографии обрабатываются как любая другая загрузка.
12.10 Клиенты
Есть три способа достучаться до узла, и все они говорят на одном протоколе с одними и теми же обработчиками.
Что это. Десктопное приложение: сам узел, с окном; браузерная панель: тот же интерфейс, который отдаёт headless-узел; телефон: тонкий клиент, привязанный к вашему узлу.
Чтение и публикация. Десктопное приложение: да; браузерная панель: да; телефон: да.
Личные сообщения, реакции, закладки. Десктопное приложение: да; браузерная панель: да; телефон: да.
Жалобы. Десктопное приложение: на посты и профили; браузерная панель: на посты и профили; телефон: только на профили.
Привязка телефона. Десктопное приложение: показывает код; браузерная панель: нет; телефон: сканирует код.
Глубокие ссылки на профили. Десктопное приложение: да; браузерная панель: нет; телефон: нет.

Телефон — не узел в том смысле, в каком узел — десктоп. На нём работает намеренно урезанный сетевой стек без адреса для входящих соединений и с жёстким бюджетом памяти, и он выступает псевдонимом (alias) аккаунта, живущего на вашем десктопном узле: вы один раз привязываете его, сканируя код, и после этого он может говорить от имени этого аккаунта, пока действует привязка. Узел помнит не больше десяти привязанных устройств. Уведомления на телефон приходят за счёт регулярного опроса вашего собственного узла, потому что push-сервиса посередине нет.
12.11 Платформы и сети
Есть сборки для Linux на Intel и ARM, для macOS и для Windows, плюс пакет для Android и бинарник реле. Для Linux есть ещё пакет в бета-канале Snap Store. Сборка для Android упакована для F-Droid и ждёт там проверки, так что пока единственный вариант — файл в релизах.
Один и тот же бинарник играет несколько ролей в зависимости от того, как он собран: полноценный узел, который запускает большинство; headless-узел, отдающий свой интерфейс браузеру; реле без состояния, помогающее новым узлам находить пиров; модератор с классификатором безопасности; и тестовый бот. Есть две отдельные сети, основная и тестовая, у каждой свои реле, своя база данных и своя версия вашей личности, так что ничего из сделанного в тестовой сети не последует за вами в основную.
13. Чего он не умеет и где он слаб
Личность.
Восстановления пароля нет — так задумано.
Соль детерминирована — это хеш фиксированной строки контекста и вашего имени пользователя, а для ключа личности к ним добавляется сеть, — поэтому украденную базу данных можно атаковать офлайн примерно по 20 миллисекунд на попытку. Стойкость пароля решает всё.
Закрытый ключ лежит на диске внутри зашифрованной базы данных — через peerstore libp2p. При выходе из аккаунта его никто не вычищает из памяти, а на Windows нет вообще никакой защиты от отладки.
Сеть.
Чёрный список фильтрует только исходящее обнаружение. Connection gater не зарегистрирован, поэтому заблокированный узел всё ещё может к вам подключиться. Лестница нарастающих банов в коде есть, но ни разу не вызывается.
Камуфляж я сам оцениваю на 5 из 10.
Неудачные пробивки NAT снаружи не видны. Оператор реле видит, кто, с кем и когда соединялся.
Протокол.
Уведомлений о доставке нет.
Нет защиты от встраивания контента в таймлайн.
Удаление твита рассылается подписчикам через gossip по приватному маршруту: узел автора публикует его в топик своих подписчиков как
/private/delete/tweet, и у каждого подписчика сообщение приходит как приватный маршрут от чужого пира — а middleware аутентификации его отклоняет; тест вcore/middlewareпроверяет именно этот отказ. Поэтому узлы подписчиков продолжают показывать твиты, которые автор удалил, пока кэшированная копия не истечёт по собственному расписанию.Согласования версий нет: изменение формата подписи раскалывает сеть.
Данные, счётчики, рейтинг.
Ваши посты доступны, когда вы в сети. Удаление на других узлах гарантировать невозможно. Псевдонимность, а не анонимность.
Приращения статистики не аутентифицированы — любой узел может увеличить счётчик любого твита под собственным ключом — и никогда не убираются сборщиком мусора. TODO.
Веса рейтинга не откалиброваны. Личность бесплатна, поэтому узел, опущенный до floor, начинает заново с 1000. А из-за ограничения на чужие наблюдения для узла, на который атаки не было, рейтинг атакующего не опускается ниже 600.
Модерация.
Личности бесплатны, а проблема корня доверия не решена. Участники проверяют только подпись председателя, поэтому одна пара ключей модератора может выпустить действительный вердикт «тройки».
Модераторы анонимны и при каждом запуске случайны — так решено; результаты аудита никак не применяются. Апелляций нет. Модель ошибается в обе стороны.
Зафиксированная версия движка не применяет шаблон безопасности модели, поэтому любой вердикт «unsafe» читается как первая категория опасности.
Нет модерации медиа.
Медиа.
Запечатанный блоб вскроет любой, у кого есть около тысячи ядер на две недели, а не только полиция.
В нём MAC-адрес каждого интерфейса и запись пользователя — тот самый компромисс, о котором шла речь.
Враждебный клиент может запечатать мусор и подписать его: подпись привязывает файл к узлу, но не к правдивости блоба.
Медиа, пришедшие через мост из Федиверса, полностью освобождены от проверки подписи.
Шлюз и кошелёк.
По умолчанию peer ID шлюза один-единственный; контент, идущий через мост, лежит там открытым текстом.
TronGrid — централизованная зависимость (честно говоря, ни на что не влияющая).
Проект.
Принцип 11 гласит, что сеть не должен разрабатывать один человек. Разрабатывает один человек. =(
Поддержки Apple пока нет — у меня нет подписки Apple Developer.
14. Попробуйте сами
На Linux: sudo snap install warpnet --beta, а затем warpnet; в стабильном канале пока ничего не опубликовано. Начните с --node.network testnet: у этой сети своя база данных, и peer ID там другой, так что ничего из сделанного там не последует за вами в основную сеть. В релизах на GitHub лежат сборки для Linux amd64 и arm64, macOS, Windows, бинарник реле и APK для Android размером около 42 МБ. В F-Droid Android-приложения пока нет: упаковка готова, merge request в fdroiddata открыт и ждёт ревьюера, поэтому пока его не примут, APK в релизе на GitHub — единственная существующая сборка.
При первом запуске вы выбираете имя пользователя и пароль длиной от 8 до 32 символов, в котором есть заглавная буква, строчная буква, цифра и спецсимвол. Этот пароль и есть ваша личность: потеряете его — потеряете аккаунт, и никто не поможет. Резервная копия — это копия ~/.warpdata/<network>/storage плюс учётные данные.
Чтобы привязать телефон, откройте меню аккаунта на боковой панели десктопного приложения, выберите «Sign in by QR-code» и отсканируйте код в приложении на телефоне; в Settings → Devices видно, что уже привязано, и там же привязку можно снять. На headless-машине docker compose up запускает удалённый узел, чья браузерная панель при первом подключении закрепляет ключ узла; флага для пароля нет. Разумеется, это программа версии до 1.0. Готовьтесь к шероховатостям и сообщайте о них.

15. Помощь
О помощи я уже просил раньше, куда более резкими словами, чем эти: пожалуйста, помогите, я выбился из сил, пока писал всё это в одиночку. Два года спустя это по-прежнему, по сути, один мейнтейнер и горстка пул-реквестов со стороны. Просьба остаётся в силе — в том же духе и с меньшим количеством мата.
В HOW-TO-HELP.md перечислено семь способов. Тестируйте: запустите узел на своей платформе, ломайте интерфейс, рассказывайте, что ломается. Чините код и — пожалуйста — фронтенд. Напишите ещё один клиент или мобильный псевдоним. Проведите аудит кода Noise, PSK и выработки ключей; раздел 13, по сути, готовое меню для такого исследования. Пишите и переводите документацию; устаревшие ссылки на like.go в ONBOARDING — открытая задача. Поддержите инфраструктуру: поднимите, пожалуйста, публично доступное реле. Расскажите о проекте другим.
Одна оговорка к последней из этих рекомендаций. Узел участника с публичным адресом уже обслуживает relay-цепи, но сегодня никто не подхватывает их автоматически: autorelay каждого узла закреплён за тремя зашитыми в код bootstrap-адресами, так что, чтобы вычеркнуть «три контейнера на одном IP» из списка слабостей, нужно ещё и добавить новые реле в этот список или передавать их через --node.bootstrap.
С чего начать: docs/ONBOARDING.md — для знакомства с кодом, метка good-first-issue на GitHub, группа в Telegram t.me/warpnetdev и доступное только для чтения зеркало веток main и develop на Codeberg. Новые файлы несут заголовок AGPLv3.
16. Заключение
В разделе 3 был задан один вопрос: если самый популярный сервер этой сети сегодня ночью исчезнет, останутся ли ваши посты и смогут ли подписчики вас найти? Для Warpnet ответ на первую часть — да, на вторую — в основном да: ваши посты никогда не лежали где-то ещё, а слабее именно вторая половина — снова найтись, потому что три bootstrap-реле стоят на одной машине. У оставшихся мягких мест есть имена: три реле на одном адресе, три модератора в testnet и один шлюз в Федиверс. Собрать и запустить любое из трёх может кто угодно, хотя сами подключаются только реле и модераторы: peer ID шлюза — значение по умолчанию, заданное при компиляции, поэтому второй шлюз обслуживает только те узлы, в настройках которых указан он. И все три по-прежнему держу я.
За два года proof of concept превратился в сеть с десктопным приложением, телефоном, headless-сборкой, подписанными конвертами, запечатанными медиа, кворумом модерации, рейтингами и мостом в те самые сети, которые я критиковал. Кое-что из того, что я называл готовым, целый год на самом деле существовало лишь в виде комментариев в коде. Самая трудная открытая проблема — не техническая: личность ничего не стоит, и каждый слой над ней построен так, чтобы назначать цену поведению, потому что назначить цену личности значило бы снова поставить кого-то посередине. Я ставил себе целью построить сеть, которую нельзя отключить. Точная формулировка того, что получилось: сеть, посередине которой нет ничего, что можно было бы отключить, а всё остальное ещё предстоит сделать.
Люди из первого раздела по-прежнему платят за квартиру доходами с тех самых аккаунтов. Разница, которую я могу предложить, — в том, у кого ключ. Чтобы понять, стоит ли она чего-нибудь, запустите сегодня вечером узел в testnet, а если у вас есть публичный адрес — поднимите реле.
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.