Перейти к основному содержимому

Запуск хаба

b4hub - это сервис, который стоит за hub.b4core.app. Он хранит каталог, принимает общие сеты, отчёты и жалобы от роутеров, подписывает то, что публикует, и раздаёт консоль модерации. Это отдельный от b4 бинарник со своей нумерацией версий, и на роутере он не запускается.

Работать он может в одном из двух режимов.

Свой хабЗеркало
Командаb4hub serveb4hub mirror
КаталогСобирается из сетов, которыми поделились с нимКопируется у вышестоящего хаба
Ключ подписиСвой, из b4hub keygenДля каталога нет, подпись остаётся чужой; зеркало со своим ключом подписывает им объявление и записи, которые передаёт
МодерацияСвои модераторы и своя консольНет
Записи от роутеровСохраняет и обрабатываетПроверяет и передаёт наверх; отчёты и жалобы ждут в ограниченной очереди, пока он недоступен
База данныхSQLite в каталоге данныхНет
Что нужно сообщить роутеруАдрес и идентификатор ключаНичего, после одобрения вышестоящим хабом

Свой хаб - это второй каталог. В нём лежат сеты, одобренные его собственными модераторами, отчёты считаются отдельно, и роутер попадает на него только после того, как в настройках вручную указаны и адрес, и идентификатор ключа. Зеркало - это тот же самый каталог по второму адресу: оно проверяет подпись вышестоящего хаба, копирует файлы и передаёт ему то, что отправляют роутеры.

к сведению

Поле Зеркала хаба, описанное в разделе Хаб сообщества, не имеет отношения к зеркалам обновления - хостам, с которых b4 скачивает свои собственные релизы.

Сервис​

Один статический бинарник со встроенной консолью. Каждая команда принимает --data - каталог, в котором лежит всё, что принадлежит сервису.

КомандаЧто делает
b4hub keygenСоздаёт ключ подписи и печатает его идентификатор. Существующий ключ перезаписать отказывается.
b4hub serveЗапускает хаб: сборщик каталога, точки входа для роутеров и консоль.
b4hub mirrorЗапускает зеркало другого хаба.
b4hub buildОдин раз собирает и подписывает каталог и завершается. Флаги --new-epoch и --revoke делают то же, что страница «Каталог» в консоли.
b4hub moderateДелает из командной строки большую часть того, что делает консоль: версии, сеты, ключи, жалобы, зеркала, история сборок и журнал.
b4hub versionПечатает версию хаба и дерево исходников b4, из которого он собран.

У флагов ниже есть переменная окружения, и обычно её и задают в unit-файле или в контейнере. Флаги отдельных команд - --new-epoch, --revoke, --new-database, --announce, --refresh и --queue-limit - задаются только в командной строке.

ФлагПеременнаяПо умолчанию
--dataB4HUB_DATAdata
--listenB4HUB_LISTEN127.0.0.1:7100
--public-urlB4HUB_PUBLIC_URLпусто
--geosite-urlB4HUB_GEOSITE_URLрелиз v2ray-rules-dat
--geoip-urlB4HUB_GEOIP_URLрелиз b4geoip
--upstreamB4HUB_UPSTREAMпусто, обязателен для mirror
--upstream-keyB4HUB_UPSTREAM_KEYпусто, то есть встроенный ключ
--trusted-proxiesB4HUB_TRUSTED_PROXIESпусто
флага нетB4HUB_ADMIN_PASSWORDпусто, и тогда консоль закрыта
флага нетB4HUB_TELEGRAM_TOKEN, B4HUB_TELEGRAM_CHATпусто; если заданы, они важнее настроек уведомлений Telegram в консоли
флага нетB4HUB_WEBHOOK_URL, B4HUB_WEBHOOK_SECRETпусто; если заданы, они важнее настроек уведомлений на webhook в консоли

В каталоге данных лежат hub.key (seed ключа подписи), hub.db (хранилище SQLite), secret (создаётся при первом запуске), blobs/ (файлы payload по хешу), public/ (подписанные файлы, которые скачивают роутеры) и geo/.

warning

b4hub говорит только по обычному HTTP и сам TLS не терминирует. Адрес по умолчанию - локальный, потому что предполагается обратный прокси перед ним. Комплект для развёртывания с vhost для nginx, unit-файлом systemd и файлом Compose опубликован в репозитории b4hub вместе с релизами.

Что запрашивают роутеры​

