feat(doorman): doorctl unlock endpoint — HA->Pi callback over Tailscale [#345]

Founder-approved 2026-09-03 (relay load side disconnected during
build). systemd socket-activated, Tailscale-bound, token+source
allowlist, EXIT-trap self-securing relay. 9/9 doorctl tests (auth
matrix + relay cycle, mocked relay), full suite + shellcheck clean.

https://projects.knownelement.com/issues/345
This commit is contained in:
2026-09-03 07:04:12 -05:00
parent 37d27f1158
commit 4fefd10abb
3 changed files with 173 additions and 0 deletions
Executable
+61
View File
@@ -0,0 +1,61 @@
#!/usr/bin/env bash
#
# bin/doorctl.sh — door unlock endpoint, systemd socket-activated.
#
# One instance per connection: systemd hands the accepted socket to
# stdin/stdout and exports REMOTE_ADDR. Reads a single HTTP request
# line; fires the door relay only for GET /unlock/<token> from an
# allowlisted source. Responds BEFORE the hold so callers are never
# blocked for the hold duration.
#
# Env (from /etc/default/doorman via the service unit):
# DOORMAN_UNLOCK_TOKEN shared secret in the URL path (required)
# DOORMAN_ALLOWED_SRC space-separated source IPs allowed to unlock
# DOORMAN_RELAY_DEV usbrelay device.channel (e.g. 3X9XI_1)
# DOORMAN_USBRELAY path to usbrelay binary
# DOORMAN_HOLD relay hold seconds
#
set -u
log() { logger -t doorctl -- "$1" 2>/dev/null || true; }
respond() { printf '%s\r\nContent-Length: 0\r\nConnection: close\r\n\r\n' "$1"; }
deny() {
respond 'HTTP/1.0 403 Forbidden'
log "DENY from ${REMOTE_ADDR:-unknown}: $2"
exit 0
}
read -r reqline || true
reqline=${reqline:-$'\r'}
method=${reqline%% *}
if [ "$method" != "GET" ]; then
deny "bad method"
fi
rest=${reqline#* }
path=${rest%% *}
[ "$path" = "/unlock/${DOORMAN_UNLOCK_TOKEN:-}" ] || deny "bad path or token"
allowed="${DOORMAN_ALLOWED_SRC:-}"
if [ -n "$allowed" ]; then
case " $allowed " in
*" ${REMOTE_ADDR:-} "*) : ;;
*) deny "source not allowlisted" ;;
esac
fi
hold="${DOORMAN_HOLD:-10}"
secure() { "${DOORMAN_USBRELAY:-/usr/bin/usbrelay}" "${DOORMAN_RELAY_DEV}=0" >/dev/null 2>&1; }
# Self-securing: whatever kills this script (systemd timeout, OOM, signal),
# the relay returns to 0 = door secure. Never rely on the happy path alone.
trap secure EXIT
log "UNLOCK from ${REMOTE_ADDR:-unknown}: ${DOORMAN_RELAY_DEV}=1 for ${hold}s"
respond 'HTTP/1.0 200 OK'
"${DOORMAN_USBRELAY:-/usr/bin/usbrelay}" "${DOORMAN_RELAY_DEV}=1" >/dev/null 2>&1
sleep "$hold"
secure
log "secure: ${DOORMAN_RELAY_DEV}=0"