Files
bitmessage/docker/run.sh
T
bitdeals 58e3f22982
Build docker image and push to registry.bitdeals.org / main-build-job (push) Successful in 1m50s
let the topology be pinned: trusted peer, outgoing switch, known nodes
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.
2026-08-06 14:51:50 +00:00

179 lines
7.3 KiB
Bash

#!/bin/sh
set -eu
export BITMESSAGE_API_USER="${BITMESSAGE_API_USER:-bitmessage_api_user}"
export BITMESSAGE_API_PASSWORD="${BITMESSAGE_API_PASSWORD:-bitmessage_api_password}"
export BITMESSAGE_SEED_ADDRESSES="${BITMESSAGE_SEED_ADDRESSES:-0}"
export BITMESSAGE_API_PORT="${BITMESSAGE_API_PORT:-8442}"
export BITMESSAGE_TTL="${BITMESSAGE_TTL:-172800}"
export BITMESSAGE_STOPRESENDINGAFTERXDAYS="${BITMESSAGE_STOPRESENDINGAFTERXDAYS:-30}"
export BITMESSAGE_APIVARIANT="${BITMESSAGE_APIVARIANT:-legacy}"
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
# unlike the credentials below it has no business containing characters that
# esc() would have to neutralise. A typo here would otherwise land in the config
# as a key the daemon silently ignores.
case "$BITMESSAGE_MAXTOTALCONNECTIONS" in
'' | *[!0-9]*)
echo "BITMESSAGE_MAXTOTALCONNECTIONS must be a positive integer" >&2
exit 1
;;
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:-}" ]
then
BITMESSAGE_SEED_PHRASE="$(cat /dev/random | tr -dc "a-z" | head -c32)"
export BITMESSAGE_SEED_PHRASE
fi
# Escape a value for use on the right-hand side of the sed expressions below.
# There, a backslash starts an escape, "&" stands for the whole match, and "|"
# ends the replacement because it is the delimiter. Unescaped, a password
# containing "&" was silently rewritten into something else and one containing
# "|" made sed fail outright.
esc() {
printf '%s' "$1" | sed -e 's/[\\&|]/\\&/g'
}
# this command must be run as root (for bind mounts to container)
if [ -f keys.dat ]
then
chown bitmessage:bitmessage keys.dat
chmod 600 keys.dat
fi
# maxtotalconnections is the only brake on a node whose P2P port (8444) is
# published: it caps inbound sockets at the total minus maxoutboundconnections.
# The substitution below is a no-op when the key is missing, which would ship a
# node that looks capped and is not -- and the PyBitmessage clone in the
# Dockerfile is unpinned, so the stock config is whatever upstream generates
# today. Add the key rather than trust the substitution alone; line 1 is the
# [bitmessagesettings] header the daemon reads it from.
if ! grep -q "^maxtotalconnections = " keys.dat
then
gosu bitmessage sed -i "1a maxtotalconnections = $BITMESSAGE_MAXTOTALCONNECTIONS" keys.dat
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
# 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
# container instead of leaving the daemon on its previous settings unnoticed --
# including the case of a bind mount with no keys.dat at all.
gosu bitmessage sed -i \
-e "s|^apiinterface = .*|apiinterface = 0.0.0.0|" \
-e "s|^apivariant = .*|apivariant = $(esc "$BITMESSAGE_APIVARIANT")|" \
-e "s|^apiusername = .*|apiusername = $(esc "$BITMESSAGE_API_USER")|" \
-e "s|^apipassword = .*|apipassword = $(esc "$BITMESSAGE_API_PASSWORD")|" \
-e "s|^apiport = .*|apiport = $(esc "$BITMESSAGE_API_PORT")|" \
-e "s|^apienabled = .*|apienabled = True|" \
-e "s|^ttl = .*|ttl = $(esc "$BITMESSAGE_TTL")|" \
-e "s|^stopresendingafterxdays = .*|stopresendingafterxdays = $(esc "$BITMESSAGE_STOPRESENDINGAFTERXDAYS")|" \
-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
# 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
if [ "$BITMESSAGE_SEED_ADDRESSES" -gt 0 ]
then
# Four attempts, not a bash {1..4}: this runs under dash, where brace
# expansion is literal and the loop would have run once. The call is
# idempotent (createDeterministicAddresses returns nothing for an address
# that already exists), so these are retries while the API comes up.
for i in 1 2 3 4
do
sleep 15
gosu bitmessage /usr/bin/python /usr/local/bin/seed_addr_gen.py
done &
fi
exec gosu bitmessage pybitmessage -d