Build docker image and push to registry.bitdeals.org / main-build-job (push) Successful in 2m5s
The image runs as its own unprivileged user, and everything in run.sh now assumes it: keys.dat is worked on by its owner, no privilege is dropped anywhere, and what confines the container is whatever the caller passed. Started as root by a `user:` override, none of that holds and the container looks identical from outside -- a silent loss of every property this image was changed to have. Four lines at the top of run.sh, and the reason is then the first line of `docker logs`. It is the same bargain as the build-time checks: a wrong posture should fail loudly rather than pass for a right one.
184 lines
17 KiB
Markdown
184 lines
17 KiB
Markdown
# Общие сведения
|
||
|
||
> 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.
|
||
- **Root'а в этом контейнере нет.** Он стартует под uid 2000 и остаётся под ним,
|
||
включая PID 1: `run.sh` работает только с файлами этого пользователя, а два,
|
||
которые заводит сам демон — `knownnodes.dat` и `messages.dat`, — он заводит в
|
||
своём домашнем каталоге. Ничего не понижается на старте, потому что повышенных
|
||
прав изначально нет. Ни на одном файле образа нет бита setuid или setgid, так
|
||
что захваченному демону не по чему подниматься. Вызывающая сторона, вернувшая
|
||
пользователя в root, получит отказ первой строкой журнала, а не контейнер,
|
||
который выглядит так же и не ограничивает ничего.
|
||
|
||
Плата за это — bind-монтирование. Каталог `/home/bitmessage`, приехавший с
|
||
хоста, должен принадлежать uid 2000: сменить владельца на входе контейнер
|
||
больше не может. Именованный том, ради которого образ и сделан, наследует
|
||
владельца и режимы от образа, и делать с ним ничего не надо.
|
||
|
||
Три вещи может задать только вызывающая сторона, и все три стоит задать.
|
||
`cap_drop: ALL`, ничего не возвращая: образу не нужна ни одна capability, и
|
||
этот же набор получают healthcheck и `docker exec`, а сузить его сам контейнер
|
||
не может. `read_only: true` с `tmpfs` под `/tmp` и `/run`: демон пишет только в
|
||
свой домашний каталог. И `pids_limit` — рядом с ограничениями по памяти и CPU,
|
||
о которых просит предыдущее замечание. Заодно `no-new-privileges:true`: без
|
||
единого setuid-файла в образе кусать ему почти нечего, но и стоит он ничего.
|