ПутьОтвет
/b4/healthok
/b4/hub/manifest.jsonПодписанный манифест: файл каталога, его размер и хеш, список зеркал, отозванные ключи, срок действия
/b4/hub/catalogue-<epoch>-<seq>.json.gzСам каталог
/b4/hub/blob/<sha256>Один файл payload
/b4/hub/v1/msgКуда отправляются общий сет, отчёт и жалоба
/b4/hub/v1/networkASN, страна и название провайдера, в которых хаб видит обратившегося

Зеркало раздаёт всё это, кроме /b4/hub/v1/network: для этого нужен собственный взгляд хаба на сеть.

Свой хаб​

b4hub keygen --data /var/lib/b4hub

Команда печатает одну строку - идентификатор ключа. Только он и опознаёт этот хаб для роутера, и создаётся он один раз: команды смены ключа нет, а keygen отказывается перезаписать существующий hub.key.

Пароль модерации читается из окружения и больше ниоткуда:

B4HUB_ADMIN_PASSWORD=... b4hub serve --data /var/lib/b4hub \
--listen 127.0.0.1:7100 --public-url https://hub.example.org

--public-url - это адрес, который хаб объявляет для себя в манифесте, поэтому указывать стоит тот, по которому роутеры действительно к нему приходят, а не локальный.

warning

Без B4HUB_ADMIN_PASSWORD хаб раздаёт каталог как обычно, но консоль закрыта: страница входа сообщает, что модерация не настроена, а вход и любое действие модератора отклоняются. Тогда модерация возможна только через b4hub moderate, и он работает рядом с сервисом: хранилище - это SQLite в режиме WAL, поэтому запущенный хаб его не блокирует. Каждое изменение оставляет запрос на сборку, который хаб подхватывает примерно за десять секунд.

Подключение роутера​

В Настройки, Интеграции, Хаб сообщества вводятся два значения: базовый адрес в поле Зеркала хаба и идентификатор ключа в поле Публичный ключ хаба.

warning

Нужны оба поля. Один только ключ заменяет тот, которому доверяет b4, но hub.b4core.app исчезает из перебираемых адресов лишь после того, как в поле Зеркала хаба указан собственный хаб. С заданным ключом и пустым полем адресов каждая синхронизация всё равно доходит до hub.b4core.app и отклоняет его каталог из-за чужой подписи.

Адрес должен начинаться с https://, без учётных данных, параметров запроса и якоря. Обычный http:// принимается только для localhost, петлевого, частного или link-local адреса, то есть для хаба в той же сети, но не для публичного имени. Адрес, который b4 не принимает, отбрасывается молча.

Ключ, не совпадающий с тем, которым подписывает хаб, проваливает каждую синхронизацию с ошибкой «manifest is not signed by a trusted key», а сохранённый каталог отбрасывается, как только ключ, которым он подписан, перестаёт быть доверенным; страница Сообщество остаётся пустой до первой удачной синхронизации.

Каталог, который он публикует​

Пока модератор не одобрит версию, не публикуется ничего, поэтому новый хаб начинает с пустого каталога.

СборкаПроверяется при запуске и затем каждые 5 минут; сборка идёт, когда ещё ничего не опубликовано, когда что-то изменилось или когда последней уже сутки
После действия модератора или автоматического скрытияСборка запускается в фоне через 1,5 секунды после последнего изменения и не позже чем через 10 секунд после первого; запрос, оставленный b4hub moderate, подхватывается примерно за 10 секунд
Файлpublic/catalogue-<epoch>-<seq>.json.gz, хранятся три последних
МанифестПодписан, действителен 14 дней с момента сборки
PayloadРаз в час удаляются файлы, на которые не ссылается ни одна сохранённая версия и которые записаны больше часа назад

Роутер принимает манифест, только если он новее имеющегося: больше эпоха или та же эпоха с большим порядковым номером. Более старый отклоняется, и роутер переходит к следующему адресу.

К своим сборкам хаб применяет то же правило. Сборка, чьи эпоха и порядковый номер не окажутся больше и каталога в public/, и самого нового каталога, который одобренное зеркало отдавало при последней проверке, отклоняется, не расходуя порядковый номер, а причина видна в истории сборок. Перед первой сборкой после запуска хаб один раз проверяет зеркала, чтобы это сравнение и список зеркал описывали зеркала такими, какие они сейчас, а не какими были до остановки хаба.

warning

