Add full TURN relay support for reliable calls from any network

Problem: Calls via FQDN work "sometimes" because STUN-only mode fails
behind strict NAT (cellular, Proton VPN, hotel WiFi, corporate firewalls).
STUN tells clients their public IP, but can't relay media when direct
UDP paths are blocked. TURN relays media as a fallback.

Changes:

coturn (docker-compose.yml):
- Upgraded from STUN-only to full STUN+TURN relay
- Uses long-term credential mechanism (--lt-cred-mech)
- Credentials shared between coturn and Asterisk automatically
- Relay port range 49152-49252 (configurable, ~50 concurrent relayed calls)
- Always-on (removed --profile stun gate)
- Conditional --external-ip (only set when PUBLIC_IP is provided)

Entrypoint (docker/entrypoint.sh):
- Auto-detects public IP (ifconfig.me → icanhazip.com → api.ipify.org)
- Auto-generates TURN password on first startup (saved to config)
- Configures rtp.conf with icesupport + stunaddr + turnaddr + credentials
- Updates pjsip.conf external_*_address if public IP changes
- Always enables ICE, STUN, and TURN for Docker deployments

Main script (easy-asterisk-v0.10.0.sh):
- Added TURN_ENABLED, TURN_SERVER, TURN_USERNAME, TURN_PASSWORD to
  load_config/save_config
- repair_core_configs: rtp.conf now includes turnaddr/turnusername/turnpassword
  when TURN is enabled
- Bash device creation: Docker mode defaults to FQDN (TLS) for all new devices
- Python device creation: reads TURN_ENABLED, auto-selects FQDN in Docker
- Main menu shows TURN status

.env.example:
- Comprehensive documentation for every setting
- DOMAIN_NAME is the only required setting
- Port forwarding requirements clearly listed
- TURN credentials and relay port range documented

The result: `docker compose up -d` gives you a fully working PBX where
any SIP client on any network can connect reliably via FQDN:5061.

