Files
bitmessage/README.ru-RU.md
T
bitdeals 88b5b896e1
Build docker image and push to registry.bitdeals.org / main-build-job (push) Successful in 7m12s
feat: supervise the daemon, and restart one that has lost every peer
A PyBitmessage daemon that has dropped to zero network connections does not find
its way back. It can sit there for days — testnet1 did, and every BitDeals deal
completed on that stand since 2026-08-30 left its escrow unspent, because the
guarantor never received a CH3 the node could no longer publish. A daemon that
has just started, by contrast, dials hard and reconnects within seconds. The
cure was already known; what was missing was anything to notice and apply it.

Docker will not: it reacts to a process exiting, never to a healthcheck, and
health-driven restarts exist only under Swarm. Doing it from outside means
handing a container the docker socket, which is root on the host and a poor
trade for a relay with a published port. So the container supervises itself.

`run.sh` no longer ends at `exec gosu bitmessage pybitmessage -d`. That made the
daemon PID 1, and it is a bad PID 1: daemonize() double-forks and parks the
grandfather in `while True: time.sleep(1)`, the final child SIGTERMs it to say
"ready", and PID 1 drops that signal for want of a handler. The grandfather slept
for ever; `docker stop` therefore reached the real daemon only as the SIGKILL ten
seconds later, cutting a startup VACUUM in half — the way a node gets trapped
retrying one it can never finish; and a daemon that died on its own left the
container Up around a corpse, because what PID 1 was doing had nothing to do with
whether the daemon lived.

Away from PID 1 that grandfather does die on the ready signal — measured here:
the start call returns at once with status 143 and leaves exactly one
pybitmessage process behind. That makes starting the daemon an ordinary blocking
call, and the supervisor ordinary shell: a trap that stops the daemon through its
own API, a restart when it is gone, and the peer rule.

What the supervisor does not do is act on a daemon whose API is not answering at
all. That is the trapped-VACUUM node; a restart does not cure it and cuts the
next VACUUM short as well. watchdog.py reports that case as its own exit code so
the loop can leave it to a person. The healthcheck is untouched: it reports, and
does not act.

Four settings, on by default: BITMESSAGE_WATCHDOG, and _PERIOD, _AFTER,
_COOLDOWN. All validated at start, where a typo is visible, rather than hours
later as a supervisor that spins or one that never acts.

Callers must raise the stop grace period — 90s in compose, or --stop-timeout 90.
A clean shutdown took 17.5 s on a small database and grows with it, so under
Docker's default ten the daemon is killed mid-write anyway and the supervisor
buys nothing. An image cannot set this for itself.
2026-09-04 09:53:07 +00:00

