diff --git a/CLAUDE.md b/CLAUDE.md index 0bd7821..fd75310 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -44,11 +44,26 @@ public-FQDN-only flow, hand-built Caddy site block, remote Authelia, Cloud Firewall — behind that one answer. Two lessons worth reusing: - **Don't rename a live install's directory or containers.** New installs - land in `~/docker/asterisk` with `easy-asterisk`; a pre-merge droplet keeps - `~/docker/asterisk-digital-ocean` and `easy-asterisk-do`, because its - Caddyfile block, UFW rules, Cloud Firewall, CrowdSec acquisition and PSTN - trunk all name those exact paths. `_asterisk_resolve_layout()` picks - whichever exists, and every sibling service probes both. + land in `~/docker/asterisk` with a container named `asterisk`; a pre-merge + droplet keeps `~/docker/asterisk-digital-ocean` and `easy-asterisk-do`, + because its Caddyfile block, UFW rules, Cloud Firewall, CrowdSec + acquisition and PSTN trunk all name those exact paths. + `_asterisk_resolve_layout()` picks whichever directory exists, and every + sibling service probes both. The plain container name was itself renamed + once already — from `easy-asterisk` (this repo's original choice, reusing + the vendor CLI tool's own name, `/usr/local/bin/easy-asterisk` inside the + container — unrelated, never renamed) to plain `asterisk`, matching every + other service's own `container_name == service name` convention. The same + "don't rename under a running deployment" rule applied: every resolver + (`_asterisk_resolve_layout()` and the duplicated copies in + `security-dashboard.sh`, `sms-inbound.sh`, `pstn-trunk.sh`, + `tools/pstn-test-check.sh`) reads the container name out of the box's own + `docker-compose.yml` instead of assuming it, so an existing `easy-asterisk` + install keeps working unchanged. Migrating one to the new name is a + deliberate, one-time action on that box (edit `docker-compose.yml`'s + `container_name:` for both Asterisk and its coturn sidecar, `docker compose + down` + `up -d`) — once done, every sibling service re-reads it from that + same file and follows automatically. - **Check whether a "flavor-specific" behavior was actually flavor-specific.** The Asterisk security-logging patch and the `logs/full` logrotate config were droplet-only purely because that's where they got written first — the diff --git a/services/asterisk.sh b/services/asterisk.sh index 1a808cd..7265000 100644 --- a/services/asterisk.sh +++ b/services/asterisk.sh @@ -298,6 +298,21 @@ register_service asterisk homelab "Easy Asterisk PBX (intercom/VoIP; auto-tunes # naming. Every sibling service in this repo (pstn-trunk, security-dashboard, # crowdsec) already probes for both directories, so both layouts stay fully # supported without further special-casing. +# +# Container name itself: new installs use the plain "asterisk" (matching +# every other service's own container_name == service name convention — +# "easy-" was this repo's install-time container name before, left over from +# when the vendor CLI tool's own name (`easy-asterisk`, still installed at +# /usr/local/bin/easy-asterisk inside the container — unrelated, never +# renamed) got reused for the container too. A box that already has a +# running container is read directly from its own docker-compose.yml instead +# of assumed from the directory, so an existing "easy-asterisk" install +# keeps working with no silent rename — same reasoning as the droplet-layout +# preservation above, just one level down (container name, not directory). +# Migrating an existing box to the new name is a deliberate, one-time action +# (edit docker-compose.yml's container_name + coturn, `docker compose down` +# + `up -d`) — once done, every sibling service here re-reads it from that +# same file and follows automatically, no further changes needed anywhere. _asterisk_resolve_layout() { if [[ -f "$DOCKER_DIR/asterisk-digital-ocean/docker-compose.yml" ]]; then ASTERISK_DIR="$DOCKER_DIR/asterisk-digital-ocean" @@ -306,9 +321,13 @@ _asterisk_resolve_layout() { ASTERISK_PROJECT="asterisk-do" else ASTERISK_DIR="$DOCKER_DIR/asterisk" - ASTERISK_CONTAINER="easy-asterisk" - ASTERISK_COTURN="easy-asterisk-coturn" ASTERISK_PROJECT="asterisk" + ASTERISK_CONTAINER="" + if [[ -f "$ASTERISK_DIR/docker-compose.yml" ]]; then + ASTERISK_CONTAINER="$(grep -m1 '^[[:space:]]*container_name:' "$ASTERISK_DIR/docker-compose.yml" | awk '{print $2}')" + fi + [[ -z "$ASTERISK_CONTAINER" ]] && ASTERISK_CONTAINER="asterisk" + ASTERISK_COTURN="${ASTERISK_CONTAINER}-coturn" fi } @@ -1284,7 +1303,7 @@ _asterisk_ensure_live_voicemail_include() { docker exec "$CONTAINER_NAME" asterisk -rx "dialplan reload" &>/dev/null || true } -# ── pjsip.conf: [transport-tls] keep_alive_interval ───────────────────────── +# ── pjsip.conf: [global] keep_alive_interval ───────────────────────────────── # Mitigation for WiFi/LAN SIP clients (e.g. Sipnetic on Android) dropping # their TLS registration every few seconds on some hosts but not others — # confirmed live on an IONOS VPS (never reproduced on a DigitalOcean droplet @@ -1293,15 +1312,28 @@ _asterisk_ensure_live_voicemail_include() { # live disconnect and CrowdSec/packet-loss both ruled out first. Leading # theory: an idle-connection timeout somewhere in IONOS's own network # virtualization layer, below anything client-side tools can see. PJSIP's -# `keep_alive_interval` sends a lightweight double-CRLF over the transport's -# TLS connection on a timer, which is the standard fix for exactly this +# `keep_alive_interval` sends a lightweight double-CRLF over connection-oriented +# transports (TCP/TLS) on a timer, which is the standard fix for exactly this # class of "idle SIP/TLS connection gets silently dropped" symptom. # -# This is a *transport* option, not an endpoint/AOR option — `qualify_frequency` -# (already set globally) is an endpoint-level OPTIONS ping that re-establishes -# a dropped connection, it doesn't stop the drop from happening in the first -# place. `rtp_keepalive` (mobile devices) is unrelated: RTP media keepalive -# during an active call, not SIP signaling connection keepalive while idle. +# **This is a `type=global` option, not a `type=transport` option** — it does +# not exist on `[transport-tls]`/`[transport-tcp]` objects at all, on any +# Asterisk version. An earlier version of this patch inserted it into +# `[transport-tls]` (right after `protocol=tls`), which sorcery always +# rejects: "Could not find option suitable for category 'transport-tls' +# named 'keep_alive_interval'" — and rejecting the option means the whole +# `type=transport` object fails to be created, so `transport-tls` never +# binds at all. Confirmed live: this silently took down TLS SIP entirely on +# a box that had picked up the patch, reproducing the exact same "network +# error on all calls" symptom the rest of this file's health check exists to +# catch. The anchor below targets `type=global` inside `[global]` instead. +# +# This is a *transport-behavior* option, not an endpoint/AOR option — +# `qualify_frequency` (already set globally) is an endpoint-level OPTIONS +# ping that re-establishes a dropped connection, it doesn't stop the drop +# from happening in the first place. `rtp_keepalive` (mobile devices) is +# unrelated: RTP media keepalive during an active call, not SIP signaling +# connection keepalive while idle. _asterisk_patch_keepalive_vendor_files() { local EA_DIR="$1" local ENTRYPOINT="$EA_DIR/docker/entrypoint.sh" @@ -1313,15 +1345,20 @@ _asterisk_patch_keepalive_vendor_files() { for f in "$ENTRYPOINT" "$EASY1" "$EASY2"; do [[ -f "$f" ]] || continue - if ! grep -q '^protocol=tls$' "$f"; then - log_warning "$(basename "$f"): '[transport-tls]' anchor not found — vendor template changed upstream." - log_warning " Add 'keep_alive_interval=15' manually inside [transport-tls] in this file's pjsip.conf heredoc." + # Undo the old, incorrect [transport-tls] placement if an earlier + # run of this function already patched it there. + if grep -q '^protocol=tls$' "$f" && grep -A2 '^protocol=tls$' "$f" | grep -q '^keep_alive_interval='; then + sed -i '/^protocol=tls$/{n;/^keep_alive_interval=/d}' "$f" + fi + if ! grep -q '^type=global$' "$f"; then + log_warning "$(basename "$f"): '[global]' anchor not found — vendor template changed upstream." + log_warning " Add 'keep_alive_interval=15' manually inside [global] (not [transport-tls]) in this file's pjsip.conf heredoc." continue fi - grep -q '^keep_alive_interval=' "$f" || sed -i '/^protocol=tls$/a keep_alive_interval=15' "$f" + grep -q '^keep_alive_interval=' "$f" || sed -i '/^type=global$/a keep_alive_interval=15' "$f" done - log_success "Vendor generator functions patched for TLS transport keepalive." + log_success "Vendor generator functions patched for SIP keepalive (pjsip.conf [global])." } # Live-file counterpart to the vendor-template patch above, same reasoning @@ -1332,20 +1369,38 @@ _asterisk_patch_keepalive_vendor_files() { # a transport object — PJSIP transports are bound at module load, not # reloadable via sorcery like endpoints/AORs are — so this restarts the # container rather than issuing a reload, same as _asterisk_ensure_live_dangerously. +# +# `keep_alive_interval` belongs in `[global]` (`type=global`), not +# `[transport-tls]` — see the comment on _asterisk_patch_keepalive_vendor_files +# for why the old placement made sorcery reject the transport object outright +# (killing TLS SIP entirely, not just the keepalive). This also self-heals a +# box that already has the bad `[transport-tls]` entry from before that fix. _asterisk_ensure_live_keepalive() { local EA_DIR="$1" CONTAINER_NAME="$2" local CONF_LIVE="$EA_DIR/config/asterisk/pjsip.conf" [[ -f "$CONF_LIVE" ]] || return 0 - if ! grep -q '^\[transport-tls\]$' "$CONF_LIVE"; then - log_warning "Couldn't find '[transport-tls]' in the live pjsip.conf — add" + + local CHANGED=false + if sed -n '/^\[transport-tls\]$/,/^\[/{/^keep_alive_interval=/p}' "$CONF_LIVE" | grep -q .; then + sed -i '/^\[transport-tls\]$/,/^\[/{/^keep_alive_interval=/d}' "$CONF_LIVE" + log_warning "Removed 'keep_alive_interval' from [transport-tls] — that option doesn't exist on a PJSIP" + log_warning "transport object and was making the whole TLS transport fail to bind. Moving it to [global]." + CHANGED=true + fi + + if ! grep -q '^\[global\]$' "$CONF_LIVE"; then + log_warning "Couldn't find '[global]' in the live pjsip.conf — add" log_warning "'keep_alive_interval=15' manually inside that section, then: docker restart ${CONTAINER_NAME}" return 0 fi - grep -q '^keep_alive_interval=' "$CONF_LIVE" && return 0 - - sed -i '/^\[transport-tls\]$/,/^\[/{/^protocol=tls$/a keep_alive_interval=15 + if ! grep -q '^keep_alive_interval=' "$CONF_LIVE"; then + sed -i '/^\[global\]$/,/^\[/{/^type=global$/a keep_alive_interval=15 }' "$CONF_LIVE" - log_success "Patched keep_alive_interval=15 into the live pjsip.conf's [transport-tls] transport." + log_success "Patched keep_alive_interval=15 into the live pjsip.conf's [global] section." + CHANGED=true + fi + + [[ "$CHANGED" == true ]] || return 0 log_info "Restarting Asterisk to apply (transport options aren't picked up by a reload)..." docker restart "$CONTAINER_NAME" &>/dev/null \ diff --git a/services/pstn-trunk.sh b/services/pstn-trunk.sh index cb8d180..15ab16c 100644 --- a/services/pstn-trunk.sh +++ b/services/pstn-trunk.sh @@ -1436,7 +1436,7 @@ _pstn_check_killswitch_clear() { # _pstn_install_periodic_timer above) ─────────────────────────────────────── _pstn_write_usage_alert_script() { local FILE="$1" EA_DIR="$2" ASTERISK_DIR="$3" RATE="$4" MONTH_THRESHOLD="$5" \ - BURST_THRESHOLD="$6" MAX_MONTHLY_SPEND="$7" NTFY_URL="$8" CONTAINER_NAME="${9:-easy-asterisk}" + BURST_THRESHOLD="$6" MAX_MONTHLY_SPEND="$7" NTFY_URL="$8" CONTAINER_NAME="${9:-asterisk}" cat > "$FILE" << 'EOF' #!/bin/bash # Auto-generated by services/pstn-trunk.sh — do not edit directly, re-run @@ -1719,7 +1719,7 @@ _pstn_apply_settings() { local EA_DIR="$1" ASTERISK_DIR="$2" local SERVER="$3" SERVER_IPS="$4" DID="$5" local RING_EXTS="$6" NTFY_URL="$7" RATE="$8" MONTH_THRESHOLD="$9" BURST_THRESHOLD="${10}" - local PROVIDER_NAME="${11}" MAX_MONTHLY_SPEND="${12:-0}" CONTAINER_NAME="${13:-easy-asterisk}" + local PROVIDER_NAME="${11}" MAX_MONTHLY_SPEND="${12:-0}" CONTAINER_NAME="${13:-asterisk}" _pstn_patch_vendor_files "$EA_DIR" || return 1 @@ -1879,8 +1879,18 @@ install_pstn-trunk() { local KILLSWITCH_FILE="$ASTERISK_DIR/pstn-trunk-killswitch.conf" local PERSONAL_DIDS_FILE="$ASTERISK_DIR/pstn-personal-dids.conf" local SETTINGS_FILE="$EA_DIR/.pstn-trunk.env" - local CONTAINER_NAME="easy-asterisk" - [[ "$ASTERISK_KIND" == "asterisk-digital-ocean" ]] && CONTAINER_NAME="easy-asterisk-do" + # Read the box's own container_name instead of assuming — new installs + # use plain "asterisk" now, but an existing "easy-asterisk" install + # (this repo's container name before that rename) keeps working + # unchanged until someone deliberately migrates it. See + # services/asterisk.sh's _asterisk_resolve_layout for the reasoning. + local CONTAINER_NAME="asterisk" + if [[ "$ASTERISK_KIND" == "asterisk-digital-ocean" ]]; then + CONTAINER_NAME="easy-asterisk-do" + elif [[ -f "$EA_DIR/docker-compose.yml" ]]; then + CONTAINER_NAME="$(grep -m1 '^[[:space:]]*container_name:' "$EA_DIR/docker-compose.yml" | awk '{print $2}')" + [[ -z "$CONTAINER_NAME" ]] && CONTAINER_NAME="asterisk" + fi if [ "$DRY_RUN" = true ]; then echo "[DRY-RUN] Would require an existing asterisk install (droplet or home/LAN)" @@ -1988,8 +1998,13 @@ install_pstn-trunk() { # never picking up dialplan changes at all. Re-assert the fresh # detection here, discarding the sourced value, so it can't drift # from reality and self-heals the persisted file too. - CONTAINER_NAME="easy-asterisk" - [[ "$ASTERISK_KIND" == "asterisk-digital-ocean" ]] && CONTAINER_NAME="easy-asterisk-do" + CONTAINER_NAME="asterisk" + if [[ "$ASTERISK_KIND" == "asterisk-digital-ocean" ]]; then + CONTAINER_NAME="easy-asterisk-do" + elif [[ -f "$EA_DIR/docker-compose.yml" ]]; then + CONTAINER_NAME="$(grep -m1 '^[[:space:]]*container_name:' "$EA_DIR/docker-compose.yml" | awk '{print $2}')" + [[ -z "$CONTAINER_NAME" ]] && CONTAINER_NAME="asterisk" + fi _pstn_check_killswitch_clear "$ASTERISK_DIR" _pstn_apply_settings "$EA_DIR" "$ASTERISK_DIR" \ "$TRUNK_SERVER" "${TRUNK_SERVER_IPS:-}" "$TRUNK_DID" \ diff --git a/services/security-dashboard.sh b/services/security-dashboard.sh index 6401e44..b6763dc 100644 --- a/services/security-dashboard.sh +++ b/services/security-dashboard.sh @@ -105,7 +105,15 @@ install_security-dashboard() { if [[ "$ASTERISK_EA_DIR" == *asterisk-digital-ocean ]]; then ASTERISK_EA_CONTAINER="easy-asterisk-do" elif [ -n "$ASTERISK_EA_DIR" ]; then - ASTERISK_EA_CONTAINER="easy-asterisk" + # Read the box's own container_name instead of assuming — new + # installs use plain "asterisk" now, but an existing "easy-asterisk" + # install (this repo's container name before that rename) keeps + # working unchanged until someone deliberately migrates it. See + # services/asterisk.sh's _asterisk_resolve_layout for the reasoning. + if [[ -f "$ASTERISK_EA_DIR/docker-compose.yml" ]]; then + ASTERISK_EA_CONTAINER="$(grep -m1 '^[[:space:]]*container_name:' "$ASTERISK_EA_DIR/docker-compose.yml" | awk '{print $2}')" + fi + [[ -z "$ASTERISK_EA_CONTAINER" ]] && ASTERISK_EA_CONTAINER="asterisk" fi echo "" diff --git a/services/sms-inbound.sh b/services/sms-inbound.sh index 1681d88..340aa23 100644 --- a/services/sms-inbound.sh +++ b/services/sms-inbound.sh @@ -53,7 +53,15 @@ _sms_detect_container_name() { if [[ "$_ea_dir" == *asterisk-digital-ocean ]]; then echo "easy-asterisk-do" else - echo "easy-asterisk" + # Read the box's own container_name instead of assuming — new + # installs use plain "asterisk" now, but an existing "easy-asterisk" + # install (this repo's container name before that rename) keeps + # working unchanged until someone deliberately migrates it. See + # services/asterisk.sh's _asterisk_resolve_layout for the reasoning. + local _name="" + [[ -f "$_ea_dir/docker-compose.yml" ]] && \ + _name="$(grep -m1 '^[[:space:]]*container_name:' "$_ea_dir/docker-compose.yml" | awk '{print $2}')" + echo "${_name:-asterisk}" fi } diff --git a/tools/pstn-test-check.sh b/tools/pstn-test-check.sh index eb46388..8157e24 100755 --- a/tools/pstn-test-check.sh +++ b/tools/pstn-test-check.sh @@ -56,9 +56,13 @@ fi # ── Container + directory detection ───────────────────────────────────────── section "Detecting install" -CONTAINER="$(docker ps --format '{{.Names}}' 2>/dev/null | grep -m1 -E '^easy-asterisk(-do)?$' || true)" +# "asterisk" is this repo's current container name; "easy-asterisk" is what +# an install kept from before that rename (never silently renamed under a +# running deployment); "easy-asterisk-do"/"asterisk-do" cover a DigitalOcean +# droplet install, old or new naming. Whichever is actually running wins. +CONTAINER="$(docker ps --format '{{.Names}}' 2>/dev/null | grep -m1 -E '^(easy-)?asterisk(-do)?$' || true)" if [ -z "$CONTAINER" ]; then - fail "No running easy-asterisk / easy-asterisk-do container found — is asterisk installed and started?" + fail "No running asterisk / easy-asterisk / *-do container found — is asterisk installed and started?" echo "" echo " $PASS passed, $WARN warnings, $FAIL failed. Stopping — nothing else can be checked without a running container." exit 1 @@ -196,8 +200,7 @@ if [ -z "$TURN_SERVER" ]; then warn "Add it via: sudo ./setup.sh asterisk (update mode)" else if grep -q '^ coturn:' "$EA_DIR/docker-compose.yml" 2>/dev/null; then - COTURN_CONTAINER="easy-asterisk-coturn" - [[ "$CONTAINER" == *-do ]] && COTURN_CONTAINER="easy-asterisk-do-coturn" + COTURN_CONTAINER="${CONTAINER}-coturn" ok "Using an embedded, per-Asterisk coturn ($COTURN_CONTAINER); tested separately below." else COTURN_CONTAINER="coturn"