База, которая ещё ни разу не публиковала каталог, не публикует поверх существующего. Её первая сборка отклоняется, если в public/ уже лежит каталог или если она подписала бы пустой каталог ключом, встроенным в b4, - так выглядит serve, запущенный с рабочим ключом не на том каталоге данных. Флаг --new-database у serve или build подтверждает, что новая база задумана. Новая эпоха этот отказ не снимает: признаком служит первая успешная публикация, а не эпоха.

к сведению

Хаб, который перестал собирать каталог, деградирует, а не ломается. Роутеры сохраняют последний полученный каталог, помечают его истёкшим через две недели, в течение которых действителен манифест, и продолжают работать с уже применёнными сетами.

Сеть, к которой относят вклад​

Хаб определяет ASN и страну адреса, с которого пришла запись, и именно к ним относится отчёт. За обратным прокси хаб видит адрес прокси, если только прокси не находится на петлевом адресе или не указан в --trusted-proxies адресом или подсетью.

warning

Недоверенный прокси схлопывает весь хаб в одну сеть. Любой вклад относится к прокси, сетевые лимиты делятся между всеми сразу, а при частном адресе прокси ASN не записывается вовсе, и отчёты теряют оценки по провайдеру и по стране.

Записи, которые передаёт зеркало, приходят с адреса зеркала. Зеркало на b4hub 1.3.0 или новее со своим ключом подписывает всё, что пересылает, - запись вместе со временем, - и хаб проверяет эту подпись по зеркалам, одобренным на его странице «Зеркала». Подпись, расходящаяся с часами хаба больше чем на 10 минут, не засчитывается, поэтому часы зеркала должны быть синхронизированы, а одобрение или отклонение зеркала начинает действовать в течение минуты. Если подпись сходится, хаб сохраняет запись без сети: отчёт «работает» или «не работает» учитывается только в общей оценке, с весом непроверенного источника, жалоба никогда не идёт в счёт автоматического скрытия, а сетевые лимиты для адреса зеркала в 10 раз выше по запросам и в 20 раз выше по новым ключам, потому что их делят все роутеры за этим зеркалом. Запись от более старого зеркала, от зеркала без ключа или неодобренного, а также с неверной подписью относится к адресу, с которого пришла, как любая другая.

ASN определяется DNS-запросом, а не по локальной базе, поэтому хосту хаба нужен работающий исходящий DNS. Файлы geosite.dat и geoip.dat, которые сервис скачивает раз в сутки, объявляются в манифесте, но для определения сети не используются.

Зеркало чужого хаба​

Зеркало раздаёт каталог хаба, которым не управляет. Раз в пять минут оно проверяет манифест вышестоящего хаба и, когда появляется более новый, скачивает каталог и те файлы payload, которых у него ещё нет, проверяет каждый из них, складывает payload в свой blobs/, а каталог и манифест записывает в public/, чтобы раздавать как есть.

b4hub mirror --data /var/lib/b4hub-mirror \
--upstream https://hub.b4core.app \
--listen 127.0.0.1:7101

--upstream - единственный обязательный флаг. Зеркалу собственного хаба нужен ещё --upstream-key с идентификатором ключа того хаба; без него единственный ключ, которому зеркало доверяет, - встроенный ключ hub.b4core.app.

Что проверяется до того, как что-либо скопировано: манифест подписан доверенным ключом, имя файла каталога имеет ожидаемый вид, манифест новее уже имеющейся копии, скачанный файл точно того размера и с тем SHA256, которые названы в манифесте, он разбирается, и каждый файл payload совпадает с хешем из каталога. Сбой на любом шаге оставляет прежнюю копию на месте, и попытка повторяется при следующей проверке.

к сведению

Зеркало продолжает работать при недоступном вышестоящем хабе. При запуске оно загружает то, что скопировало последним, поэтому даже перезапуск во время сбоя не мешает ему раздавать последний полученный каталог.

Корень зеркала, /, - страница для браузера: какой хаб оно зеркалирует, какой каталог хранит с номером сборки, датой сборки и сроком действия, когда вышестоящий хаб проверялся в последний раз и удалась ли проверка, и чем закончилось последнее объявление. Язык страницы - английский или русский по языку браузера. Тексты ошибок, запись объявления и ключ подписи на неё не попадают; они есть в журнале зеркала.

Почему роутер оказывается на зеркале​