163 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Общие сведения
> English version: [README.md](README.md)
[PyBitmessage](https://bitmessage.org/) — клиент P2P-протокола обмена сообщениями Bitmessage, служащий для отправки шифрованных сообщений как одному адресату, так и множеству подписчиков.
Клиент PyBitmessage, работающий демоном в docker-контейнере с включённым XML-RPC API.
Репозиторий описывает только развёртывание в docker.
# Использование
Контейнер создаёт детерминированные адреса Bitmessage на основе переменной `BITMESSAGE_SEED_PHRASE`.
Ниже — примеры, с которых удобно начать создание контейнера.
У контейнера два порта, и они не взаимозаменяемы. **8442** — XML-RPC API: он
полностью управляет демоном и не имеет TLS, поэтому задайте свои учётные данные
и оставьте его на loopback. **8444** — P2P-порт Bitmessage: опубликуйте его,
чтобы к узлу могли подключаться другие, или не публикуйте, и тогда узел работает
только на исходящих. Внутри контейнера демон слушает оба в любом случае.
## docker-compose
```yaml
services:
pybitmessage:
build:
context: https://git.bitdeals.org/private/bitmessage.git
dockerfile: ./docker/Dockerfile
image: registry.bitdeals.org/bitmessage
environment:
- BITMESSAGE_API_USER=CHANGE_ME
- BITMESSAGE_API_PASSWORD=CHANGE_ME
- BITMESSAGE_SEED_PHRASE=bitmessage_seed_phrase
- BITMESSAGE_SEED_ADDRESSES=1
- BITMESSAGE_TTL=172800
- BITMESSAGE_STOPRESENDINGAFTERXDAYS=60
- BITMESSAGE_MAXTOTALCONNECTIONS=40
ports:
- 127.0.0.1:8442:8442 # API — только loopback
- 8444:8444 # P2P — уберите строку, чтобы остаться на исходящих
volumes:
- bitmessage:/home/bitmessage
volumes:
bitmessage:
```
## docker cli
```sh
docker run -d \
-e BITMESSAGE_API_USER=CHANGE_ME \
-e BITMESSAGE_API_PASSWORD=CHANGE_ME \
-e BITMESSAGE_SEED_PHRASE=bitmessage_seed_phrase \
-e BITMESSAGE_SEED_ADDRESSES=1 \
-e BITMESSAGE_TTL=172800 \
-e BITMESSAGE_STOPRESENDINGAFTERXDAYS=60 \
-e BITMESSAGE_MAXTOTALCONNECTIONS=40 \
-p 127.0.0.1:8442:8442 \
-p 8444:8444 \
-v bitmessage:/home/bitmessage \
registry.bitdeals.org/bitmessage
```
# Параметры
Образы контейнера настраиваются параметрами, передаваемыми при запуске.
|Параметр|Назначение|
|:--------|:-------|
|-p 127.0.0.1:8442|Порт API. Внутри контейнера демон всегда слушает `0.0.0.0`, поэтому доступность определяет то, что опубликовано|
|-p 8444|P2P-порт Bitmessage. Необязательный: без него узел всё равно подключается к пирам сам, просто к нему подключиться нельзя. Публиковать только как `8444:8444` — см. «Замечания»|
|-v /home/bitmessage|Каталог данных: `keys.dat` (личность, настройки) и `messages.dat`. Без него после каждого обновления это новый узел|
|-e BITMESSAGE_API_USER|Пользователь XML-RPC API. По умолчанию: `bitmessage_api_user` — измените|
|-e BITMESSAGE_API_PASSWORD|Пароль XML-RPC API. По умолчанию: `bitmessage_api_password` — измените, см. «Замечания»|
|-e BITMESSAGE_SEED_PHRASE|Парольная фраза для создания детерминированных адресов. По умолчанию: генерируется заново при каждом старте, то есть адреса каждый раз другие. Используется только при `BITMESSAGE_SEED_ADDRESSES` больше `0`|
|-e BITMESSAGE_SEED_ADDRESSES|Количество создаваемых детерминированных адресов. По умолчанию: `0`|
|-e BITMESSAGE_TTL|Срок жизни вновь отправляемых сообщений, в секундах. По умолчанию: `172800`|
|-e BITMESSAGE_STOPRESENDINGAFTERXDAYS|Прекратить повторную отправку недоставленного сообщения через X дней. По умолчанию: `30`|
|-e BITMESSAGE_APIVARIANT|Предоставляемый API: xml или json-RPC. По умолчанию: `legacy`|
|-e BITMESSAGE_MAXTOTALCONNECTIONS|Предел одновременных соединений, входящих и исходящих вместе (`maxoutboundconnections` равен 8, то есть это значение минус 8 — запас на входящие). По умолчанию: `200`, штатное значение PyBitmessage — снижайте, если P2P-порт опубликован|
|-e BITMESSAGE_TRUSTED_PEER|`host:port` единственного пира, к которому узел подключается наружу; больше ни к кому. По умолчанию: пусто — узел выбирает пиров сам. См. «Замечания»|
|-e BITMESSAGE_SEND_OUTGOING|Подключается ли узел наружу вообще: `True` или `False`. По умолчанию: `True`. При `False` получается узел, который только принимает входящие, — центр приватного контура|
|-e BITMESSAGE_KNOWN_NODES|Список `host:port` через запятую; записывается в `knownnodes.dat` при каждом старте вместо того, что там было. По умолчанию: пусто — файл не трогается. Заодно выключает бутстрап по DNS, см. «Замечания»|
|-e BITMESSAGE_WATCHDOG|Перезапускать ли демона, потерявшего всех пиров: `True` или `False`. По умолчанию: `True`. См. «Замечания»|
|-e BITMESSAGE_WATCHDOG_PERIOD|Секунд между проверками числа пиров. По умолчанию: `60`|
|-e BITMESSAGE_WATCHDOG_AFTER|Сколько проверок подряд без пиров нужно, чтобы вмешаться. По умолчанию: `5` — то есть пять минут при периоде по умолчанию|
|-e BITMESSAGE_WATCHDOG_COOLDOWN|Минимальный промежуток между двумя перезапусками, в секундах. По умолчанию: `900`. `0` снимает ограничение|
# Замечания
- `%` в пароле API использовать нельзя: он ломает собственный чтец конфигурации
PyBitmessage, и любой вызов API отвечает `500`, притом что `keys.dat`
выглядит правильным. Остальные символы допустимы, контейнер их экранирует.
- Клиенты обязаны кодировать учётные данные — они попадают в
`http://user:password@host:port/`, где `@`, `#`, `/` и `:` меняют разбор URL.
Скрипты этого образа кодируют, ваш клиент должен тоже.
- Контейнер становится здоровым, когда у демона появилось сетевое соединение,
— на новом узле это несколько минут. Начальный период ожидания заодно
покрывает стартовый `VACUUM` файла `messages.dat`.
- **Демона без пиров перезапускаем, молчащего — нет.** Демон, потерявший всех
пиров, сам обратно дорогу не находит: он может простоять на нуле соединений
сколько угодно, — а только что запущенный ищет пиров напористо и находит.
Поэтому контейнер присматривает за собственным демоном: раз в
`BITMESSAGE_WATCHDOG_PERIOD` спрашивает у API число соединений и после
`BITMESSAGE_WATCHDOG_AFTER` нулей подряд останавливает демона через его же API
и поднимает заново. *Контейнер* при этом не пересоздаётся и никто снаружи не
участвует, так что соседний узел сохраняет адреса и видит лишь моргнувший API.
Демона, у которого API не отвечает вовсе, сторож не трогает намеренно. Это
узел, застрявший на стартовом `VACUUM`, которого он не может закончить;
перезапуск его не лечит, а лишь обрывает следующий `VACUUM` на середине.
Проверка здоровья показывает `unhealthy` в обоих случаях, но вмешиваться стоит
только в первый, а во втором нужен человек.
**Задайте контейнеру `stop_grace_period: 90s`** (compose) или
`--stop-timeout 90` (`docker run`). Корректное закрытие `messages.dat` не
укладывается в десять секунд, отпущенные Docker по умолчанию, — замерено около
тринадцати на небольшой базе, и растёт вместе с файлом, — так что без этого
демона убьёт SIGKILL на середине записи, ровно то, ради предотвращения чего
супервизор и заведён. Сам образ это задать не может, только вызывающая сторона.
`BITMESSAGE_WATCHDOG=False` выключает правило о пирах. Супервизор остаётся в
любом случае: он же корректно останавливает демона по `docker stop` и
поднимает того, кто умер сам, вместо контейнера, стоящего вокруг трупа.
- **P2P-порт публикуется только как `8444:8444`.** Пирам демон сообщает порт из
собственной конфигурации (`port` в `keys.dat`), а не тот, в который вы его
отобразили, поэтому `8555:8444` объявляет сети порт, где никого нет. Для
другого порта на хосте нужен `extport` в `keys.dat`, а его этот контейнер не
подставляет.
- **Свой адрес нигде не указывается.** В version-сообщении демон шлёт
захардкоженный `127.0.0.1`, который каждый пир отбрасывает и берёт вместо него
IP, увиденный на сокете, а порт — из того же сообщения. Поэтому узел с
опубликованным 8444 сеть находит сама, как только он подключится наружу:
ни IP, ни DNS-имя хоста задавать негде. Единственное исключение — скрытый
сервис Tor, которому нужен явный `onionhostname`.
- **Приватному контуру нужны назначенные пиры и звезда, в которую их назначать.**
PyBitmessage отвергает кандидата, чья сетевая группа (для IPv4 — /16) уже
представлена среди его исходящих соединений. Все контейнеры одного
compose-проекта живут в одной /16, поэтому каждый узел удерживает ровно одно
исходящее соединение — со случайно выбранным пиром, и контур чаще всего
распадается на компоненты. Задайте одному узлу
`BITMESSAGE_SEND_OUTGOING=False`, и он станет хабом, который только принимает
(проверка смотрит лишь на исходящие, входящие она не ограничивает), остальные
направьте на него через `BITMESSAGE_TRUSTED_PEER=<ip-хаба>:8444` — объекты
пойдут спица → хаб → спицы. Адреса задавайте IP, а не именами сервисов:
проверка разбирает хост как IP, да и обмен `addr` между узлами оперирует IP.
- **`BITMESSAGE_KNOWN_NODES` — то, что делает приватный контур приватным.** Узел,
у которого в `knownnodes.dat` есть пир не из встроенного списка PyBitmessage,
перестаёт спрашивать адреса у `bootstrap8080.bitmessage.org`; без этого даже
узел с назначенным пиром при каждом старте резолвит публичный бутстрап-хост.
Файл переписывается на каждом старте, поэтому во что узел верит при загрузке,
определяет переменная, а не история контейнера.
- **Публикация 8444 — осознанный компромисс по безопасности.** Демон работает на
Python 2 и уже разбирает недоверенные данные от своих исходящих пиров, так что
открытый порт эту поверхность не создаёт — он меняет то, *кто* может
подключиться, *когда* и *сколько их*. Разбор до рукопожатия становится доступен
кому угодно, а реальный риск — исчерпание ресурсов, а не выполнение кода.
Держите `BITMESSAGE_MAXTOTALCONNECTIONS` низким и ограничьте контейнер по
памяти и CPU.