feat(cell): configure private loopback ARI for user Asterisk

This commit is contained in:
2026-10-04 12:35:37 +08:00
parent fce01d1be6
commit 1d12007244
3 changed files with 96 additions and 2 deletions
+8 -2
View File
@@ -22,8 +22,14 @@ installs `go-sip-asterisk.service` under `systemd --user`. Production requires
`loginctl enable-linger rogee` and a verified reboot-persistent `enabled+active`
service; `--nonprod` allows a session-scoped Debian 12 native build but does
not certify reboot persistence. The management-approved static `pjsip.conf`,
ARI and RTP configuration must be supplied separately. The bundled stage has
no live SIP trunk or dialing authorization; verify loaded endpoints and contacts
ARI and RTP configuration must be supplied separately. For the isolated
nonproduction user service only, `configure-asterisk-user-ari.sh` creates
private `ari.conf`, loopback-only `http.conf` and an owner-only `ari-secret`
without printing credentials, overwriting existing files or restarting the
service. Restart the user service only after separately verifying zero active
calls, then read back ARI HTTP and both `enabled`/`active` state. Do not treat
this local test setup as a management-approved production configuration.
The bundled stage has no live SIP trunk or dialing authorization; verify loaded endpoints and contacts
before any call. Do not substitute another Asterisk version or a container image.
The Go Agent package only consumes the resulting approved Cell artifact and
reports its applied revision. Local Mock fixtures do not prove real services.
+33
View File
@@ -0,0 +1,33 @@
#!/usr/bin/env bash
# Prepare private ARI/HTTP config for the native nonproduction user service.
# Does not change the service state; restart only after confirming zero calls.
set -euo pipefail
config="$HOME/.config/go-sip-asterisk"
[[ -d "$config" && -r "$config/asterisk.conf" ]] || { echo 'user Asterisk config missing; fail-closed' >&2; exit 1; }
for name in ari.conf http.conf ari-secret; do
[[ ! -e "$config/$name" && ! -L "$config/$name" ]] || { echo "$name already exists; refusing to overwrite" >&2; exit 1; }
done
umask 077
stage="$(mktemp -d "$config/.ari-setup.XXXXXXXX")"
cleanup() {
rm -f -- "$stage/ari.conf" "$stage/http.conf" "$stage/ari-secret"
rmdir -- "$stage"
}
trap cleanup EXIT
secret="$(openssl rand -hex 32)"
[[ "$secret" =~ ^[0-9a-f]{64}$ ]] || { echo 'could not generate ARI credential' >&2; exit 1; }
printf '%s' "$secret" >"$stage/ari-secret"
printf '[general]\nenabled = yes\npretty = no\n\n[go-sip-agent]\ntype = user\nread_only = no\npassword = %s\npassword_format = plain\n' "$secret" >"$stage/ari.conf"
printf '[general]\nenabled = yes\nbindaddr = 127.0.0.1\nbindport = 8088\n' >"$stage/http.conf"
created=()
for name in ari-secret ari.conf http.conf; do
if ! ln -- "$stage/$name" "$config/$name"; then
for own_file in "${created[@]}"; do rm -f -- "$config/$own_file"; done
echo "cannot create $name without overwriting another file; fail-closed" >&2
exit 1
fi
created+=("$name")
done
echo 'private Asterisk ARI/HTTP configuration created; service not restarted'
+55
View File
@@ -0,0 +1,55 @@
package config_test
import (
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
)
func TestConfigureUserAsteriskARIPrivateAndNoOverwrite(t *testing.T) {
home := t.TempDir()
root := filepath.Join(home, ".config", "go-sip-asterisk")
if err := os.MkdirAll(root, 0700); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(root, "asterisk.conf"), []byte("[directories]\n"), 0600); err != nil {
t.Fatal(err)
}
path := "../../deploys/cell/configure-asterisk-user-ari.sh"
run := func() (string, error) {
command := exec.Command("bash", path)
command.Env = append(os.Environ(), "HOME="+home)
output, err := command.CombinedOutput()
return string(output), err
}
if output, err := run(); err != nil {
t.Fatalf("private ARI configuration failed: %v %s", err, output)
}
for _, name := range []string{"ari.conf", "http.conf", "ari-secret"} {
info, err := os.Stat(filepath.Join(root, name))
if err != nil || info.Mode().Perm() != 0600 {
t.Fatalf("ARI file %s must be private: info=%v err=%v", name, info, err)
}
}
password, err := os.ReadFile(filepath.Join(root, "ari-secret"))
if err != nil || len(password) != 64 {
t.Fatalf("ARI credential must be a generated 32-byte hex secret: err=%v length=%d", err, len(password))
}
ari, err := os.ReadFile(filepath.Join(root, "ari.conf"))
if err != nil || !strings.Contains(string(ari), "password = "+string(password)) || !strings.Contains(string(ari), "read_only = no") {
t.Fatalf("ARI config did not bind generated credential: err=%v", err)
}
http, err := os.ReadFile(filepath.Join(root, "http.conf"))
if err != nil || !strings.Contains(string(http), "bindaddr = 127.0.0.1") || !strings.Contains(string(http), "bindport = 8088") {
t.Fatalf("HTTP must be explicitly restricted to the local Cell: err=%v", err)
}
if output, err := run(); err == nil || !strings.Contains(output, "already exists") {
t.Fatalf("repeat install must not overwrite private credentials: err=%v output=%s", err, output)
}
again, err := os.ReadFile(filepath.Join(root, "ari-secret"))
if err != nil || string(again) != string(password) {
t.Fatal("existing ARI credential was changed by a repeat install")
}
}