Files
computer-playbook/roles/svc-db-postgres/files/test/test.sh
kevinveenbirkenbachandClaude Opus 5 04eac9d73d fix(svc-db-tests): report why the SOCKS connect failed instead of swallowing it
The Tor-path credential tests tunnel their client through a local SOCKS5 shim.
When the CONNECT fails, handle() closed the client socket and returned, so the
client library saw nothing but an empty socket and reported whatever its own
generic read-EOF error is.

For MySQL that is `(2013, 'Lost connection to MySQL server during query')` —
which claims a query was in flight when none ever was. Fediwall 1 printed that
twenty times and it sent the investigation looking for a server that had hung
up, when in truth no HiddenServicePort existed behind the dialled onion port.
Postgres and ldap3 are less misleading ("server closed the connection
unexpectedly", "Connection reset by peer") but equally silent about the cause.

Reproduced for all three against a SOCKS5 proxy that completes the no-auth
greeting and rejects CONNECT with rep=5, running the real python block out of
each test.sh: every one now prints `socks connect failed: SOCKS5 CONNECT failed
rep=5` ahead of its library error.

This does not change any verdict — the root cause is fixed by 2ba3a14b9, where
tor_reachable requires both `services.tor.enabled` and an `exposed: true` that
actually earns a forward. This only stops the next occurrence from lying.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-10 19:33:32 +02:00

179 lines
5.1 KiB
Bash

#!/usr/bin/env bash
# shellcheck shell=bash
#
# svc-db-postgres credential test. Authenticate to the database with real
# credentials and run a trivial query (SELECT 1). The transport depends on the
# variant:
# tor enabled (variant 0, exposed) -> over the Tor network: connect to
# <node-onion>:<port> through the SOCKS proxy (psycopg2 via a
# local SOCKS5 forward) and authenticate.
# tor disabled (variant 1) -> local: authenticate inside the DB
# container over TCP (forces password auth via pg_hba).
#
# Env (rendered into test.env from templates/test.env.j2):
# TOR_ENABLED true|false (lookup('tor_reachable', application_id))
# TOR_SOCKS SOCKS proxy (set in test.env.j2)
# ONION_HOST node onion host (svc-net-tor services.tor.node)
# DB_PORT onion-forwarded database port
# DB_CONTAINER database container name
# DB_USER database user (postgres)
# DB_NAME database name (postgres)
# DB_PASSWORD_B64 base64 password (avoids shell-quoting issues)
# RETRIES attempts (default 20)
# SLEEP_SECONDS wait between (default 15)
set -uo pipefail
RETRIES="${RETRIES:-20}"
SLEEP_SECONDS="${SLEEP_SECONDS:-15}"
DB_PASSWORD="$(printf '%s' "${DB_PASSWORD_B64}" | base64 -d 2>/dev/null)"
[ -n "${DB_PASSWORD}" ] || { echo "[FATAL] DB_PASSWORD_B64 missing or undecodable" >&2; exit 2; }
# Local: authenticate inside the container over TCP (127.0.0.1) so pg_hba
# enforces password auth — a genuine credential check, not a peer/trust socket.
auth_local() {
container exec -e PGPASSWORD="${DB_PASSWORD}" "${DB_CONTAINER}" \
psql -h 127.0.0.1 -U "${DB_USER}" -d "${DB_NAME}" -Atqc 'SELECT 1' 2>/dev/null |
grep -qx 1
}
# Over Tor: psycopg2 (on this controller) connects through a local SOCKS5 -> onion
# forward and authenticates with the credentials.
auth_tor() {
DB_USER="${DB_USER}" DB_NAME="${DB_NAME}" DB_PW="${DB_PASSWORD}" \
python3 - "${TOR_SOCKS}" "${ONION_HOST}" "${DB_PORT}" <<'PY'
import os, socket, sys, threading
proxy, onion, port = sys.argv[1], sys.argv[2], int(sys.argv[3])
ph, pp = proxy.rsplit(":", 1)
pp = int(pp)
def recvall(s, n):
buf = b""
while len(buf) < n:
chunk = s.recv(n - len(buf))
if not chunk:
raise OSError("short read from SOCKS")
buf += chunk
return buf
def socks_connect():
s = socket.create_connection((ph, pp), timeout=120)
s.settimeout(120)
s.sendall(b"\x05\x01\x00")
if recvall(s, 2) != b"\x05\x00":
raise OSError("SOCKS5 no-auth rejected")
host = onion.encode()
s.sendall(b"\x05\x01\x00\x03" + bytes([len(host)]) + host + port.to_bytes(2, "big"))
hdr = recvall(s, 4)
if hdr[1] != 0:
raise OSError("SOCKS5 CONNECT failed rep=%d" % hdr[1])
atyp = hdr[3]
if atyp == 1:
recvall(s, 4)
elif atyp == 3:
recvall(s, recvall(s, 1)[0])
elif atyp == 4:
recvall(s, 16)
recvall(s, 2)
s.settimeout(None)
return s
def pipe(a, b):
try:
while True:
data = a.recv(65536)
if not data:
break
b.sendall(data)
except OSError:
pass
finally:
try:
b.shutdown(socket.SHUT_WR)
except OSError:
pass
def handle(conn):
try:
up = socks_connect()
except OSError as exc:
print("socks connect failed: %s" % exc, file=sys.stderr)
conn.close()
return
threading.Thread(target=pipe, args=(conn, up), daemon=True).start()
pipe(up, conn)
lsock = socket.socket()
lsock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
lsock.bind(("127.0.0.1", 0))
lsock.listen(4)
lport = lsock.getsockname()[1]
def serve():
while True:
try:
conn, _ = lsock.accept()
except OSError:
return
threading.Thread(target=handle, args=(conn,), daemon=True).start()
threading.Thread(target=serve, daemon=True).start()
import psycopg2
try:
conn = psycopg2.connect(
host="127.0.0.1", port=lport, user=os.environ["DB_USER"],
password=os.environ["DB_PW"], dbname=os.environ["DB_NAME"],
connect_timeout=120,
)
cur = conn.cursor()
cur.execute("SELECT 1")
row = cur.fetchone()
cur.close()
conn.close()
except Exception as exc:
print("auth error: %s" % exc, file=sys.stderr)
sys.exit(1)
sys.exit(0 if row and row[0] == 1 else 1)
PY
}
run_auth() {
if [ "${TOR_ENABLED}" = "true" ]; then
auth_tor
else
auth_local
fi
}
if [ "${TOR_ENABLED}" = "true" ]; then
[ -n "${ONION_HOST}" ] || { echo "[FATAL] ONION_HOST unset but tor enabled" >&2; exit 2; }
mode="over Tor"
else
[ -n "${DB_CONTAINER}" ] || { echo "[FATAL] DB_CONTAINER unset for local auth" >&2; exit 2; }
mode="local"
fi
attempt=1
while [ "${attempt}" -le "${RETRIES}" ]; do
if run_auth; then
echo "[OK] postgres credential auth ${mode} succeeded (SELECT 1)"
exit 0
fi
attempt=$((attempt + 1))
[ "${attempt}" -le "${RETRIES}" ] && sleep "${SLEEP_SECONDS}"
done
echo "[FAIL] postgres credential auth ${mode} failed after ${RETRIES} attempts" >&2
exit 1