0ee11a065b
The smarthost routes all mail through one rspamd: inbound that survived the edge gets the AI (LiteLLM Haiku) pass and is tagged for Stalwart's Junk filing; outbound from the lab and pods is DKIM-signed. - add rspamd (milter via ClusterIP only), ClamAV and a 3-node sentinel Valkey - tag inbound (`X-Spam`, `X-Spam-Status`, `X-Spamd-Result`), reject only viruses, tempfail when ClamAV fails - score inbound against the hop before the edge relays (`external_relay`) - DKIM-sign and skip GPT only for the smarthost's mynetworks sources - elect the highest-offset Valkey on full restart; RDB, 384mb `volatile-ttl` - read a hashed controller password (`password_hash`) Requires `password_hash` in kv `mail/default/rspamd-controller`. Reviewed-on: #542 Co-authored-by: unkin-agent <unkin-agent@unkin.net> Co-committed-by: unkin-agent <unkin-agent@unkin.net>
83 lines
2.5 KiB
Bash
Executable File
83 lines
2.5 KiB
Bash
Executable File
#!/bin/sh
|
|
# Usage: start.sh valkey|sentinel. valkey boots read-only as a replica of
|
|
# 127.0.0.1:1 ("undecided") so peers can compare offsets; decide() then follows
|
|
# the sentinels' master, a live peer master, or the highest-offset node.
|
|
set -eu
|
|
name="$(hostname)"
|
|
sts="${name%-*}"
|
|
self="${name}${DOMAIN_SUFFIX}"
|
|
|
|
info() {
|
|
timeout 3 valkey-cli -h "$1" info replication 2>/dev/null | tr -d '\r' || true
|
|
}
|
|
|
|
field() {
|
|
printf '%s\n' "$1" | sed -n "s/^$2://p"
|
|
}
|
|
|
|
undecided() {
|
|
[ "$(field "$1" master_host)" = 127.0.0.1 ]
|
|
}
|
|
|
|
choose_master() {
|
|
deadline=$(($(date +%s) + 60))
|
|
while :; do
|
|
best=""
|
|
best_off=-1
|
|
answered=0
|
|
for i in 0 1 2; do
|
|
peer="${sts}-${i}${DOMAIN_SUFFIX}"
|
|
if [ "$peer" = "$self" ]; then
|
|
r="$(info 127.0.0.1)"
|
|
else
|
|
m="$(timeout 3 valkey-cli -h "$peer" -p 26379 --raw sentinel get-master-addr-by-name rspamd 2>/dev/null | head -n1 || true)"
|
|
[ -n "$m" ] && { echo "$m"; return; }
|
|
r="$(info "$peer")"
|
|
[ -n "$r" ] || continue
|
|
answered=$((answered + 1))
|
|
[ "$(field "$r" role)" = master ] && { echo "$peer"; return; }
|
|
fi
|
|
undecided "$r" || continue
|
|
off="$(field "$r" master_repl_offset)"
|
|
[ "${off:-0}" -gt "$best_off" ] && best="$peer" best_off="${off:-0}"
|
|
done
|
|
if [ "$best" = "$self" ] && { [ "$answered" -eq 2 ] || [ "$(date +%s)" -ge "$deadline" ]; }; then
|
|
echo "$self"
|
|
return
|
|
fi
|
|
sleep 3
|
|
done
|
|
}
|
|
|
|
decide() {
|
|
until valkey-cli ping >/dev/null 2>&1; do sleep 1; done
|
|
master="$(choose_master)"
|
|
target="$master 6379"
|
|
[ "$master" = "$self" ] && target="no one"
|
|
until valkey-cli replicaof $target; do sleep 1; done
|
|
}
|
|
|
|
if [ "$1" = sentinel ]; then
|
|
until r="$(info 127.0.0.1)" && [ -n "$(field "$r" role)" ] && ! undecided "$r"; do sleep 2; done
|
|
master="$self"
|
|
[ "$(field "$r" role)" = master ] || master="$(field "$r" master_host)"
|
|
cat > /sentinel/sentinel.conf <<CONF
|
|
port 26379
|
|
protected-mode no
|
|
sentinel resolve-hostnames yes
|
|
sentinel announce-hostnames yes
|
|
sentinel announce-ip $self
|
|
sentinel monitor rspamd $master 6379 2
|
|
sentinel down-after-milliseconds rspamd 5000
|
|
sentinel failover-timeout rspamd 60000
|
|
sentinel parallel-syncs rspamd 1
|
|
CONF
|
|
exec valkey-sentinel /sentinel/sentinel.conf
|
|
fi
|
|
|
|
decide &
|
|
# RDB only: an AOF reload resets the replication offset that choose_master compares.
|
|
exec valkey-server --dir /data --appendonly no --save "60 1" \
|
|
--maxmemory 384mb --maxmemory-policy volatile-ttl \
|
|
--protected-mode no --replica-announce-ip "$self" --replicaof 127.0.0.1 1
|