let the topology be pinned: trusted peer, outgoing switch, known nodes
Build docker image and push to registry.bitdeals.org / main-build-job (push) Successful in 1m50s
Build docker image and push to registry.bitdeals.org / main-build-job (push) Successful in 1m50s
A private Bitmessage contour cannot be assembled by letting the nodes find each other. The sybil check in connectionpool refuses a candidate whose /16 is already among the outbound connections, and every container of a compose project shares one /16 -- so each node keeps a single outbound connection to a randomly chosen peer, and the contour splits into components on some runs and not on others. Three variables make the topology explicit instead: BITMESSAGE_TRUSTED_PEER trustedpeer = host:port BITMESSAGE_SEND_OUTGOING sendoutgoingconnections = True/False BITMESSAGE_KNOWN_NODES host:port,... -> knownnodes.dat With them a star is one line of config per node: the hub takes SEND_OUTGOING=False and only accepts, the spokes take TRUSTED_PEER=<hub>:8444. Details worth knowing: - trustedpeer is absent from the stock keys.dat, so a substitution alone would be a silent no-op. The key is added the same way maxtotalconnections is, and it is added even when the value is empty -- that is how a node that was pinned before can be unpinned. Its anchors stop at "=" rather than "= ", because an empty value leaves no trailing space to match. - knownnodes.dat is rewritten on every start, not only when missing. "Only when missing" would never have fired: the image ships one, built by the `pybitmessage -t` run in the Dockerfile, and a named volume inherits it. Seeding it is also what stops the DNS bootstrap -- deserialising any peer that is neither a DEFAULT_NODE nor "self" raises knownNodesActual, and startBootstrappers only runs while that flag is down. - Both peer variables are validated here. PyBitmessage does check trustedpeer, but with a sys.exit() from a constructor in the network thread, which reads as a container that died for no stated reason.
This commit is contained in:
@@ -81,6 +81,9 @@ Container images are configured using parameters passed at runtime.
|
|||||||
|-e BITMESSAGE_STOPRESENDINGAFTERXDAYS|Stop resending unreceived message after X days. Default: `30`|
|
|-e BITMESSAGE_STOPRESENDINGAFTERXDAYS|Stop resending unreceived message after X days. Default: `30`|
|
||||||
|-e BITMESSAGE_APIVARIANT|provides xml or json-RPC API. Default: `legacy`|
|
|-e BITMESSAGE_APIVARIANT|provides xml or json-RPC API. Default: `legacy`|
|
||||||
|-e BITMESSAGE_MAXTOTALCONNECTIONS|Cap on all connections at once, inbound and outbound together (`maxoutboundconnections` is 8, so this minus 8 is the inbound headroom). Default: `200`, the PyBitmessage stock value — lower it when the P2P port is published|
|
|-e BITMESSAGE_MAXTOTALCONNECTIONS|Cap on all connections at once, inbound and outbound together (`maxoutboundconnections` is 8, so this minus 8 is the inbound headroom). Default: `200`, the PyBitmessage stock value — lower it when the P2P port is published|
|
||||||
|
|-e BITMESSAGE_TRUSTED_PEER|`host:port` of the one peer this node may connect out to; it dials nothing else. Default: empty — the node chooses its own peers. See Notes|
|
||||||
|
|-e BITMESSAGE_SEND_OUTGOING|Whether the node dials out at all, `True` or `False`. Default: `True`. `False` gives a node that only accepts inbound connections — the hub of a private contour|
|
||||||
|
|-e BITMESSAGE_KNOWN_NODES|Comma-separated `host:port` list, written into `knownnodes.dat` on every start in place of whatever was there. Default: empty — the file is left as it is. Also switches the DNS bootstrap off, see Notes|
|
||||||
|
|
||||||
# Notes
|
# Notes
|
||||||
|
|
||||||
@@ -103,6 +106,23 @@ Container images are configured using parameters passed at runtime.
|
|||||||
published is found by the network on its own, as soon as it connects out —
|
published is found by the network on its own, as soon as it connects out —
|
||||||
there is no host IP or DNS name to set anywhere. The one exception is a Tor
|
there is no host IP or DNS name to set anywhere. The one exception is a Tor
|
||||||
hidden service, which needs an explicit `onionhostname`.
|
hidden service, which needs an explicit `onionhostname`.
|
||||||
|
- **A private contour needs its peers pinned, and a star to pin them into.**
|
||||||
|
PyBitmessage refuses a candidate whose network group (the /16 for IPv4) is
|
||||||
|
already represented among its outbound connections. Every container of one
|
||||||
|
compose project lives in a single /16, so each node keeps exactly one outbound
|
||||||
|
connection, to a peer it picked at random — which as often as not leaves the
|
||||||
|
contour split into components. Give one node `BITMESSAGE_SEND_OUTGOING=False`
|
||||||
|
so it becomes a hub that only accepts (the check looks at outbound connections
|
||||||
|
only, so inbound are not capped by it), point the rest at it with
|
||||||
|
`BITMESSAGE_TRUSTED_PEER=<hub-ip>:8444`, and objects travel spoke → hub →
|
||||||
|
spokes. Use IP addresses, not service names: the sybil check parses the host
|
||||||
|
as an IP, and the `addr` exchange between nodes carries IPs anyway.
|
||||||
|
- **`BITMESSAGE_KNOWN_NODES` is what keeps a private contour private.** A node
|
||||||
|
whose `knownnodes.dat` names a peer outside PyBitmessage's built-in default
|
||||||
|
list stops asking `bootstrap8080.bitmessage.org` for more; without it even a
|
||||||
|
pinned node resolves the public bootstrap host on every start. The file is
|
||||||
|
rewritten on each start, so the variable, not the container's history, is what
|
||||||
|
the node believes on boot.
|
||||||
- **Publishing 8444 is a deliberate security trade-off.** The daemon runs on
|
- **Publishing 8444 is a deliberate security trade-off.** The daemon runs on
|
||||||
Python 2 and already parses untrusted data from its outbound peers, so an open
|
Python 2 and already parses untrusted data from its outbound peers, so an open
|
||||||
port does not create that exposure — it changes *who* may connect, *when*, and
|
port does not create that exposure — it changes *who* may connect, *when*, and
|
||||||
|
|||||||
@@ -81,6 +81,9 @@ docker run -d \
|
|||||||
|-e BITMESSAGE_STOPRESENDINGAFTERXDAYS|Прекратить повторную отправку недоставленного сообщения через X дней. По умолчанию: `30`|
|
|-e BITMESSAGE_STOPRESENDINGAFTERXDAYS|Прекратить повторную отправку недоставленного сообщения через X дней. По умолчанию: `30`|
|
||||||
|-e BITMESSAGE_APIVARIANT|Предоставляемый API: xml или json-RPC. По умолчанию: `legacy`|
|
|-e BITMESSAGE_APIVARIANT|Предоставляемый API: xml или json-RPC. По умолчанию: `legacy`|
|
||||||
|-e BITMESSAGE_MAXTOTALCONNECTIONS|Предел одновременных соединений, входящих и исходящих вместе (`maxoutboundconnections` равен 8, то есть это значение минус 8 — запас на входящие). По умолчанию: `200`, штатное значение PyBitmessage — снижайте, если P2P-порт опубликован|
|
|-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, см. «Замечания»|
|
||||||
|
|
||||||
# Замечания
|
# Замечания
|
||||||
|
|
||||||
@@ -104,6 +107,23 @@ docker run -d \
|
|||||||
опубликованным 8444 сеть находит сама, как только он подключится наружу:
|
опубликованным 8444 сеть находит сама, как только он подключится наружу:
|
||||||
ни IP, ни DNS-имя хоста задавать негде. Единственное исключение — скрытый
|
ни IP, ни DNS-имя хоста задавать негде. Единственное исключение — скрытый
|
||||||
сервис Tor, которому нужен явный `onionhostname`.
|
сервис 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 — осознанный компромисс по безопасности.** Демон работает на
|
- **Публикация 8444 — осознанный компромисс по безопасности.** Демон работает на
|
||||||
Python 2 и уже разбирает недоверенные данные от своих исходящих пиров, так что
|
Python 2 и уже разбирает недоверенные данные от своих исходящих пиров, так что
|
||||||
открытый порт эту поверхность не создаёт — он меняет то, *кто* может
|
открытый порт эту поверхность не создаёт — он меняет то, *кто* может
|
||||||
|
|||||||
@@ -10,6 +10,9 @@ export BITMESSAGE_TTL="${BITMESSAGE_TTL:-172800}"
|
|||||||
export BITMESSAGE_STOPRESENDINGAFTERXDAYS="${BITMESSAGE_STOPRESENDINGAFTERXDAYS:-30}"
|
export BITMESSAGE_STOPRESENDINGAFTERXDAYS="${BITMESSAGE_STOPRESENDINGAFTERXDAYS:-30}"
|
||||||
export BITMESSAGE_APIVARIANT="${BITMESSAGE_APIVARIANT:-legacy}"
|
export BITMESSAGE_APIVARIANT="${BITMESSAGE_APIVARIANT:-legacy}"
|
||||||
export BITMESSAGE_MAXTOTALCONNECTIONS="${BITMESSAGE_MAXTOTALCONNECTIONS:-200}"
|
export BITMESSAGE_MAXTOTALCONNECTIONS="${BITMESSAGE_MAXTOTALCONNECTIONS:-200}"
|
||||||
|
export BITMESSAGE_TRUSTED_PEER="${BITMESSAGE_TRUSTED_PEER:-}"
|
||||||
|
export BITMESSAGE_SEND_OUTGOING="${BITMESSAGE_SEND_OUTGOING:-True}"
|
||||||
|
export BITMESSAGE_KNOWN_NODES="${BITMESSAGE_KNOWN_NODES:-}"
|
||||||
|
|
||||||
# Reject anything but a plain number: this value is written into keys.dat, and
|
# Reject anything but a plain number: this value is written into keys.dat, and
|
||||||
# unlike the credentials below it has no business containing characters that
|
# unlike the credentials below it has no business containing characters that
|
||||||
@@ -22,6 +25,40 @@ case "$BITMESSAGE_MAXTOTALCONNECTIONS" in
|
|||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
|
# sendoutgoingconnections is read with safeGetBoolean, which would take "yes" or
|
||||||
|
# "1" too; keys.dat is written by hand often enough that it is worth keeping one
|
||||||
|
# spelling in it. Anything else is a typo, and a typo here reads as False --
|
||||||
|
# a node that quietly never dials out.
|
||||||
|
case "$BITMESSAGE_SEND_OUTGOING" in
|
||||||
|
[Tt]rue) BITMESSAGE_SEND_OUTGOING=True ;;
|
||||||
|
[Ff]alse) BITMESSAGE_SEND_OUTGOING=False ;;
|
||||||
|
*)
|
||||||
|
echo "BITMESSAGE_SEND_OUTGOING must be True or False" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# host:port with a numeric port -- the form both consumers need. PyBitmessage
|
||||||
|
# does check trustedpeer itself, but by sys.exit() from a constructor deep in
|
||||||
|
# the network thread: the container dies with the reason buried in the daemon
|
||||||
|
# log. Fail here, where the message is the first thing in `docker logs`.
|
||||||
|
check_peer() {
|
||||||
|
case "$1" in
|
||||||
|
*:*) ;;
|
||||||
|
*) return 1 ;;
|
||||||
|
esac
|
||||||
|
[ -n "${1%:*}" ] || return 1
|
||||||
|
case "${1##*:}" in
|
||||||
|
'' | *[!0-9]*) return 1 ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
if [ -n "$BITMESSAGE_TRUSTED_PEER" ] && ! check_peer "$BITMESSAGE_TRUSTED_PEER"
|
||||||
|
then
|
||||||
|
echo "BITMESSAGE_TRUSTED_PEER must be host:port" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
if [ -z "${BITMESSAGE_SEED_PHRASE:-}" ]
|
if [ -z "${BITMESSAGE_SEED_PHRASE:-}" ]
|
||||||
then
|
then
|
||||||
BITMESSAGE_SEED_PHRASE="$(cat /dev/random | tr -dc "a-z" | head -c32)"
|
BITMESSAGE_SEED_PHRASE="$(cat /dev/random | tr -dc "a-z" | head -c32)"
|
||||||
@@ -56,6 +93,18 @@ then
|
|||||||
gosu bitmessage sed -i "1a maxtotalconnections = $BITMESSAGE_MAXTOTALCONNECTIONS" keys.dat
|
gosu bitmessage sed -i "1a maxtotalconnections = $BITMESSAGE_MAXTOTALCONNECTIONS" keys.dat
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# trustedpeer is absent from the stock keys.dat entirely, so the substitution
|
||||||
|
# below is a no-op until the key exists -- same trap as maxtotalconnections.
|
||||||
|
# The key is added even when the value is empty, which is how it can be taken
|
||||||
|
# back off a node that was pinned before: safeGet returns "" and connectionpool
|
||||||
|
# falls back to chooseConnection. That empty case is also why the anchors here
|
||||||
|
# stop at "=" instead of "= ": with nothing to the right there is no trailing
|
||||||
|
# space to match, and the substitution would never fire again.
|
||||||
|
if ! grep -q "^trustedpeer =" keys.dat
|
||||||
|
then
|
||||||
|
gosu bitmessage sed -i "1a trustedpeer = $(esc "$BITMESSAGE_TRUSTED_PEER")" keys.dat
|
||||||
|
fi
|
||||||
|
|
||||||
# Set config values. Every expression is anchored to the start of the line and
|
# Set config values. Every expression is anchored to the start of the line and
|
||||||
# names its key in the replacement, so no backreference is involved and nothing
|
# names its key in the replacement, so no backreference is involved and nothing
|
||||||
# in another section can match. With set -e a failure here now stops the
|
# in another section can match. With set -e a failure here now stops the
|
||||||
@@ -71,8 +120,47 @@ gosu bitmessage sed -i \
|
|||||||
-e "s|^ttl = .*|ttl = $(esc "$BITMESSAGE_TTL")|" \
|
-e "s|^ttl = .*|ttl = $(esc "$BITMESSAGE_TTL")|" \
|
||||||
-e "s|^stopresendingafterxdays = .*|stopresendingafterxdays = $(esc "$BITMESSAGE_STOPRESENDINGAFTERXDAYS")|" \
|
-e "s|^stopresendingafterxdays = .*|stopresendingafterxdays = $(esc "$BITMESSAGE_STOPRESENDINGAFTERXDAYS")|" \
|
||||||
-e "s|^maxtotalconnections = .*|maxtotalconnections = $BITMESSAGE_MAXTOTALCONNECTIONS|" \
|
-e "s|^maxtotalconnections = .*|maxtotalconnections = $BITMESSAGE_MAXTOTALCONNECTIONS|" \
|
||||||
|
-e "s|^trustedpeer =.*|trustedpeer = $(esc "$BITMESSAGE_TRUSTED_PEER")|" \
|
||||||
|
-e "s|^sendoutgoingconnections = .*|sendoutgoingconnections = $BITMESSAGE_SEND_OUTGOING|" \
|
||||||
-e "s|^udp = .*|udp = False|" keys.dat
|
-e "s|^udp = .*|udp = False|" keys.dat
|
||||||
|
|
||||||
|
# BITMESSAGE_KNOWN_NODES pins the peers the daemon starts from, and is rewritten
|
||||||
|
# on every start: in a private contour the seed *is* the topology, and a file
|
||||||
|
# left over from an earlier run names nodes that may no longer exist. Seeding it
|
||||||
|
# also switches off the DNS bootstrap -- json_deserialize_knownnodes raises
|
||||||
|
# knownNodesActual for any peer that is neither DEFAULT_NODES nor "self", and
|
||||||
|
# connectionpool calls startBootstrappers only while that flag is down, so the
|
||||||
|
# node never reaches bootstrap8080.bitmessage.org.
|
||||||
|
#
|
||||||
|
# Writing it "only when the file is missing" would have been a permanent no-op:
|
||||||
|
# the image ships a knownnodes.dat, produced by the `pybitmessage -t` run in the
|
||||||
|
# Dockerfile, and a named volume inherits it on first use.
|
||||||
|
if [ -n "$BITMESSAGE_KNOWN_NODES" ]
|
||||||
|
then
|
||||||
|
now="$(date +%s)"
|
||||||
|
nodes=""
|
||||||
|
oldifs="$IFS"
|
||||||
|
IFS=","
|
||||||
|
for peer in $BITMESSAGE_KNOWN_NODES
|
||||||
|
do
|
||||||
|
IFS="$oldifs"
|
||||||
|
if ! check_peer "$peer"
|
||||||
|
then
|
||||||
|
echo "BITMESSAGE_KNOWN_NODES entry '$peer' must be host:port" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
[ -z "$nodes" ] || nodes="$nodes,"
|
||||||
|
nodes="$nodes
|
||||||
|
{\"stream\": 1, \"peer\": {\"host\": \"${peer%:*}\", \"port\": ${peer##*:}},
|
||||||
|
\"info\": {\"lastseen\": $now, \"rating\": 0, \"self\": false}}"
|
||||||
|
IFS=","
|
||||||
|
done
|
||||||
|
IFS="$oldifs"
|
||||||
|
printf '[%s\n]\n' "$nodes" > knownnodes.dat
|
||||||
|
chown bitmessage:bitmessage knownnodes.dat
|
||||||
|
chmod 600 knownnodes.dat
|
||||||
|
fi
|
||||||
|
|
||||||
# generate address from seed
|
# generate address from seed
|
||||||
if [ "$BITMESSAGE_SEED_ADDRESSES" -gt 0 ]
|
if [ "$BITMESSAGE_SEED_ADDRESSES" -gt 0 ]
|
||||||
then
|
then
|
||||||
|
|||||||
Reference in New Issue
Block a user