Роутер перебирает адреса по порядку: сначала заданные вручную, затем встроенный, затем выученные. Ответить может любой из них, и ответ - один и тот же подписанный каталог, поэтому для успешной синхронизации достаточно, чтобы отозвался хоть один адрес. Зеркало - это второе место, откуда можно взять те же самые байты, для сети, в которой хаб не отвечает; выученное зеркало, которое должно идти первым, вписывается в поле Зеркала хаба вручную - тогда оно встаёт перед хабом, а остальные остаются за ним.

Зеркала выучиваются сами. Каждый подписанный манифест несёт список зеркал, которые хаб одобрил; роутер, проверивший манифест, сохраняет этот список рядом с подписавшим ключом, не больше 32 адресов, и добавляет его к заданным вручную. Смена поля Публичный ключ хаба этот список отбрасывает: списку зеркал доверяют ровно настолько, насколько доверяют объявившему его ключу.

Объявление зеркала​

Зеркало, которое должно попасть в этот список, объявляет себя:

b4hub keygen --data /var/lib/b4hub-mirror

b4hub mirror --data /var/lib/b4hub-mirror \
--upstream https://hub.b4core.app \
--public-url https://mirror.example.org --announce

Для --announce нужны и собственный ключ, чтобы подписать объявление, и --public-url - объявляемый адрес. Объявление уходит при запуске зеркала и затем раз в сутки. Адрес должен быть https:// или http:// для localhost, петлевого, частного или link-local адреса, не длиннее 200 символов, без учётных данных, параметров запроса и якоря.

warning

Объявление ничего не публикует. На вышестоящем хабе оно появляется со статусом ожидает и не объявляется роутерам, пока модератор не одобрит его на странице «Зеркала» консоли того хаба. Повторное объявление обновляет время последнего появления и никогда не меняет уже принятое решение, поэтому отклонённое зеркало остаётся отклонённым. Одобренное или отклонённое зеркало сохраняет и ключ, с которым по нему приняли решение: объявление его адреса, подписанное другим ключом, отклоняется с mirror_key_mismatch, и освободить адрес можно только удалением зеркала на той странице. Пока зеркало ожидает решения, ключ задаёт последнее объявление.

После одобрения зеркало проверяется раз в 10 минут и ещё раз перед первой сборкой после запуска хаба: его /b4/health должен отвечать ok, а манифест - проходить проверку ключом самого хаба. Оно остаётся в манифесте, пока проверка удавалась в последние 24 часа, так что кратковременный сбой его не выбрасывает, а длительный выбрасывает. Исключение - сбой сразу всех одобренных зеркал: чаще это значит, что хаб не может до них достучаться, а не что они все пропали, и тогда хаб продолжает объявлять одобренные зеркала из предыдущего манифеста, а отклонение или удаление зеркала по-прежнему убирает его.

Записи, отправленные через зеркало​

У зеркала нет базы данных, и решений модерации оно не принимает, но то, что передаёт, проверяет. Запись должна разбираться и нести верную подпись, а ключ и подпись - быть в каноническом кодировании; иначе зеркало само отвечает тем же bad_record или bad_signature, что ответил бы хаб, и до хаба запись не доходит. Запись незнакомой зеркалу версии формата или незнакомого вида уходит хабу без проверки. Одна клиентская сеть, /24 для IPv4 или /48 для IPv6, может отправить 600 записей в час; сверх этого она получает 429 до конца часа.

Когда вышестоящий хабЧто получает роутер
Отвечает в течение 15 секундСобственный ответ хаба, без изменений
Не отвечает или отвечает 502, 503, 504Отчёт «работает» или «не работает» и жалоба ставятся в очередь с ответом 202 queued; общий сет и объявление зеркала получают 502 hub_unreachable
Отвечает 429 на адрес самого зеркалаОтчёты и жалобы ставятся в очередь; всё остальное получает 503 hub_busy, и роутер пробует следующий адрес

После двух сбоев подряд зеркало перестаёт ждать вышестоящий хаб. 30 секунд, с удвоением до 5 минут, оно отвечает сразу, не обращаясь к нему, а затем пропускает одну запись для проверки. Успешная проверка манифеста снимает паузу.

