From 225e277abbc590809519146f1f0b98b10ce26ee5 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 1 Jul 2026 17:19:57 +0000 Subject: [PATCH] asterisk: default to FQDN mode, auto-detect VLANs, auto-sync Caddy certs Defaults install_asterisk() to FQDN networking mode and prompts for VLAN/VPN subnets (with host-network auto-detection to filter out noise like Docker bridges) so phones on other networks get correct NAT/SDP handling from the first boot. The container now mounts Caddy's cert store read-only when Caddy is installed, and the entrypoint syncs a matching Let's Encrypt cert for DOMAIN_NAME automatically, re-checking every 12h to pick up renewals without a restart. Falls back to self-signed only when no matching cert is found. Also fixes a real bug hit in the field: a preserved/migrated pjsip.conf could be missing the transport-udp/transport-tcp sections entirely, with no bind error logged, silently blocking any device that registers without TLS. Adds the same migration-injection already used for transport-tls. --- services/asterisk.sh | 78 +++++++++++++++++++++-- vendor/easy-asterisk/docker/entrypoint.sh | 76 ++++++++++++++++++++++ 2 files changed, 150 insertions(+), 4 deletions(-) diff --git a/services/asterisk.sh b/services/asterisk.sh index 85ae9c4..fd8e64d 100644 --- a/services/asterisk.sh +++ b/services/asterisk.sh @@ -244,16 +244,42 @@ install_asterisk() { # ── Networking mode ─────────────────────────────────────────────────────── echo "" echo " Networking mode:" - echo " 1) LAN-only — no domain, self-signed cert, works on local network/VPN only" - echo " 2) FQDN — TLS + TURN relay, works from anywhere (requires public domain)" + echo " 1) FQDN (recommended) — TLS + TURN relay, every phone connects the" + echo " same way regardless of LAN/VLAN/remote" + echo " 2) LAN-only — no domain, self-signed cert, local network/VPN only" local HA_NETMODE="" prompt_text "Choose [1]:" "1" HA_NETMODE local DOMAIN_NAME="" - if [[ "$HA_NETMODE" == "2" ]]; then - prompt_text "FQDN (e.g. asterisk.${SITE_DOMAIN:-example.com}) [blank=skip]:" "" DOMAIN_NAME + if [[ "$HA_NETMODE" != "2" ]]; then + prompt_text "FQDN (e.g. asterisk.${SITE_DOMAIN:-example.com}) [blank=fall back to LAN-only]:" "" DOMAIN_NAME + [[ -z "$DOMAIN_NAME" ]] && log_warning "No FQDN entered — proceeding in LAN-only mode." fi + # ── Local networks / VLANs ──────────────────────────────────────────────── + # Feeds HAS_VLANS/VLAN_SUBNETS into .env, which the entrypoint reads to add + # extra local_net= entries in pjsip.conf so phones on those subnets get + # correct NAT/SDP handling (this is what fixes the "no sound" symptom for + # devices on a VLAN the server isn't itself attached to). + echo "" + echo " Detecting networks this host can see..." + local DETECTED_NETS="" + DETECTED_NETS="$(ip -o -f inet addr show scope global 2>/dev/null \ + | awk '{print $2, $4}' \ + | grep -Ev '^(docker|br-|veth|tun|tap|wg)' \ + | awk '{ split($2,a,"/"); split(a[1],o,"."); print o[1]"."o[2]"."o[3]".0/"a[2] }' \ + | sort -u)" + if [[ -n "$DETECTED_NETS" ]]; then + echo " This host is directly attached to:" + echo "$DETECTED_NETS" | sed 's/^/ /' + fi + echo " Phones on OTHER VLANs (this server usually can't see those directly)" + echo " still need to be listed here so their media is treated as local/trusted." + local VLAN_SUBNETS_VAL="" + prompt_text "VLAN/VPN subnets, space-separated CIDRs [blank=none]:" "" VLAN_SUBNETS_VAL + local HAS_VLANS_VAL="n" + [[ -n "$VLAN_SUBNETS_VAL" ]] && HAS_VLANS_VAL="y" + # ── Secrets ─────────────────────────────────────────────────────────────── local TURN_PASSWORD TURN_PASSWORD="$(generate_password 24)" @@ -280,6 +306,7 @@ services: - ./spool:/var/spool/asterisk - ./lib:/var/lib/asterisk - ./easy-asterisk.sh:/usr/local/bin/easy-asterisk:ro +CADDY_VOLUME_PLACEHOLDER env_file: .env restart: unless-stopped healthcheck: @@ -316,6 +343,15 @@ services: EOF + # Share Caddy's cert store (read-only) so the entrypoint can auto-sync a + # real Let's Encrypt cert for DOMAIN_NAME instead of falling back to + # self-signed. No-op if Caddy isn't installed on this box. + if [[ -d "$DOCKER_DIR/caddy/data" ]]; then + sed -i "s#CADDY_VOLUME_PLACEHOLDER# - ${DOCKER_DIR}/caddy/data:/caddy-data:ro#" docker-compose.yml + else + sed -i "/CADDY_VOLUME_PLACEHOLDER/d" docker-compose.yml + fi + # ── .env ────────────────────────────────────────────────────────────────── cat > .env << ENV # ── Domain ──────────────────────────────────────────────────── @@ -333,6 +369,12 @@ TURN_SERVER=${TURN_SERVER_VAL} RTP_START=10000 RTP_END=20000 +# ── VLAN/VPN subnets ────────────────────────────────────────── +# Extra local_net= entries for phones on networks this server isn't +# itself attached to. Space-separated CIDRs. +HAS_VLANS=${HAS_VLANS_VAL} +VLAN_SUBNETS=${VLAN_SUBNETS_VAL} + # ── Web admin ───────────────────────────────────────────────── WEB_ADMIN_PORT=8080 WEB_ADMIN_AUTH_DISABLED=false @@ -404,6 +446,34 @@ docker exec -it easy-asterisk easy-asterisk --help Recommended softphones: Linphone, Zoiper, Bria, Grandstream Wave. +For a phone to work the same way regardless of network (LAN, VLAN, remote, +no VPN), register it against `:5061` over TLS — that's what +FQDN mode is for. Plain UDP/TCP on 5060 still works for LAN-only devices, +but only the FQDN+TLS path is location-independent. + +## VLANs / other subnets + +`.env` → `HAS_VLANS`/`VLAN_SUBNETS` lists extra networks (space-separated +CIDRs) this server isn't itself attached to but that phones live on. These +become `local_net=` entries in `pjsip.conf` so NAT/SDP handling is correct +for those devices (missing entries here is the most common cause of calls +connecting with no audio). To change this after install: + +```bash +docker exec -it easy-asterisk easy-asterisk +# Server Settings → Configure VLAN/VPN Subnets +``` + +## TLS certificate + +If Caddy is installed and already holds a Let's Encrypt cert for +`DOMAIN_NAME` (i.e. there's a Caddyfile site block for that exact hostname), +the container mounts Caddy's cert store read-only and the entrypoint syncs +it in automatically on every start — and re-checks every 12h so renewals +get picked up without a restart. No Caddyfile block for the domain, or no +Caddy at all, falls back to a self-signed cert (phones must be configured +to accept it). + ## Web admin Access the Easy Asterisk web interface at http://:8080 diff --git a/vendor/easy-asterisk/docker/entrypoint.sh b/vendor/easy-asterisk/docker/entrypoint.sh index da1cfa8..b62caa9 100755 --- a/vendor/easy-asterisk/docker/entrypoint.sh +++ b/vendor/easy-asterisk/docker/entrypoint.sh @@ -73,8 +73,38 @@ if [[ "$raw_cidr" =~ \.([0-9]+)/([0-9]+)$ ]]; then default_cidr="${raw_cidr%.*}.0/${BASH_REMATCH[2]}" fi +# ── 4b. Sync Caddy-issued TLS cert (if the compose file shared it) ─────── +# If services/asterisk.sh mounted Caddy's data dir read-only at /caddy-data, +# and Caddy already holds a real Let's Encrypt cert for our own DOMAIN_NAME +# (e.g. because a matching site block exists in the Caddyfile), prefer that +# over a self-signed cert — phones then get a CA-trusted TLS connection. +CADDY_CERT_DIR="/caddy-data/caddy/certificates/acme-v02.api.letsencrypt.org-directory" +sync_caddy_cert() { + [[ -n "${DOMAIN_NAME:-}" ]] || return 1 + local src_crt="${CADDY_CERT_DIR}/${DOMAIN_NAME}/${DOMAIN_NAME}.crt" + local src_key="${CADDY_CERT_DIR}/${DOMAIN_NAME}/${DOMAIN_NAME}.key" + [[ -f "$src_crt" && -f "$src_key" ]] || return 1 + + if [[ -f /etc/asterisk/certs/server.crt ]] && cmp -s "$src_crt" /etc/asterisk/certs/server.crt; then + return 1 # already in sync + fi + + mkdir -p /etc/asterisk/certs + cp "$src_crt" /etc/asterisk/certs/server.crt + cp "$src_key" /etc/asterisk/certs/server.key + chown asterisk:asterisk /etc/asterisk/certs/server.crt /etc/asterisk/certs/server.key + chmod 644 /etc/asterisk/certs/server.crt + chmod 600 /etc/asterisk/certs/server.key + return 0 +} + +if [[ -d "$CADDY_CERT_DIR" ]] && sync_caddy_cert; then + log_info "Synced Caddy-issued Let's Encrypt cert for ${DOMAIN_NAME} (CA-trusted, no self-signed warning)" +fi + # ── 5. Generate self-signed certs ────────────────────────────── # Regenerate if missing OR if existing cert lacks SANs (modern TLS clients require them) +# Skipped entirely if the Caddy sync above just installed a real cert. regen_cert=false if [[ ! -f /etc/asterisk/certs/server.crt ]]; then regen_cert=true @@ -279,6 +309,38 @@ EOF chown asterisk:asterisk /etc/asterisk/pjsip.conf fi +# ── Ensure transport-udp/transport-tcp exist (same migration path as TLS) ── +# Same scenario as above: a preserved/migrated pjsip.conf can be missing the +# plain SIP transports entirely, silently, with no bind error — LAN/VLAN +# devices registering without TLS then can never connect. Inject if absent. +if [[ -f /etc/asterisk/pjsip.conf ]] && ! grep -q "^\[transport-udp\]" /etc/asterisk/pjsip.conf; then + log_info "transport-udp missing from pjsip.conf — adding UDP transport..." + cat >> /etc/asterisk/pjsip.conf << EOF + +[transport-udp] +type=transport +protocol=udp +bind=0.0.0.0:5060 +${nat_settings} + +EOF + chown asterisk:asterisk /etc/asterisk/pjsip.conf +fi + +if [[ -f /etc/asterisk/pjsip.conf ]] && ! grep -q "^\[transport-tcp\]" /etc/asterisk/pjsip.conf; then + log_info "transport-tcp missing from pjsip.conf — adding TCP transport..." + cat >> /etc/asterisk/pjsip.conf << EOF + +[transport-tcp] +type=transport +protocol=tcp +bind=0.0.0.0:5060 +${nat_settings} + +EOF + chown asterisk:asterisk /etc/asterisk/pjsip.conf +fi + # ── rtp.conf (always regenerated) ── # ICE is enabled so Asterisk participates in ICE negotiation with clients. # stunaddr/turnaddr are NOT set here because: @@ -378,6 +440,20 @@ if [[ -f "$WEB_ADMIN_SCRIPT" ]]; then python3 "$WEB_ADMIN_SCRIPT" & fi +# ── 10b. Keep the Caddy-issued cert fresh across renewals ──────────────── +# Let's Encrypt certs renew every ~60-90 days without the container restarting. +# Re-check periodically and hot-reload Asterisk when Caddy's copy changes. +if [[ -d "$CADDY_CERT_DIR" ]]; then + ( + while sleep 43200; do # every 12h + if sync_caddy_cert; then + log_info "Caddy cert renewed for ${DOMAIN_NAME} — reloading Asterisk" + asterisk -rx "core reload" >/dev/null 2>&1 || true + fi + done + ) & +fi + # ── 11. Signal handling for clean shutdown ──────────────────── cleanup() { log_info "Shutting down..."