fix(test): inspect user Asterisk service before nonproduction calls
This commit is contained in:
@@ -32,6 +32,16 @@ restricted evidence ownership is recorded in `diagnostic-errors.txt` and fails
|
||||
the check; an already failed call keeps its nonzero result. Isolated tests
|
||||
replace host tools with fakes: they do not prove a real host or supplier is ready.
|
||||
|
||||
For the **nonproduction user-level Asterisk service only**, pass
|
||||
`--asterisk-scope user` and explicitly set `ASTERISK_BIN` to its installed
|
||||
native binary and `ASTERISK_CONFIG` to its user-owned `asterisk.conf`; the
|
||||
native library directory defaults to the binary's sibling `../lib` and may be
|
||||
set via `ASTERISK_LIBRARY_PATH`. This mode checks `go-sip-asterisk.service`
|
||||
through `systemctl --user`, runs the CLI with the exact config and collects
|
||||
the user journal. Missing settings or status fail closed; it neither skips
|
||||
the installed-artifact checksum/capture requirements nor proves reboot
|
||||
persistence when lingering is disabled. The default remains system scope.
|
||||
|
||||
The offline OSS environment file is a fixture for isolated tests only. It
|
||||
contains no real credentials or production approval. The former
|
||||
`ai-dental-meiba-v1.json` is archived at
|
||||
|
||||
@@ -12,6 +12,7 @@ Options:
|
||||
--evidence-dir DIR Evidence root (default: /var/lib/sip-go-agent/evidence/<call-id>).
|
||||
--interface IFACE Capture interface (default: default-route interface; any fallback).
|
||||
--run-as USER Run COMMAND as this non-root user (default: rogee).
|
||||
--asterisk-scope SCOPE system (default) or explicitly configured user service.
|
||||
--sip-port PORT SIP UDP port (default: 5060).
|
||||
--rtp-start PORT RTP range start (default: 10000).
|
||||
--rtp-end PORT RTP range end (default: 10800).
|
||||
@@ -29,6 +30,7 @@ evidence_dir=""
|
||||
recording_dir="/var/lib/sip-go-agent/recordings"
|
||||
interface="any"
|
||||
run_as="rogee"
|
||||
asterisk_scope="system"
|
||||
sip_port=5060
|
||||
rtp_start=10000
|
||||
rtp_end=10800
|
||||
@@ -47,6 +49,7 @@ while (($#)); do
|
||||
--recording-dir) [[ $# -ge 2 ]] || usage; recording_dir=$2; shift 2 ;;
|
||||
--interface) [[ $# -ge 2 ]] || usage; interface=$2; shift 2 ;;
|
||||
--run-as) [[ $# -ge 2 ]] || usage; run_as=$2; shift 2 ;;
|
||||
--asterisk-scope) [[ $# -ge 2 ]] || usage; asterisk_scope=$2; shift 2 ;;
|
||||
--sip-port) [[ $# -ge 2 ]] || usage; sip_port=$2; shift 2 ;;
|
||||
--rtp-start) [[ $# -ge 2 ]] || usage; rtp_start=$2; shift 2 ;;
|
||||
--rtp-end) [[ $# -ge 2 ]] || usage; rtp_end=$2; shift 2 ;;
|
||||
@@ -65,6 +68,7 @@ case "$environment" in
|
||||
production) echo 'production requires the separate production gate' >&2; exit 1 ;;
|
||||
*) echo 'invalid non-production environment' >&2; exit 1 ;;
|
||||
esac
|
||||
[[ "$asterisk_scope" == system || "$asterisk_scope" == user ]] || { echo 'invalid Asterisk service scope' >&2; exit 1; }
|
||||
[[ "$call_id" =~ ^[A-Za-z0-9._-]+$ ]] || { echo 'invalid call id' >&2; exit 1; }
|
||||
[[ "$trunk" =~ ^(provider-primary|provider-second|provider-third|trunk-[A-Za-z0-9._-]+)$ ]] || { echo 'trunk is not an approved non-production trunk id' >&2; exit 1; }
|
||||
[[ "$target" =~ ^(15003164745|15830461047)$ ]] || { echo 'target is outside the approved outbound whitelist' >&2; exit 1; }
|
||||
@@ -96,6 +100,12 @@ install -d -o "$run_as" -g "$run_as" -m 0700 "$recording_dir"
|
||||
touch "$evidence_dir/recording-start.marker"
|
||||
|
||||
asterisk_bin="${ASTERISK_BIN:-/usr/sbin/asterisk}"
|
||||
if [[ "$asterisk_scope" == user ]]; then
|
||||
[[ -n "${ASTERISK_BIN:-}" && -n "${ASTERISK_CONFIG:-}" && -r "$ASTERISK_CONFIG" ]] || { echo 'explicit user Asterisk configuration required; fail-closed'; exit 1; }
|
||||
asterisk_lib="${ASTERISK_LIBRARY_PATH:-$(dirname "$asterisk_bin")/../lib}"
|
||||
[[ -d "$asterisk_lib" ]] || { echo 'user Asterisk runtime libraries unavailable; fail-closed'; exit 1; }
|
||||
asterisk_user_uid="$(id -u "$run_as")" || { echo 'user Asterisk service account unavailable; fail-closed'; exit 1; }
|
||||
fi
|
||||
tcpdump_bin="${TCPDUMP_BIN:-$(command -v tcpdump || true)}"
|
||||
[[ -x "$asterisk_bin" ]] || { echo 'Asterisk CLI unavailable; fail-closed'; exit 1; }
|
||||
[[ -n "$tcpdump_bin" && -x "$tcpdump_bin" ]] || { echo 'tcpdump unavailable; fail-closed'; exit 1; }
|
||||
@@ -120,12 +130,34 @@ redact() {
|
||||
sed -E 's/(password|secret|token|authorization|api[_-]?key)[^[:space:]]*/\1=<redacted>/Ig'
|
||||
}
|
||||
|
||||
if ! systemctl is-enabled asterisk.service >"$evidence_dir/asterisk-enabled.txt" 2>&1 ||
|
||||
asterisk_service() {
|
||||
if [[ "$asterisk_scope" == user ]]; then
|
||||
runuser -u "$run_as" -- env XDG_RUNTIME_DIR="/run/user/$asterisk_user_uid" DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/$asterisk_user_uid/bus" systemctl --user "$@" go-sip-asterisk.service
|
||||
else
|
||||
systemctl "$@" asterisk.service
|
||||
fi
|
||||
}
|
||||
asterisk_cli() {
|
||||
if [[ "$asterisk_scope" == user ]]; then
|
||||
runuser -u "$run_as" -- env LD_LIBRARY_PATH="$asterisk_lib" "$asterisk_bin" -C "$ASTERISK_CONFIG" -rx "$1"
|
||||
else
|
||||
"$asterisk_bin" -rx "$1"
|
||||
fi
|
||||
}
|
||||
asterisk_journal() {
|
||||
if [[ "$asterisk_scope" == user ]]; then
|
||||
runuser -u "$run_as" -- env XDG_RUNTIME_DIR="/run/user/$asterisk_user_uid" DBUS_SESSION_BUS_ADDRESS="unix:path=/run/user/$asterisk_user_uid/bus" journalctl --user -u go-sip-asterisk.service "$@"
|
||||
else
|
||||
journalctl -u asterisk.service "$@"
|
||||
fi
|
||||
}
|
||||
|
||||
if ! asterisk_service is-enabled >"$evidence_dir/asterisk-enabled.txt" 2>&1 ||
|
||||
! grep -qx enabled "$evidence_dir/asterisk-enabled.txt"; then
|
||||
echo 'Asterisk service must be enabled and active; fail-closed' >&2
|
||||
exit 1
|
||||
fi
|
||||
if ! systemctl is-active asterisk.service >"$evidence_dir/asterisk-active.txt" 2>&1 ||
|
||||
if ! asterisk_service is-active >"$evidence_dir/asterisk-active.txt" 2>&1 ||
|
||||
! grep -qx active "$evidence_dir/asterisk-active.txt"; then
|
||||
echo 'Asterisk service must be enabled and active; fail-closed' >&2
|
||||
exit 1
|
||||
@@ -134,21 +166,21 @@ uname -a >"$evidence_dir/uname.txt"
|
||||
cat /etc/os-release >"$evidence_dir/os-release.txt"
|
||||
ip -brief address >"$evidence_dir/ip-address.txt"
|
||||
ss -lunp >"$evidence_dir/udp-listeners.txt" 2>&1 || ss -lun >"$evidence_dir/udp-listeners.txt"
|
||||
"$asterisk_bin" -rx "module show like res_ari.so" 2>&1 | redact >"$evidence_dir/ari-module-status.txt"
|
||||
"$asterisk_bin" -rx "http show status" 2>&1 | redact >"$evidence_dir/ari-http-status.txt"
|
||||
asterisk_cli "module show like res_ari.so" 2>&1 | redact >"$evidence_dir/ari-module-status.txt"
|
||||
asterisk_cli "http show status" 2>&1 | redact >"$evidence_dir/ari-http-status.txt"
|
||||
if ! grep -Eq 'res_ari\.so.*Running' "$evidence_dir/ari-module-status.txt" ||
|
||||
! grep -Fq 'Server Enabled and Bound' "$evidence_dir/ari-http-status.txt" ||
|
||||
! grep -Fq '/ari/' "$evidence_dir/ari-http-status.txt"; then
|
||||
echo 'ARI module or HTTP route unavailable; fail-closed' >&2
|
||||
exit 1
|
||||
fi
|
||||
"$asterisk_bin" -rx "pjsip show endpoint $trunk" 2>&1 | redact >"$evidence_dir/pjsip-endpoint.txt"
|
||||
asterisk_cli "pjsip show endpoint $trunk" 2>&1 | redact >"$evidence_dir/pjsip-endpoint.txt"
|
||||
if ! grep -Eq 'Endpoint:[[:space:]]*' "$evidence_dir/pjsip-endpoint.txt" || ! grep -Fq "$trunk" "$evidence_dir/pjsip-endpoint.txt"; then
|
||||
echo 'PJSIP endpoint unavailable; fail-closed' >&2
|
||||
exit 1
|
||||
fi
|
||||
"$asterisk_bin" -rx "pjsip show contacts" 2>&1 | redact >"$evidence_dir/pjsip-contacts-before.txt"
|
||||
"$asterisk_bin" -rx "core show channels verbose" 2>&1 | redact >"$evidence_dir/channels-before.txt"
|
||||
asterisk_cli "pjsip show contacts" 2>&1 | redact >"$evidence_dir/pjsip-contacts-before.txt"
|
||||
asterisk_cli "core show channels verbose" 2>&1 | redact >"$evidence_dir/channels-before.txt"
|
||||
if ! sha256sum /opt/sip-go-agent/current/sip-go-agent /etc/sip-go-agent/artifacts/*.json /etc/sip-go-agent/ai/*.json >"$evidence_dir/installed-sha256.txt" 2>&1; then
|
||||
echo 'installed package/config SHA-256 unavailable; fail-closed' >&2
|
||||
exit 1
|
||||
@@ -263,7 +295,7 @@ cleanup() {
|
||||
trap - EXIT
|
||||
set +e
|
||||
stop_capture
|
||||
if ((logger_enabled)) && ! "$asterisk_bin" -rx "pjsip set logger off" >"$evidence_dir/pjsip-logger-off.txt" 2>&1; then
|
||||
if ((logger_enabled)) && ! asterisk_cli "pjsip set logger off" >"$evidence_dir/pjsip-logger-off.txt" 2>&1; then
|
||||
printf 'PJSIP logger stop failed\n' >>"$evidence_dir/diagnostic-errors.txt"
|
||||
evidence_failed=1
|
||||
fi
|
||||
@@ -284,7 +316,7 @@ cleanup() {
|
||||
printf 'recording SHA-256 unavailable\n' >>"$evidence_dir/diagnostic-errors.txt"
|
||||
evidence_failed=1
|
||||
fi
|
||||
if ! journalctl -u asterisk.service --since "$started_at" --no-pager 2>/dev/null | redact >"$evidence_dir/asterisk-journal.txt"; then
|
||||
if ! asterisk_journal --since "$started_at" --no-pager 2>/dev/null | redact >"$evidence_dir/asterisk-journal.txt"; then
|
||||
printf 'Asterisk journal unavailable\n' >>"$evidence_dir/diagnostic-errors.txt"
|
||||
evidence_failed=1
|
||||
fi
|
||||
@@ -345,7 +377,7 @@ reserve_attempt() {
|
||||
}
|
||||
|
||||
reserve_attempt
|
||||
"$asterisk_bin" -rx "pjsip set logger on" >"$evidence_dir/pjsip-logger-on.txt" 2>&1 || { echo 'cannot enable PJSIP logger; fail-closed' >&2; exit 1; }
|
||||
asterisk_cli "pjsip set logger on" >"$evidence_dir/pjsip-logger-on.txt" 2>&1 || { echo 'cannot enable PJSIP logger; fail-closed' >&2; exit 1; }
|
||||
logger_enabled=1
|
||||
|
||||
"$tcpdump_bin" -i "$interface" -nn -s0 -U -w "$evidence_dir/capture.pcap" \
|
||||
@@ -369,8 +401,8 @@ call_status=$?
|
||||
set -e
|
||||
printf '%s\n' "call_exit=$call_status"
|
||||
stop_capture
|
||||
"$asterisk_bin" -rx "pjsip show contacts" 2>&1 | redact >"$evidence_dir/pjsip-contacts-after.txt"
|
||||
"$asterisk_bin" -rx "core show channels verbose" 2>&1 | redact >"$evidence_dir/channels-after.txt"
|
||||
asterisk_cli "pjsip show contacts" 2>&1 | redact >"$evidence_dir/pjsip-contacts-after.txt"
|
||||
asterisk_cli "core show channels verbose" 2>&1 | redact >"$evidence_dir/channels-after.txt"
|
||||
capture_packets="$(awk '/ packets captured/{print $1; exit}' "$evidence_dir/tcpdump.log" 2>/dev/null || true)"
|
||||
[[ "$capture_packets" =~ ^[0-9]+$ ]] || capture_packets=0
|
||||
capture_status=0
|
||||
|
||||
@@ -11,6 +11,82 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestNonprodUserAsteriskScopeRequiresExplicitConfig(t *testing.T) {
|
||||
tools := t.TempDir()
|
||||
for name, script := range map[string]string{
|
||||
"id": "if [ \"$1\" = -u ]; then echo 0; else exec /usr/bin/id \"$@\"; fi\n",
|
||||
"date": "if [ \"${TZ-}\" = Asia/Shanghai ] && [ \"$1\" = +%H%M ]; then echo 1000; else exec /usr/bin/date \"$@\"; fi\n",
|
||||
} {
|
||||
if err := os.WriteFile(filepath.Join(tools, name), []byte("#!/bin/sh\n"+script), 0700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
currentUser, err := user.Current()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
root := t.TempDir()
|
||||
command := exec.Command("bash", "../../deploys/test/nonprod-call-evidence.sh", "--environment", "mock",
|
||||
"--asterisk-scope", "user", "--trunk", "provider-primary", "--target", "15003164745", "--run-as", currentUser.Username,
|
||||
"--interface", "lo", "--recording-dir", filepath.Join(root, "recordings"), "--evidence-dir", filepath.Join(root, "evidence"),
|
||||
"--attempt-ledger", filepath.Join(root, "attempts.tsv"), "--preflight-only", "--", "/bin/true")
|
||||
command.Env = append(os.Environ(), "PATH="+tools+":"+os.Getenv("PATH"), "ASTERISK_BIN=/bin/true", "ASTERISK_CONFIG=")
|
||||
output, err := command.CombinedOutput()
|
||||
if err == nil || !strings.Contains(string(output), "explicit user Asterisk configuration required") {
|
||||
t.Fatalf("user-scope diagnostics must refuse unbound Asterisk instance: err=%v output=%s", err, output)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNonprodUserAsteriskScopeUsesUserServiceAndConfiguredCLI(t *testing.T) {
|
||||
tools := t.TempDir()
|
||||
write := func(name, body string) string {
|
||||
t.Helper()
|
||||
path := filepath.Join(tools, name)
|
||||
if err := os.WriteFile(path, []byte("#!/bin/sh\n"+body), 0700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return path
|
||||
}
|
||||
write("id", "if [ \"$1\" = -u ]; then echo 0; else exec /usr/bin/id \"$@\"; fi\n")
|
||||
write("date", "if [ \"${TZ-}\" = Asia/Shanghai ] && [ \"$1\" = +%H%M ]; then echo 1000; else exec /usr/bin/date \"$@\"; fi\n")
|
||||
write("runuser", "[ \"$1\" = -u ] && [ \"$3\" = -- ] || exit 99\nshift 3\nexec \"$@\"\n")
|
||||
write("systemctl", "[ \"$1\" = --user ] && [ \"$3\" = go-sip-asterisk.service ] || exit 99\nprintf '%s\\n' \"$2\" >>\"$TEST_SERVICE_LOG\"\ncase \"$2\" in is-enabled) echo enabled;; is-active) echo active;; *) exit 99;; esac\n")
|
||||
write("ip", "echo 'lo UNKNOWN 127.0.0.1/8'\n")
|
||||
write("ss", "echo 'udp 127.0.0.1:5060'\n")
|
||||
write("tcpdump", "case \" $* \" in *' -c 1 '*) exit 124;; esac\nexit 99\n")
|
||||
asterisk := write("asterisk", "[ \"$1\" = -C ] && [ \"$2\" = \"$TEST_CONFIG\" ] && [ \"$3\" = -rx ] || exit 98\nprintf '%s\\n' \"$4\" >>\"$TEST_CLI_LOG\"\ncase \"$4\" in 'module show like res_ari.so') echo 'res_ari.so Asterisk REST Interface 0 Running';; 'http show status') echo 'Server Enabled and Bound to 127.0.0.1:8088'; echo '/ari/...';; 'pjsip show endpoint '*) echo 'Endpoint: not-loaded';; *) exit 97;; esac\n")
|
||||
root := t.TempDir()
|
||||
configFile := filepath.Join(root, "asterisk.conf")
|
||||
if err := os.WriteFile(configFile, []byte("[directories]\n"), 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
currentUser, err := user.Current()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
serviceLog := filepath.Join(root, "service-checked")
|
||||
cliLog := filepath.Join(root, "cli-checked")
|
||||
command := exec.Command("bash", "../../deploys/test/nonprod-call-evidence.sh", "--environment", "mock",
|
||||
"--asterisk-scope", "user", "--trunk", "provider-primary", "--target", "15003164745", "--run-as", currentUser.Username,
|
||||
"--interface", "lo", "--recording-dir", filepath.Join(root, "recordings"), "--evidence-dir", filepath.Join(root, "evidence"),
|
||||
"--attempt-ledger", filepath.Join(root, "attempts.tsv"), "--preflight-only", "--", "/bin/true")
|
||||
command.Env = append(os.Environ(), "PATH="+tools+":"+os.Getenv("PATH"), "ASTERISK_BIN="+asterisk, "ASTERISK_CONFIG="+configFile,
|
||||
"ASTERISK_LIBRARY_PATH="+tools, "TCPDUMP_BIN="+filepath.Join(tools, "tcpdump"), "TEST_CONFIG="+configFile,
|
||||
"TEST_SERVICE_LOG="+serviceLog, "TEST_CLI_LOG="+cliLog)
|
||||
output, err := command.CombinedOutput()
|
||||
if err == nil || !strings.Contains(string(output), "PJSIP endpoint unavailable") {
|
||||
t.Fatalf("missing user-service endpoint must block before dialing: err=%v output=%s", err, output)
|
||||
}
|
||||
serviceChecks, err := os.ReadFile(serviceLog)
|
||||
if err != nil || !strings.Contains(string(serviceChecks), "is-enabled\nis-active\n") {
|
||||
t.Fatalf("user service status was not verified: %v %q", err, serviceChecks)
|
||||
}
|
||||
cliChecks, err := os.ReadFile(cliLog)
|
||||
if err != nil || !strings.Contains(string(cliChecks), "module show like res_ari.so\nhttp show status\npjsip show endpoint provider-primary\n") {
|
||||
t.Fatalf("configured user Asterisk CLI was not used: %v %q", err, cliChecks)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNonprodPreflightRejectsIncompleteCapturedEvidence(t *testing.T) {
|
||||
tools := t.TempDir()
|
||||
write := func(name, body string) string {
|
||||
|
||||
Reference in New Issue
Block a user