https://claude.ai/code/session_01Vm6NLaQuzM4VosAotqS1q8
This commit is contained in:
Claude
2026-02-21 16:09:50 +00:00
parent d05f1f9e3b
commit 8934d71391
4 changed files with 386 additions and 216 deletions
+68 -33
View File
@@ -1,57 +1,92 @@
# ================================================================ # ================================================================
# Easy Asterisk - Environment Configuration # Easy Asterisk - Environment Configuration
# #
# Copy this file to .env and modify as needed: # Setup:
# cp .env.example .env # 1. cp .env.example .env
# 2. Set DOMAIN_NAME (the only required setting)
# 3. docker compose up -d
# 4. docker exec -it easy-asterisk easy-asterisk
# #
# Then start with: # Port forwarding required on your router:
# docker compose up -d # Asterisk only # 5061/tcp → SIP TLS signaling
# docker compose --profile stun up -d # Asterisk + STUN server # 3478/udp → STUN/TURN (NAT traversal + media relay)
# 3478/tcp → TURN TCP fallback (for restrictive networks)
# 10000-20000/udp → RTP media (or your custom range below)
#
# How it works:
# - All SIP clients connect to DOMAIN_NAME:5061 (TLS)
# - coturn handles NAT traversal (STUN) and media relay (TURN)
# - Works from any network: LAN, cellular, Proton VPN, hotel WiFi
# - TURN credentials are auto-generated if TURN_PASSWORD is empty
# ================================================================ # ================================================================
# ── Network Mode ───────────────────────────────────────────── # ── Domain Name (REQUIRED) ────────────────────────────────────
# Leave DOMAIN_NAME empty for LAN/VPN mode (recommended for VPN setups) # The FQDN that points to this server's public IP.
# Set to your FQDN for internet calling mode (requires TLS certs) # This is what SIP clients use to connect.
# Example: asterisk.yourdomain.com
DOMAIN_NAME= DOMAIN_NAME=
ENABLE_TLS=n
# Primary network CIDR (auto-detected if empty) # ── Public IP ─────────────────────────────────────────────────
# Your server's public IP address.
# Leave empty to auto-detect (uses ifconfig.me).
# Set manually if auto-detection fails (e.g., behind double NAT).
PUBLIC_IP=
# ── TLS ───────────────────────────────────────────────────────
# Always "y" for remote access. Self-signed certs are auto-generated.
# For trusted certs (no client warnings), mount your Let's Encrypt
# certs into /etc/asterisk/certs/ via docker compose volumes.
ENABLE_TLS=y
# ── Local Network ─────────────────────────────────────────────
# Your LAN CIDR. Auto-detected if empty.
# Example: 192.168.1.0/24 # Example: 192.168.1.0/24
LOCAL_CIDR= LOCAL_CIDR=
# ── VPN / VLAN Subnets ────────────────────────────────────── # ── Additional Subnets (optional) ─────────────────────────────
# Set HAS_VLANS=y and list your VPN/VLAN subnets (space-separated) # Only needed for site-to-site VPNs or VLANs where the server
# This tells Asterisk to treat these as local networks # has a direct route to client IPs (e.g., WireGuard, Tailscale).
#
# NOT needed for client-side VPNs (Proton, NordVPN, etc.)
# - Those clients appear with random public IPs
# - TURN handles media relay for them automatically
# #
# Examples: # Examples:
# WireGuard: 10.8.0.0/24 # WireGuard: VLAN_SUBNETS=10.8.0.0/24
# OpenVPN: 10.10.0.0/24 # Tailscale: VLAN_SUBNETS=100.64.0.0/10
# Tailscale: 100.64.0.0/10 # Multiple: VLAN_SUBNETS=10.8.0.0/24 10.10.0.0/24
# Third-party: Check your VPN client for the assigned subnet
HAS_VLANS=n HAS_VLANS=n
VLAN_SUBNETS= VLAN_SUBNETS=
# ── STUN Server ────────────────────────────────────────────── # ── TURN/STUN Credentials ────────────────────────────────────
# For self-hosted coturn (recommended for DNS-filtered networks): # Used by coturn for TURN relay authentication.
# 1. Start with: docker compose --profile stun up -d # If TURN_PASSWORD is empty, a random password is generated on
# 2. Set STUN_SERVER to your server's VPN IP:3478 # first startup and saved to /etc/easy-asterisk/config.
# Example: STUN_SERVER=10.8.0.1:3478
# #
# For Google STUN (requires DNS access to stun.l.google.com): # These credentials are shared between coturn and Asterisk.
# STUN_SERVER=stun.l.google.com:19302 # SIP clients do NOT need these - only the server uses them.
# TURN_USERNAME=easyasterisk
# Leave empty if VPN provides direct routing (no NAT between endpoints) TURN_PASSWORD=
STUN_SERVER=
VPN_ICE_ENABLED=n
CUSTOM_STUN_SERVER=
# ── RTP Port Range ─────────────────────────────────────────── # ── TURN Relay Port Range ─────────────────────────────────────
# Ports coturn uses for media relay. Forward this range on your router.
# Default is 100 ports (enough for ~50 simultaneous relayed calls).
# Most calls use direct paths; TURN relay is the fallback.
TURN_RELAY_MIN=49152
TURN_RELAY_MAX=49252
# ── RTP Port Range ────────────────────────────────────────────
# Asterisk's own RTP media ports. Forward this range on your router.
# Default: 10000-20000 (10,000 ports) # Default: 10000-20000 (10,000 ports)
# For constrained environments, reduce to e.g. 10000-10100 # For constrained environments: 10000-10200
RTP_START=10000 RTP_START=10000
RTP_END=20000 RTP_END=20000
# ── Web Admin ──────────────────────────────────────────────── # ── Web Admin ────────────────────────────────────────────────
# HTTP management interface. Access via browser at:
# http://your-server:8080/clients
#
# For HTTPS: put this behind Caddy or nginx reverse proxy,
# then set WEB_ADMIN_AUTH_DISABLED=true (let the proxy handle auth).
WEB_ADMIN_PORT=8080 WEB_ADMIN_PORT=8080
# Set to true if using a reverse proxy with its own auth
WEB_ADMIN_AUTH_DISABLED=false WEB_ADMIN_AUTH_DISABLED=false
+51 -47
View File
@@ -2,15 +2,13 @@
# Easy Asterisk - Docker Compose # Easy Asterisk - Docker Compose
# #
# Usage: # Usage:
# docker compose up -d # Asterisk only # docker compose up -d # Start everything
# docker compose --profile stun up -d # Asterisk + self-hosted STUN
# docker exec -it easy-asterisk easy-asterisk # Interactive management # docker exec -it easy-asterisk easy-asterisk # Interactive management
# docker exec -it easy-asterisk vpn-diagnostics # VPN diagnostics # docker exec -it easy-asterisk vpn-diagnostics # VPN diagnostics
# docker exec -it easy-asterisk dns-whitelist # DNS whitelist check
# #
# For third-party VPNs with DNS filtering: # All clients connect via FQDN (TLS) regardless of their network.
# Use --profile stun to run a self-hosted STUN server # coturn provides STUN (NAT detection) + TURN (media relay) so calls
# This eliminates all external DNS dependencies # work even behind strict firewalls, cellular NAT, or VPNs like Proton.
# ================================================================ # ================================================================
services: services:
@@ -21,9 +19,12 @@ services:
container_name: easy-asterisk container_name: easy-asterisk
# Host networking required for: # Host networking required for:
# - RTP media ports (10000-20000 UDP) - too many to map individually # - RTP media ports (10000-20000 UDP) - too many to map individually
# - VPN interface access (tun/tap/wg devices)
# - Proper NAT detection and SIP Contact headers # - Proper NAT detection and SIP Contact headers
# - Direct access to coturn on localhost
network_mode: host network_mode: host
depends_on:
coturn:
condition: service_healthy
volumes: volumes:
- asterisk-config:/etc/asterisk - asterisk-config:/etc/asterisk
- easy-asterisk-config:/etc/easy-asterisk - easy-asterisk-config:/etc/easy-asterisk
@@ -31,28 +32,32 @@ services:
- asterisk-spool:/var/spool/asterisk - asterisk-spool:/var/spool/asterisk
- asterisk-lib:/var/lib/asterisk - asterisk-lib:/var/lib/asterisk
environment: environment:
# ── Network Mode ── # ── Domain (REQUIRED for remote access) ──
# Leave DOMAIN_NAME empty for LAN/VPN mode (recommended) # Your FQDN that points to this server's public IP
- DOMAIN_NAME=${DOMAIN_NAME:-} - DOMAIN_NAME=${DOMAIN_NAME:?Set DOMAIN_NAME in .env}
- ENABLE_TLS=${ENABLE_TLS:-n} - ENABLE_TLS=${ENABLE_TLS:-y}
# ── Public IP ──
# Auto-detected if empty. Set manually if detection fails.
- PUBLIC_IP=${PUBLIC_IP:-}
# ── Local Network ──
- LOCAL_CIDR=${LOCAL_CIDR:-} - LOCAL_CIDR=${LOCAL_CIDR:-}
# ── VPN Configuration ── # ── Additional Subnets ──
# Add your VPN subnet(s) here, space-separated # Space-separated CIDRs for VLANs, site-to-site VPNs, etc.
# Example: 10.8.0.0/24 or 100.64.0.0/10 (Tailscale) # NOT needed for client-side VPNs (Proton, NordVPN) - TURN handles those
- HAS_VLANS=${HAS_VLANS:-n} - HAS_VLANS=${HAS_VLANS:-n}
- VLAN_SUBNETS=${VLAN_SUBNETS:-} - VLAN_SUBNETS=${VLAN_SUBNETS:-}
# ── STUN Server ── # ── TURN/STUN Server ──
# For self-hosted coturn: use your server's VPN/LAN IP + port 3478 # Points to the coturn service (auto-configured)
# Example: STUN_SERVER=10.8.0.1:3478 - TURN_ENABLED=y
# Leave empty to disable STUN (fine if VPN provides direct routing) - TURN_SERVER=${DOMAIN_NAME:?}:3478
- STUN_SERVER=${STUN_SERVER:-} - TURN_USERNAME=${TURN_USERNAME:-easyasterisk}
- VPN_ICE_ENABLED=${VPN_ICE_ENABLED:-n} - TURN_PASSWORD=${TURN_PASSWORD:-}
- CUSTOM_STUN_SERVER=${CUSTOM_STUN_SERVER:-}
# ── RTP Port Range ── # ── RTP Port Range ──
# Reduce range for constrained environments
- RTP_START=${RTP_START:-10000} - RTP_START=${RTP_START:-10000}
- RTP_END=${RTP_END:-20000} - RTP_END=${RTP_END:-20000}
@@ -66,36 +71,35 @@ services:
timeout: 5s timeout: 5s
retries: 3 retries: 3
# ── Self-Hosted STUN Server (coturn) ─────────────────────── # ── TURN/STUN Relay Server (coturn) ──────────────────────────
# Activate with: docker compose --profile stun up -d # Provides:
# STUN - Tells clients their public IP (NAT detection)
# TURN - Relays media when direct UDP paths are blocked
# (corporate firewalls, cellular NAT, Proton VPN, etc.)
# #
# Why self-hosted STUN? # Without TURN, calls work "sometimes" - with TURN, they always work.
# - No external DNS dependencies (critical for DNS-filtered networks)
# - STUN reached by IP address, not hostname
# - Faster response than public STUN servers
# - Works entirely within your VPN network
#
# After starting, set STUN_SERVER to your server's IP:3478
# in the .env file and restart the asterisk container.
coturn: coturn:
image: coturn/coturn:latest image: coturn/coturn:latest
container_name: easy-asterisk-stun container_name: easy-asterisk-coturn
network_mode: host network_mode: host
command: > entrypoint: ["/bin/sh", "-c"]
-n command:
--no-auth - |
--no-tls # Build coturn arguments
--no-dtls ARGS="-n --listening-port=3478 --fingerprint --lt-cred-mech"
--stun-only ARGS="$$ARGS --user=${TURN_USERNAME:-easyasterisk}:${TURN_PASSWORD:-changeme}"
--listening-port=3478 ARGS="$$ARGS --realm=${DOMAIN_NAME:-localhost}"
--fingerprint ARGS="$$ARGS --min-port=${TURN_RELAY_MIN:-49152}"
--no-cli ARGS="$$ARGS --max-port=${TURN_RELAY_MAX:-49252}"
--no-multicast-peers # Only add external-ip if PUBLIC_IP is set
--no-loopback-peers if [ -n "${PUBLIC_IP:-}" ]; then
--log-file=stdout ARGS="$$ARGS --external-ip=${PUBLIC_IP}"
fi
ARGS="$$ARGS --no-tls --no-dtls --no-cli"
ARGS="$$ARGS --no-multicast-peers --no-loopback-peers"
ARGS="$$ARGS --log-file=stdout"
exec turnserver $$ARGS
restart: unless-stopped restart: unless-stopped
profiles:
- stun
healthcheck: healthcheck:
test: ["CMD-SHELL", "ss -uln | grep -q ':3478'"] test: ["CMD-SHELL", "ss -uln | grep -q ':3478'"]
interval: 30s interval: 30s
+156 -79
View File
@@ -2,8 +2,11 @@
# ================================================================ # ================================================================
# Easy Asterisk Docker Entrypoint # Easy Asterisk Docker Entrypoint
# #
# Initializes configuration, starts Asterisk + Web Admin # Fully automated:
# Uses the same config functions as the main script # - Detects public IP
# - Generates TURN credentials if not provided
# - Configures Asterisk with FQDN, TLS, ICE, STUN, TURN
# - Starts web admin + Asterisk
# ================================================================ # ================================================================
set -e set -e
@@ -11,70 +14,120 @@ set -e
GREEN='\033[0;32m' GREEN='\033[0;32m'
YELLOW='\033[1;33m' YELLOW='\033[1;33m'
CYAN='\033[0;36m' CYAN='\033[0;36m'
RED='\033[0;31m'
NC='\033[0m' NC='\033[0m'
log_info() { echo -e "${GREEN}[entrypoint]${NC} $1"; } log_info() { echo -e "${GREEN}[entrypoint]${NC} $1"; }
log_warn() { echo -e "${YELLOW}[entrypoint]${NC} $1"; } log_warn() { echo -e "${YELLOW}[entrypoint]${NC} $1"; }
log_error() { echo -e "${RED}[entrypoint]${NC} $1"; }
CONFIG_DIR="/etc/easy-asterisk" CONFIG_DIR="/etc/easy-asterisk"
CONFIG_FILE="${CONFIG_DIR}/config" CONFIG_FILE="${CONFIG_DIR}/config"
WEB_ADMIN_SCRIPT="/usr/local/bin/easy-asterisk-webadmin" WEB_ADMIN_SCRIPT="/usr/local/bin/easy-asterisk-webadmin"
# ── Helper: generate random password ─────────────────────────
gen_password() {
openssl rand -base64 18 | tr -dc 'a-zA-Z0-9' | head -c 24
}
# ── 1. Ensure asterisk user exists ─────────────────────────── # ── 1. Ensure asterisk user exists ───────────────────────────
if ! id asterisk >/dev/null 2>&1; then if ! id asterisk >/dev/null 2>&1; then
useradd -r -s /bin/false -d /var/lib/asterisk asterisk 2>/dev/null || true useradd -r -s /bin/false -d /var/lib/asterisk asterisk 2>/dev/null || true
fi fi
# ── 2. Generate self-signed certs if missing ────────────────── # ── 2. Detect public IP ──────────────────────────────────────
PUBLIC_IP="${PUBLIC_IP:-}"
if [[ -z "$PUBLIC_IP" ]]; then
log_info "Auto-detecting public IP..."
PUBLIC_IP=$(curl -s -4 --connect-timeout 5 ifconfig.me 2>/dev/null || true)
if [[ -z "$PUBLIC_IP" ]]; then
PUBLIC_IP=$(curl -s -4 --connect-timeout 5 icanhazip.com 2>/dev/null || true)
fi
if [[ -z "$PUBLIC_IP" ]]; then
PUBLIC_IP=$(curl -s -4 --connect-timeout 5 api.ipify.org 2>/dev/null || true)
fi
fi
if [[ -n "$PUBLIC_IP" ]]; then
log_info "Public IP: ${PUBLIC_IP}"
else
log_warn "Could not detect public IP. Set PUBLIC_IP in .env"
fi
# ── 3. Generate TURN password if not provided ─────────────────
TURN_USERNAME="${TURN_USERNAME:-easyasterisk}"
if [[ -z "${TURN_PASSWORD:-}" ]] || [[ "${TURN_PASSWORD}" == "changeme" ]]; then
# Check if we already generated one previously
if [[ -f "$CONFIG_FILE" ]] && grep -q "^TURN_PASSWORD=" "$CONFIG_FILE"; then
TURN_PASSWORD=$(grep "^TURN_PASSWORD=" "$CONFIG_FILE" | cut -d'"' -f2)
fi
if [[ -z "${TURN_PASSWORD:-}" ]] || [[ "${TURN_PASSWORD}" == "changeme" ]]; then
TURN_PASSWORD=$(gen_password)
log_info "Generated TURN password (saved to config)"
fi
fi
# ── 4. Detect local network ──────────────────────────────────
local_ip=$(hostname -I 2>/dev/null | awk '{print $1}')
raw_cidr=$(ip -o -f inet addr show 2>/dev/null | awk '/scope global/ {print $4}' | head -1)
default_cidr="$raw_cidr"
if [[ "$raw_cidr" =~ \.([0-9]+)/([0-9]+)$ ]]; then
default_cidr="${raw_cidr%.*}.0/${BASH_REMATCH[2]}"
fi
# ── 5. Generate self-signed certs if missing ──────────────────
if [[ ! -f /etc/asterisk/certs/server.crt ]]; then if [[ ! -f /etc/asterisk/certs/server.crt ]]; then
log_info "Generating self-signed TLS certificate..." log_info "Generating self-signed TLS certificate..."
mkdir -p /etc/asterisk/certs mkdir -p /etc/asterisk/certs
# Use DOMAIN_NAME as CN if available
cn="${DOMAIN_NAME:-asterisk-local}"
openssl req -x509 -nodes -days 3650 -newkey rsa:2048 \ openssl req -x509 -nodes -days 3650 -newkey rsa:2048 \
-keyout /etc/asterisk/certs/server.key \ -keyout /etc/asterisk/certs/server.key \
-out /etc/asterisk/certs/server.crt \ -out /etc/asterisk/certs/server.crt \
-subj "/CN=asterisk-local" 2>/dev/null -subj "/CN=${cn}" 2>/dev/null
chown asterisk:asterisk /etc/asterisk/certs/server.* chown asterisk:asterisk /etc/asterisk/certs/server.*
chmod 644 /etc/asterisk/certs/server.crt chmod 644 /etc/asterisk/certs/server.crt
chmod 600 /etc/asterisk/certs/server.key chmod 600 /etc/asterisk/certs/server.key
fi fi
# ── 3. Create config from environment if first run ──────────── # ── 6. Write config file ─────────────────────────────────────
mkdir -p "$CONFIG_DIR" mkdir -p "$CONFIG_DIR"
if [[ ! -f "$CONFIG_FILE" ]]; then # Determine TURN/STUN server address
log_info "Creating initial configuration from environment..." turn_server="${TURN_SERVER:-${DOMAIN_NAME:-$local_ip}:3478}"
cat > "$CONFIG_FILE" << EOF cat > "$CONFIG_FILE" << EOF
# Easy Asterisk Configuration (Docker) # Easy Asterisk Configuration (Docker) - $(date)
KIOSK_USER="" KIOSK_USER=""
KIOSK_UID="" KIOSK_UID=""
KIOSK_EXTENSION="" KIOSK_EXTENSION=""
KIOSK_NAME="" KIOSK_NAME=""
SIP_PASSWORD="" SIP_PASSWORD=""
ASTERISK_HOST="${ASTERISK_HOST:-}" ASTERISK_HOST="${DOMAIN_NAME:-$local_ip}"
DOMAIN_NAME="${DOMAIN_NAME:-}" DOMAIN_NAME="${DOMAIN_NAME:-}"
ENABLE_TLS="${ENABLE_TLS:-n}" ENABLE_TLS="${ENABLE_TLS:-y}"
HAS_VLANS="${HAS_VLANS:-n}" HAS_VLANS="${HAS_VLANS:-n}"
VLAN_SUBNETS="${VLAN_SUBNETS:-}" VLAN_SUBNETS="${VLAN_SUBNETS:-}"
CERT_PATH="" CERT_PATH=""
KEY_PATH="" KEY_PATH=""
INSTALLED_SERVER="y" INSTALLED_SERVER="y"
INSTALLED_CLIENT="n" INSTALLED_CLIENT="n"
CURRENT_PUBLIC_IP="" CURRENT_PUBLIC_IP="${PUBLIC_IP}"
PTT_DEVICE="" PTT_DEVICE=""
PTT_KEYCODE="" PTT_KEYCODE=""
LOCAL_CIDR="${LOCAL_CIDR:-}" LOCAL_CIDR="${LOCAL_CIDR:-$default_cidr}"
WEB_ADMIN_PORT="${WEB_ADMIN_PORT:-8080}" WEB_ADMIN_PORT="${WEB_ADMIN_PORT:-8080}"
WEB_ADMIN_AUTH_DISABLED="${WEB_ADMIN_AUTH_DISABLED:-false}" WEB_ADMIN_AUTH_DISABLED="${WEB_ADMIN_AUTH_DISABLED:-false}"
VPN_ICE_ENABLED="${VPN_ICE_ENABLED:-n}" VPN_ICE_ENABLED="y"
CUSTOM_STUN_SERVER="${CUSTOM_STUN_SERVER:-}" CUSTOM_STUN_SERVER="${turn_server}"
TURN_ENABLED="y"
TURN_SERVER="${turn_server}"
TURN_USERNAME="${TURN_USERNAME}"
TURN_PASSWORD="${TURN_PASSWORD}"
EOF EOF
chmod 644 "$CONFIG_FILE" chmod 644 "$CONFIG_FILE"
fi
# Source config for use in this script # ── 7. Initialize categories & rooms if missing ──────────────
source "$CONFIG_FILE" 2>/dev/null || true
# ── 4. Initialize default categories if missing ──────────────
CATEGORIES_FILE="${CONFIG_DIR}/categories.conf" CATEGORIES_FILE="${CONFIG_DIR}/categories.conf"
if [[ ! -f "$CATEGORIES_FILE" ]]; then if [[ ! -f "$CATEGORIES_FILE" ]]; then
log_info "Creating default device categories..." log_info "Creating default device categories..."
@@ -85,43 +138,39 @@ custom|Custom|no|Custom configuration
EOF EOF
fi fi
# ── 5. Configure STUN/ICE ──────────────────────────────────── ROOMS_FILE="${CONFIG_DIR}/rooms.conf"
# Allow STUN_SERVER env to override config if [[ ! -f "$ROOMS_FILE" ]]; then
if [[ -n "${STUN_SERVER:-}" ]]; then cat > "$ROOMS_FILE" << 'EOF'
log_info "STUN server configured: ${STUN_SERVER}" # ext|name|members|timeout|type
# Update config file EOF
if ! grep -q "^VPN_ICE_ENABLED=" "$CONFIG_FILE" 2>/dev/null; then
echo "VPN_ICE_ENABLED=\"y\"" >> "$CONFIG_FILE"
echo "CUSTOM_STUN_SERVER=\"${STUN_SERVER}\"" >> "$CONFIG_FILE"
else
sed -i "s|^VPN_ICE_ENABLED=.*|VPN_ICE_ENABLED=\"y\"|" "$CONFIG_FILE"
sed -i "s|^CUSTOM_STUN_SERVER=.*|CUSTOM_STUN_SERVER=\"${STUN_SERVER}\"|" "$CONFIG_FILE"
fi
VPN_ICE_ENABLED="y"
CUSTOM_STUN_SERVER="${STUN_SERVER}"
fi fi
# ── 6. Generate Asterisk configs if missing ─────────────────── # ── 8. Generate Asterisk configs ─────────────────────────────
local_ip=$(hostname -I 2>/dev/null | awk '{print $1}')
raw_cidr=$(ip -o -f inet addr show 2>/dev/null | awk '/scope global/ {print $4}' | head -1)
default_cidr="$raw_cidr"
if [[ "$raw_cidr" =~ \.([0-9]+)/24$ ]]; then default_cidr="${raw_cidr%.*}.0/24"; fi
# Build NAT/local_net settings # Build local_net entries
nat_settings=""
all_local_nets="local_net=${LOCAL_CIDR:-$default_cidr}" all_local_nets="local_net=${LOCAL_CIDR:-$default_cidr}"
if [[ "${HAS_VLANS:-n}" == "y" && -n "${VLAN_SUBNETS:-}" ]]; then if [[ "${HAS_VLANS:-n}" == "y" && -n "${VLAN_SUBNETS:-}" ]]; then
for subnet in $VLAN_SUBNETS; do for subnet in $VLAN_SUBNETS; do
all_local_nets="${all_local_nets} all_local_nets="${all_local_nets}
local_net=${subnet}" local_net=${subnet}"
done done
fi
# NAT settings - always include external addresses for FQDN mode
nat_settings=""
if [[ -n "$PUBLIC_IP" ]]; then
nat_settings="external_media_address=${PUBLIC_IP}
external_signaling_address=${PUBLIC_IP}
${all_local_nets}"
else
nat_settings="${all_local_nets}" nat_settings="${all_local_nets}"
fi fi
# ── pjsip.conf (only if empty/missing - preserves existing devices) ──
if [[ ! -f /etc/asterisk/pjsip.conf ]] || [[ ! -s /etc/asterisk/pjsip.conf ]]; then if [[ ! -f /etc/asterisk/pjsip.conf ]] || [[ ! -s /etc/asterisk/pjsip.conf ]]; then
log_info "Generating PJSIP configuration..." log_info "Generating PJSIP configuration..."
cat > /etc/asterisk/pjsip.conf << EOF cat > /etc/asterisk/pjsip.conf << EOF
; Easy Asterisk (Docker) ; Easy Asterisk (Docker) - FQDN: ${DOMAIN_NAME:-none}
[global] [global]
type=global type=global
user_agent=EasyAsterisk user_agent=EasyAsterisk
@@ -130,21 +179,21 @@ user_agent=EasyAsterisk
type=transport type=transport
protocol=udp protocol=udp
bind=0.0.0.0:5060 bind=0.0.0.0:5060
; Server IP: ${local_ip} ; Server IP: ${local_ip} | Public IP: ${PUBLIC_IP:-unknown}
${nat_settings} ${nat_settings}
[transport-tcp] [transport-tcp]
type=transport type=transport
protocol=tcp protocol=tcp
bind=0.0.0.0:5060 bind=0.0.0.0:5060
; Server IP: ${local_ip} ; Server IP: ${local_ip} | Public IP: ${PUBLIC_IP:-unknown}
${nat_settings} ${nat_settings}
[transport-tls] [transport-tls]
type=transport type=transport
protocol=tls protocol=tls
bind=0.0.0.0:5061 bind=0.0.0.0:5061
; Server IP: ${local_ip} ; Server IP: ${local_ip} | Public IP: ${PUBLIC_IP:-unknown}
cert_file=/etc/asterisk/certs/server.crt cert_file=/etc/asterisk/certs/server.crt
priv_key_file=/etc/asterisk/certs/server.key priv_key_file=/etc/asterisk/certs/server.key
ca_list_file=/etc/ssl/certs/ca-certificates.crt ca_list_file=/etc/ssl/certs/ca-certificates.crt
@@ -153,8 +202,34 @@ ${nat_settings}
EOF EOF
chown asterisk:asterisk /etc/asterisk/pjsip.conf chown asterisk:asterisk /etc/asterisk/pjsip.conf
else
# Update NAT settings in existing pjsip.conf transports if public IP changed
if [[ -n "$PUBLIC_IP" ]]; then
current_ext=$(grep "^external_media_address=" /etc/asterisk/pjsip.conf 2>/dev/null | head -1 | cut -d= -f2)
if [[ "$current_ext" != "$PUBLIC_IP" && -n "$current_ext" ]]; then
log_info "Updating public IP in pjsip.conf: ${current_ext} -> ${PUBLIC_IP}"
sed -i "s|external_media_address=.*|external_media_address=${PUBLIC_IP}|g" /etc/asterisk/pjsip.conf
sed -i "s|external_signaling_address=.*|external_signaling_address=${PUBLIC_IP}|g" /etc/asterisk/pjsip.conf
fi
fi
fi fi
# ── rtp.conf (always regenerated - includes TURN credentials) ──
log_info "Configuring RTP with ICE + STUN + TURN..."
cat > /etc/asterisk/rtp.conf << EOF
[general]
rtpstart=${RTP_START:-10000}
rtpend=${RTP_END:-20000}
strictrtp=yes
icesupport=yes
stunaddr=${turn_server}
turnaddr=${turn_server}
turnusername=${TURN_USERNAME}
turnpassword=${TURN_PASSWORD}
EOF
chown asterisk:asterisk /etc/asterisk/rtp.conf
# ── extensions.conf (only if missing) ──
if [[ ! -f /etc/asterisk/extensions.conf ]] || [[ ! -s /etc/asterisk/extensions.conf ]]; then if [[ ! -f /etc/asterisk/extensions.conf ]] || [[ ! -s /etc/asterisk/extensions.conf ]]; then
log_info "Generating dialplan..." log_info "Generating dialplan..."
cat > /etc/asterisk/extensions.conf << 'EOF' cat > /etc/asterisk/extensions.conf << 'EOF'
@@ -168,26 +243,7 @@ EOF
chown asterisk:asterisk /etc/asterisk/extensions.conf chown asterisk:asterisk /etc/asterisk/extensions.conf
fi fi
# RTP config with STUN/ICE support # ── Other core configs (only if missing) ──
log_info "Configuring RTP..."
ice_stun_config="# icesupport disabled - LAN only mode"
if [[ "${VPN_ICE_ENABLED:-n}" == "y" ]] || [[ -n "${DOMAIN_NAME:-}" ]]; then
stun_addr="${CUSTOM_STUN_SERVER:-stun.l.google.com:19302}"
ice_stun_config="icesupport=yes
stunaddr=${stun_addr}"
log_info "ICE enabled, STUN: ${stun_addr}"
fi
cat > /etc/asterisk/rtp.conf << EOF
[general]
rtpstart=${RTP_START:-10000}
rtpend=${RTP_END:-20000}
strictrtp=yes
${ice_stun_config}
EOF
chown asterisk:asterisk /etc/asterisk/rtp.conf
# Generate other core configs if missing
if [[ ! -f /etc/asterisk/asterisk.conf ]]; then if [[ ! -f /etc/asterisk/asterisk.conf ]]; then
cat > /etc/asterisk/asterisk.conf << 'EOF' cat > /etc/asterisk/asterisk.conf << 'EOF'
[directories] [directories]
@@ -209,13 +265,29 @@ if [[ ! -f /etc/asterisk/modules.conf ]]; then
cat > /etc/asterisk/modules.conf << 'EOF' cat > /etc/asterisk/modules.conf << 'EOF'
[modules] [modules]
autoload=yes autoload=yes
noload => chan_sip.so
noload => chan_iax2.so
load => res_pjsip.so
load => res_pjsip_session.so
load => res_pjsip_logger.so
load => chan_pjsip.so
load => codec_ulaw.so
load => codec_alaw.so
load => codec_g722.so
load => codec_opus.so
load => res_rtp_asterisk.so
load => app_dial.so
load => app_page.so
load => pbx_config.so
EOF EOF
fi fi
# ── 7. Fix permissions ─────────────────────────────────────── # ── 9. Fix permissions ───────────────────────────────────────
chown -R asterisk:asterisk /etc/asterisk /var/lib/asterisk /var/log/asterisk /var/spool/asterisk /var/run/asterisk 2>/dev/null || true chown -R asterisk:asterisk /etc/asterisk /var/lib/asterisk /var/log/asterisk /var/spool/asterisk /var/run/asterisk 2>/dev/null || true
# ── 8. Start Web Admin in background ───────────────────────── # ── 10. Start Web Admin in background ─────────────────────────
# The web admin script is generated by the 'easy-asterisk' management tool.
# On first run: docker exec -it easy-asterisk easy-asterisk → Web Admin menu → Start
if [[ -f "$WEB_ADMIN_SCRIPT" ]]; then if [[ -f "$WEB_ADMIN_SCRIPT" ]]; then
log_info "Starting Web Admin on port ${WEB_ADMIN_PORT:-8080}..." log_info "Starting Web Admin on port ${WEB_ADMIN_PORT:-8080}..."
WEBADMIN_PORT="${WEB_ADMIN_PORT:-8080}" \ WEBADMIN_PORT="${WEB_ADMIN_PORT:-8080}" \
@@ -223,28 +295,33 @@ if [[ -f "$WEB_ADMIN_SCRIPT" ]]; then
python3 "$WEB_ADMIN_SCRIPT" & python3 "$WEB_ADMIN_SCRIPT" &
fi fi
# ── 9. Trap signals for clean shutdown ──────────────────────── # ── 11. Signal handling for clean shutdown ────────────────────
cleanup() { cleanup() {
log_info "Shutting down..." log_info "Shutting down..."
# Stop web admin
pkill -f "easy-asterisk-webadmin" 2>/dev/null || true pkill -f "easy-asterisk-webadmin" 2>/dev/null || true
# Graceful Asterisk shutdown
asterisk -rx "core stop now" 2>/dev/null || true asterisk -rx "core stop now" 2>/dev/null || true
exit 0 exit 0
} }
trap cleanup SIGTERM SIGINT trap cleanup SIGTERM SIGINT
# ── 10. Start Asterisk in foreground ────────────────────────── # ── 12. Start Asterisk ───────────────────────────────────────
log_info "Starting Asterisk PBX..." log_info "Starting Asterisk PBX..."
echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" echo ""
echo -e "${CYAN}══════════════════════════════════════════════════════════════${NC}"
echo -e "${CYAN} Easy Asterisk (Docker)${NC}" echo -e "${CYAN} Easy Asterisk (Docker)${NC}"
echo -e "${CYAN} Server IP: ${local_ip}${NC}" echo -e "${CYAN}══════════════════════════════════════════════════════════════${NC}"
[[ "${VPN_ICE_ENABLED:-n}" == "y" ]] && echo -e "${CYAN} STUN/ICE: ${CUSTOM_STUN_SERVER:-auto}${NC}" echo -e " FQDN: ${GREEN}${DOMAIN_NAME:-not set}${NC}"
echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" echo -e " Public IP: ${GREEN}${PUBLIC_IP:-unknown}${NC}"
echo -e "${CYAN} Management: docker exec -it easy-asterisk easy-asterisk${NC}" echo -e " TURN/STUN: ${GREEN}${turn_server}${NC}"
echo -e "${CYAN} Diagnostics: docker exec -it easy-asterisk vpn-diagnostics${NC}" echo -e " TLS: ${GREEN}Enabled (port 5061)${NC}"
echo -e "${CYAN} DNS Check: docker exec -it easy-asterisk dns-whitelist${NC}" echo -e " ICE: ${GREEN}Enabled${NC}"
echo -e "${CYAN} Web Admin: http://${local_ip}:${WEB_ADMIN_PORT:-8080}/clients${NC}" echo -e "${CYAN}──────────────────────────────────────────────────────────────${NC}"
echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" echo -e " SIP clients connect to: ${GREEN}${DOMAIN_NAME:-$local_ip}:5061${NC} (TLS)"
echo -e " Web Admin: ${GREEN}http://${local_ip}:${WEB_ADMIN_PORT:-8080}/clients${NC}"
echo -e "${CYAN}──────────────────────────────────────────────────────────────${NC}"
echo -e " Management: docker exec -it easy-asterisk easy-asterisk"
echo -e " Diagnostics: docker exec -it easy-asterisk vpn-diagnostics"
echo -e "${CYAN}══════════════════════════════════════════════════════════════${NC}"
echo ""
exec asterisk -f -U asterisk -G asterisk exec asterisk -f -U asterisk -G asterisk
+71 -17
View File
@@ -283,6 +283,10 @@ load_config() {
WEB_ADMIN_AUTH_DISABLED="${WEB_ADMIN_AUTH_DISABLED:-false}" WEB_ADMIN_AUTH_DISABLED="${WEB_ADMIN_AUTH_DISABLED:-false}"
VPN_ICE_ENABLED="${VPN_ICE_ENABLED:-n}" VPN_ICE_ENABLED="${VPN_ICE_ENABLED:-n}"
CUSTOM_STUN_SERVER="${CUSTOM_STUN_SERVER:-}" CUSTOM_STUN_SERVER="${CUSTOM_STUN_SERVER:-}"
TURN_ENABLED="${TURN_ENABLED:-n}"
TURN_SERVER="${TURN_SERVER:-}"
TURN_USERNAME="${TURN_USERNAME:-}"
TURN_PASSWORD="${TURN_PASSWORD:-}"
return 0 return 0
} }
@@ -322,6 +326,10 @@ WEB_ADMIN_PORT="$WEB_ADMIN_PORT"
WEB_ADMIN_AUTH_DISABLED="$WEB_ADMIN_AUTH_DISABLED" WEB_ADMIN_AUTH_DISABLED="$WEB_ADMIN_AUTH_DISABLED"
VPN_ICE_ENABLED="$VPN_ICE_ENABLED" VPN_ICE_ENABLED="$VPN_ICE_ENABLED"
CUSTOM_STUN_SERVER="$CUSTOM_STUN_SERVER" CUSTOM_STUN_SERVER="$CUSTOM_STUN_SERVER"
TURN_ENABLED="$TURN_ENABLED"
TURN_SERVER="$TURN_SERVER"
TURN_USERNAME="$TURN_USERNAME"
TURN_PASSWORD="$TURN_PASSWORD"
EOF EOF
chmod 644 "$CONFIG_FILE" chmod 644 "$CONFIG_FILE"
@@ -736,6 +744,38 @@ add_device_menu() {
local display_transport="UDP" local display_transport="UDP"
local display_encryption="None" local display_encryption="None"
# In Docker with FQDN: default to FQDN mode for all devices
if is_docker && [[ -n "$DOMAIN_NAME" ]]; then
echo ""
echo "═══════════════════════════════════════════════════════════════"
echo -e " HOW WILL THIS DEVICE CONNECT?"
echo "═══════════════════════════════════════════════════════════════"
echo ""
echo -e " 1) ${CYAN}FQDN (recommended)${NC} - Via ${DOMAIN_NAME} (TLS) - works from any network"
echo -e " 2) ${GREEN}LAN only${NC} - Same local network (UDP)"
echo ""
read -p " Select [1]: " conn_choice
conn_choice="${conn_choice:-1}"
if [[ "$conn_choice" == "2" ]]; then
transport_block="transport=transport-udp"
encryption_block="media_encryption=no"
display_server="$(hostname -I | awk '{print $1}')"
display_port="5060"
display_transport="UDP"
display_encryption="None"
ice_block="ice_support=yes"
else
conn_type="fqdn"
transport_block="transport=transport-tls"
encryption_block="media_encryption=sdes"
ice_block="ice_support=yes"
display_server="$DOMAIN_NAME"
display_port="5061"
display_transport="TLS"
display_encryption="SRTP (SDES)"
fi
else
echo "" echo ""
echo "═══════════════════════════════════════════════════════════════" echo "═══════════════════════════════════════════════════════════════"
echo -e " HOW WILL THIS DEVICE CONNECT?" echo -e " HOW WILL THIS DEVICE CONNECT?"
@@ -777,6 +817,7 @@ add_device_menu() {
display_transport="TLS" display_transport="TLS"
display_encryption="SRTP (SDES)" display_encryption="SRTP (SDES)"
fi fi
fi
backup_config "/etc/asterisk/pjsip.conf" backup_config "/etc/asterisk/pjsip.conf"
@@ -2989,30 +3030,31 @@ transport=config,pjsip.conf,criteria=type=transport
EOF EOF
fi fi
# ICE and STUN configuration # ICE / STUN / TURN configuration
# Enabled for: FQDN/internet mode OR VPN with ICE enabled # Enabled for: FQDN/internet mode OR VPN with ICE enabled
load_config load_config
local ice_stun_config="" local ice_config=""
if [[ -n "$DOMAIN_NAME" ]]; then if [[ -n "$DOMAIN_NAME" ]] || [[ "$VPN_ICE_ENABLED" == "y" ]] || [[ "$TURN_ENABLED" == "y" ]]; then
# FQDN mode: always enable ICE local stun_addr="${TURN_SERVER:-${CUSTOM_STUN_SERVER:-stun.l.google.com:19302}}"
local stun_addr="${CUSTOM_STUN_SERVER:-stun.l.google.com:19302}" ice_config="icesupport=yes
ice_stun_config="icesupport=yes
stunaddr=${stun_addr}"
elif [[ "$VPN_ICE_ENABLED" == "y" ]]; then
# VPN mode with ICE: use custom or self-hosted STUN
local stun_addr="${CUSTOM_STUN_SERVER:-stun.l.google.com:19302}"
ice_stun_config="icesupport=yes
stunaddr=${stun_addr}" stunaddr=${stun_addr}"
# Add TURN relay if configured (required for calls through strict NAT/VPN)
if [[ "$TURN_ENABLED" == "y" && -n "$TURN_SERVER" && -n "$TURN_USERNAME" && -n "$TURN_PASSWORD" ]]; then
ice_config="${ice_config}
turnaddr=${TURN_SERVER}
turnusername=${TURN_USERNAME}
turnpassword=${TURN_PASSWORD}"
fi
else else
ice_stun_config="# icesupport disabled - LAN only mode" ice_config="# icesupport disabled - LAN only mode"
fi fi
cat > /etc/asterisk/rtp.conf << EOF cat > /etc/asterisk/rtp.conf << EOF
[general] [general]
rtpstart=10000 rtpstart=${RTP_START:-10000}
rtpend=20000 rtpend=${RTP_END:-20000}
strictrtp=yes strictrtp=yes
${ice_stun_config} ${ice_config}
EOF EOF
cat > /etc/asterisk/logger.conf << EOF cat > /etc/asterisk/logger.conf << EOF
@@ -3935,7 +3977,11 @@ show_main_menu() {
echo -e " Web Admin: ${YELLOW}Stopped${NC}" echo -e " Web Admin: ${YELLOW}Stopped${NC}"
fi fi
[[ -n "$DOMAIN_NAME" ]] && echo -e " Domain: ${DOMAIN_NAME}" [[ -n "$DOMAIN_NAME" ]] && echo -e " Domain: ${DOMAIN_NAME}"
[[ "$VPN_ICE_ENABLED" == "y" ]] && echo -e " VPN ICE: ${GREEN}Enabled${NC} (${CUSTOM_STUN_SERVER:-auto})" if [[ "$TURN_ENABLED" == "y" ]]; then
echo -e " TURN: ${GREEN}Enabled${NC} (${TURN_SERVER:-auto})"
elif [[ "$VPN_ICE_ENABLED" == "y" ]]; then
echo -e " STUN/ICE: ${GREEN}Enabled${NC} (${CUSTOM_STUN_SERVER:-auto})"
fi
echo "" echo ""
declare -A menu_map declare -A menu_map
@@ -4618,11 +4664,19 @@ def add_device(name, category, extension, conn_type='lan', auto_answer=None):
# Determine transport and encryption # Determine transport and encryption
# Check if VPN ICE mode is enabled (for third-party VPNs) # Check if VPN ICE mode is enabled (for third-party VPNs)
vpn_ice = 'n' vpn_ice = 'n'
turn_enabled = 'n'
is_container = os.path.exists('/.dockerenv')
if os.path.exists(CONFIG_FILE): if os.path.exists(CONFIG_FILE):
with open(CONFIG_FILE, 'r') as cf: with open(CONFIG_FILE, 'r') as cf:
for cline in cf: for cline in cf:
if cline.startswith('VPN_ICE_ENABLED='): if cline.startswith('VPN_ICE_ENABLED='):
vpn_ice = cline.strip().split('=', 1)[1].strip('"') vpn_ice = cline.strip().split('=', 1)[1].strip('"')
elif cline.startswith('TURN_ENABLED='):
turn_enabled = cline.strip().split('=', 1)[1].strip('"')
# In Docker: always use FQDN mode for web-created devices
if is_container and conn_type == 'lan':
conn_type = 'fqdn'
if conn_type == 'fqdn': if conn_type == 'fqdn':
transport = 'transport=transport-tls' transport = 'transport=transport-tls'
@@ -4631,7 +4685,7 @@ def add_device(name, category, extension, conn_type='lan', auto_answer=None):
else: else:
transport = 'transport=transport-udp' transport = 'transport=transport-udp'
encryption = 'media_encryption=no' encryption = 'media_encryption=no'
ice = 'ice_support=yes' if vpn_ice == 'y' else '' ice = 'ice_support=yes' if (vpn_ice == 'y' or turn_enabled == 'y') else ''
aa_tag = '' aa_tag = ''
if auto_answer == 'yes': if auto_answer == 'yes':