В очереди не больше --queue-limit записей, по умолчанию 5000, а 0 значит не держать ни одной; не больше 200 в сутки от одной клиентской сети и 40 в сутки от одного ключа; и только записи до 8 КиБ. Сверх лимита ответ - 502 hub_unreachable, 503 hub_busy, пока хаб ограничивает зеркало, или 503 queue_full, когда очередь заполнена, и роутер оставляет запись в своей очереди на потом. Записи из очереди уходят наверх при каждой проверке, от старых к новым и не больше 100 за раз, чтобы накопившееся не расходовало сетевой лимит хаба, нужный и живым записям. Запись, которую хаб не принял из-за лимита ключа, ждёт, пока лимит не снимется. После того как хаб отказал незнакомому ему ключу, записи других ключей тоже ждут, кроме ключей, которые хаб принял через это зеркало с момента его запуска. Файлы очереди хранятся 30 дней и доступны только пользователю сервиса, а журнал зеркала при каждой проверке пишет, сколько записей ждёт.

Запись, вернувшаяся на то же зеркало, пока она ещё в пути, отклоняется с 508 relay_loop - так останавливается и зеркало, чей вышестоящий адрес ведёт обратно на него же. Зеркало не запускается, если --upstream совпадает с его --public-url, а проверка манифеста, попавшая на само зеркало, завершается ошибкой с объяснением.

warning

Лимиты на клиента считаются по адресу, с которого пришёл запрос, и не действуют для частных и служебных адресов. За обратным прокси, CDN или балансировщиком не на петлевом адресе это адрес прокси, если --trusted-proxies его не называет, и тогда все роутеры за ним делят лимиты одного клиента. Зеркало пишет предупреждение в журнал, когда запросы от недоверенного прокси несут X-Forwarded-For, X-Real-IP или CF-Connecting-IP. Зеркало, чей вышестоящий адрес - другое зеркало, тоже считается там одним клиентом.

к сведению

Как хаб относит запись, переданную зеркалом, описано в разделе Сеть, к которой относят вклад: с верной подписью одобренного зеркала она не относится ни к какой сети, а у адреса зеркала сетевые лимиты выше.

Зеркало также не отвечает на /b4/hub/v1/network, поэтому роутер, синхронизирующийся только через зеркала, не узнаёт свою сеть от хаба и берёт её из последнего запуска DPI Детектора.

Почему зеркало не может подделать сет​

Роутер доверяет ключу, а не адресу. Доверенный ключ - это встроенный в b4 или введённый в поле Публичный ключ хаба, за вычетом отозванных манифестом. Любой манифест, с какого бы адреса он ни пришёл, должен быть подписан этим ключом; манифест называет файл каталога с его размером и хешем; каталог называет каждый файл payload с его хешем. Закрытой части у зеркала нет, поэтому один изменённый байт заставляет роутер отвергнуть ответ: манифест или каталог, не прошедший проверку, отправляет его к следующему адресу, а файл payload с несовпавшим хешем просто не скачивается.

Та же проверка работает уровнем выше: прежде чем что-либо копировать, зеркало проверяет манифест вышестоящего хаба своим доверенным ключом.

Хаб и зеркало на одном хосте​

Это разные команды и разные процессы. Каталог одного хаба не сливается с хранилищем другого, а роутер доверяет одному ключу хаба за раз, поэтому свой хаб и зеркало центрального - это два развёртывания, а не две половины одного.

Чтобы запустить оба на одной машине, нужны два каталога данных и два адреса прослушивания; направленные на один каталог данных, они перезаписывали бы опубликованные файлы друг друга. Третий хаб, в свою очередь, может зеркалить собственный хаб, если передать его идентификатор ключа в --upstream-key.

Сохранность данных​

Каталог данных переносится целиком. hub.key, hub.db, secret, blobs/ и public/ принадлежат друг другу.

осторожно

Файл secret создаётся молча, если его нет. Он задаёт HMAC, которым везде заменён ключ участника, поэтому база, восстановленная без исходного secret, больше никого не узнаёт: блокировки, отметки доверия, авторство уже опубликованных сетов и устранение повторов в отчётах перестают работать.

warning

Восстановление старой базы откатывает эпоху и порядковый номер назад, а роутеры отклоняют каталог старее имеющегося. Хаб тоже отказывается собирать такой каталог, пока более новый лежит в public/ или есть у одобренного зеркала. Опубликовать восстановленную базу всё же позволяет Начать новую эпоху - на странице «Каталог» в консоли или b4hub build --new-epoch --data <каталог>.

Потерю hub.key со стороны хаба исправить нечем. Пути смены ключа нет, поэтому новый идентификатор пришлось бы вручную вписать на каждом роутере, направленном на этот хаб.