diff --git a/.env.example b/.env.example index 944d583..c97e871 100644 --- a/.env.example +++ b/.env.example @@ -1,57 +1,92 @@ # ================================================================ # Easy Asterisk - Environment Configuration # -# Copy this file to .env and modify as needed: -# cp .env.example .env +# Setup: +# 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: -# docker compose up -d # Asterisk only -# docker compose --profile stun up -d # Asterisk + STUN server +# Port forwarding required on your router: +# 5061/tcp → SIP TLS signaling +# 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 ───────────────────────────────────────────── -# Leave DOMAIN_NAME empty for LAN/VPN mode (recommended for VPN setups) -# Set to your FQDN for internet calling mode (requires TLS certs) +# ── Domain Name (REQUIRED) ──────────────────────────────────── +# The FQDN that points to this server's public IP. +# This is what SIP clients use to connect. +# Example: asterisk.yourdomain.com 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 LOCAL_CIDR= -# ── VPN / VLAN Subnets ────────────────────────────────────── -# Set HAS_VLANS=y and list your VPN/VLAN subnets (space-separated) -# This tells Asterisk to treat these as local networks +# ── Additional Subnets (optional) ───────────────────────────── +# Only needed for site-to-site VPNs or VLANs where the server +# 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: -# WireGuard: 10.8.0.0/24 -# OpenVPN: 10.10.0.0/24 -# Tailscale: 100.64.0.0/10 -# Third-party: Check your VPN client for the assigned subnet +# WireGuard: VLAN_SUBNETS=10.8.0.0/24 +# Tailscale: VLAN_SUBNETS=100.64.0.0/10 +# Multiple: VLAN_SUBNETS=10.8.0.0/24 10.10.0.0/24 HAS_VLANS=n VLAN_SUBNETS= -# ── STUN Server ────────────────────────────────────────────── -# For self-hosted coturn (recommended for DNS-filtered networks): -# 1. Start with: docker compose --profile stun up -d -# 2. Set STUN_SERVER to your server's VPN IP:3478 -# Example: STUN_SERVER=10.8.0.1:3478 +# ── TURN/STUN Credentials ──────────────────────────────────── +# Used by coturn for TURN relay authentication. +# If TURN_PASSWORD is empty, a random password is generated on +# first startup and saved to /etc/easy-asterisk/config. # -# For Google STUN (requires DNS access to stun.l.google.com): -# STUN_SERVER=stun.l.google.com:19302 -# -# Leave empty if VPN provides direct routing (no NAT between endpoints) -STUN_SERVER= -VPN_ICE_ENABLED=n -CUSTOM_STUN_SERVER= +# These credentials are shared between coturn and Asterisk. +# SIP clients do NOT need these - only the server uses them. +TURN_USERNAME=easyasterisk +TURN_PASSWORD= -# ── 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) -# For constrained environments, reduce to e.g. 10000-10100 +# For constrained environments: 10000-10200 RTP_START=10000 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 -# Set to true if using a reverse proxy with its own auth WEB_ADMIN_AUTH_DISABLED=false diff --git a/docker-compose.yml b/docker-compose.yml index c49410c..52d8031 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,15 +2,13 @@ # Easy Asterisk - Docker Compose # # Usage: -# docker compose up -d # Asterisk only -# docker compose --profile stun up -d # Asterisk + self-hosted STUN -# docker exec -it easy-asterisk easy-asterisk # Interactive management +# docker compose up -d # Start everything +# docker exec -it easy-asterisk easy-asterisk # Interactive management # 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: -# Use --profile stun to run a self-hosted STUN server -# This eliminates all external DNS dependencies +# All clients connect via FQDN (TLS) regardless of their network. +# coturn provides STUN (NAT detection) + TURN (media relay) so calls +# work even behind strict firewalls, cellular NAT, or VPNs like Proton. # ================================================================ services: @@ -21,9 +19,12 @@ services: container_name: easy-asterisk # Host networking required for: # - 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 + # - Direct access to coturn on localhost network_mode: host + depends_on: + coturn: + condition: service_healthy volumes: - asterisk-config:/etc/asterisk - easy-asterisk-config:/etc/easy-asterisk @@ -31,28 +32,32 @@ services: - asterisk-spool:/var/spool/asterisk - asterisk-lib:/var/lib/asterisk environment: - # ── Network Mode ── - # Leave DOMAIN_NAME empty for LAN/VPN mode (recommended) - - DOMAIN_NAME=${DOMAIN_NAME:-} - - ENABLE_TLS=${ENABLE_TLS:-n} + # ── Domain (REQUIRED for remote access) ── + # Your FQDN that points to this server's public IP + - DOMAIN_NAME=${DOMAIN_NAME:?Set DOMAIN_NAME in .env} + - 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:-} - # ── VPN Configuration ── - # Add your VPN subnet(s) here, space-separated - # Example: 10.8.0.0/24 or 100.64.0.0/10 (Tailscale) + # ── Additional Subnets ── + # Space-separated CIDRs for VLANs, site-to-site VPNs, etc. + # NOT needed for client-side VPNs (Proton, NordVPN) - TURN handles those - HAS_VLANS=${HAS_VLANS:-n} - VLAN_SUBNETS=${VLAN_SUBNETS:-} - # ── STUN Server ── - # For self-hosted coturn: use your server's VPN/LAN IP + port 3478 - # Example: STUN_SERVER=10.8.0.1:3478 - # Leave empty to disable STUN (fine if VPN provides direct routing) - - STUN_SERVER=${STUN_SERVER:-} - - VPN_ICE_ENABLED=${VPN_ICE_ENABLED:-n} - - CUSTOM_STUN_SERVER=${CUSTOM_STUN_SERVER:-} + # ── TURN/STUN Server ── + # Points to the coturn service (auto-configured) + - TURN_ENABLED=y + - TURN_SERVER=${DOMAIN_NAME:?}:3478 + - TURN_USERNAME=${TURN_USERNAME:-easyasterisk} + - TURN_PASSWORD=${TURN_PASSWORD:-} # ── RTP Port Range ── - # Reduce range for constrained environments - RTP_START=${RTP_START:-10000} - RTP_END=${RTP_END:-20000} @@ -66,36 +71,35 @@ services: timeout: 5s retries: 3 - # ── Self-Hosted STUN Server (coturn) ─────────────────────── - # Activate with: docker compose --profile stun up -d + # ── TURN/STUN Relay Server (coturn) ────────────────────────── + # 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? - # - 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. + # Without TURN, calls work "sometimes" - with TURN, they always work. coturn: image: coturn/coturn:latest - container_name: easy-asterisk-stun + container_name: easy-asterisk-coturn network_mode: host - command: > - -n - --no-auth - --no-tls - --no-dtls - --stun-only - --listening-port=3478 - --fingerprint - --no-cli - --no-multicast-peers - --no-loopback-peers - --log-file=stdout + entrypoint: ["/bin/sh", "-c"] + command: + - | + # Build coturn arguments + ARGS="-n --listening-port=3478 --fingerprint --lt-cred-mech" + ARGS="$$ARGS --user=${TURN_USERNAME:-easyasterisk}:${TURN_PASSWORD:-changeme}" + ARGS="$$ARGS --realm=${DOMAIN_NAME:-localhost}" + ARGS="$$ARGS --min-port=${TURN_RELAY_MIN:-49152}" + ARGS="$$ARGS --max-port=${TURN_RELAY_MAX:-49252}" + # Only add external-ip if PUBLIC_IP is set + if [ -n "${PUBLIC_IP:-}" ]; then + 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 - profiles: - - stun healthcheck: test: ["CMD-SHELL", "ss -uln | grep -q ':3478'"] interval: 30s diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index 20fb8e8..ec73fa3 100644 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -2,8 +2,11 @@ # ================================================================ # Easy Asterisk Docker Entrypoint # -# Initializes configuration, starts Asterisk + Web Admin -# Uses the same config functions as the main script +# Fully automated: +# - Detects public IP +# - Generates TURN credentials if not provided +# - Configures Asterisk with FQDN, TLS, ICE, STUN, TURN +# - Starts web admin + Asterisk # ================================================================ set -e @@ -11,70 +14,120 @@ set -e GREEN='\033[0;32m' YELLOW='\033[1;33m' CYAN='\033[0;36m' +RED='\033[0;31m' NC='\033[0m' log_info() { echo -e "${GREEN}[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_FILE="${CONFIG_DIR}/config" 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 ─────────────────────────── if ! id asterisk >/dev/null 2>&1; then useradd -r -s /bin/false -d /var/lib/asterisk asterisk 2>/dev/null || true 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 log_info "Generating self-signed TLS certificate..." 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 \ -keyout /etc/asterisk/certs/server.key \ -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.* chmod 644 /etc/asterisk/certs/server.crt chmod 600 /etc/asterisk/certs/server.key fi -# ── 3. Create config from environment if first run ──────────── +# ── 6. Write config file ───────────────────────────────────── mkdir -p "$CONFIG_DIR" -if [[ ! -f "$CONFIG_FILE" ]]; then - log_info "Creating initial configuration from environment..." - cat > "$CONFIG_FILE" << EOF -# Easy Asterisk Configuration (Docker) +# Determine TURN/STUN server address +turn_server="${TURN_SERVER:-${DOMAIN_NAME:-$local_ip}:3478}" + +cat > "$CONFIG_FILE" << EOF +# Easy Asterisk Configuration (Docker) - $(date) KIOSK_USER="" KIOSK_UID="" KIOSK_EXTENSION="" KIOSK_NAME="" SIP_PASSWORD="" -ASTERISK_HOST="${ASTERISK_HOST:-}" +ASTERISK_HOST="${DOMAIN_NAME:-$local_ip}" DOMAIN_NAME="${DOMAIN_NAME:-}" -ENABLE_TLS="${ENABLE_TLS:-n}" +ENABLE_TLS="${ENABLE_TLS:-y}" HAS_VLANS="${HAS_VLANS:-n}" VLAN_SUBNETS="${VLAN_SUBNETS:-}" CERT_PATH="" KEY_PATH="" INSTALLED_SERVER="y" INSTALLED_CLIENT="n" -CURRENT_PUBLIC_IP="" +CURRENT_PUBLIC_IP="${PUBLIC_IP}" PTT_DEVICE="" PTT_KEYCODE="" -LOCAL_CIDR="${LOCAL_CIDR:-}" +LOCAL_CIDR="${LOCAL_CIDR:-$default_cidr}" WEB_ADMIN_PORT="${WEB_ADMIN_PORT:-8080}" WEB_ADMIN_AUTH_DISABLED="${WEB_ADMIN_AUTH_DISABLED:-false}" -VPN_ICE_ENABLED="${VPN_ICE_ENABLED:-n}" -CUSTOM_STUN_SERVER="${CUSTOM_STUN_SERVER:-}" +VPN_ICE_ENABLED="y" +CUSTOM_STUN_SERVER="${turn_server}" +TURN_ENABLED="y" +TURN_SERVER="${turn_server}" +TURN_USERNAME="${TURN_USERNAME}" +TURN_PASSWORD="${TURN_PASSWORD}" EOF - chmod 644 "$CONFIG_FILE" -fi +chmod 644 "$CONFIG_FILE" -# Source config for use in this script -source "$CONFIG_FILE" 2>/dev/null || true - -# ── 4. Initialize default categories if missing ────────────── +# ── 7. Initialize categories & rooms if missing ────────────── CATEGORIES_FILE="${CONFIG_DIR}/categories.conf" if [[ ! -f "$CATEGORIES_FILE" ]]; then log_info "Creating default device categories..." @@ -85,43 +138,39 @@ custom|Custom|no|Custom configuration EOF fi -# ── 5. Configure STUN/ICE ──────────────────────────────────── -# Allow STUN_SERVER env to override config -if [[ -n "${STUN_SERVER:-}" ]]; then - log_info "STUN server configured: ${STUN_SERVER}" - # Update config file - 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}" +ROOMS_FILE="${CONFIG_DIR}/rooms.conf" +if [[ ! -f "$ROOMS_FILE" ]]; then + cat > "$ROOMS_FILE" << 'EOF' +# ext|name|members|timeout|type +EOF fi -# ── 6. Generate Asterisk configs if missing ─────────────────── -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 +# ── 8. Generate Asterisk configs ───────────────────────────── -# Build NAT/local_net settings -nat_settings="" +# Build local_net entries all_local_nets="local_net=${LOCAL_CIDR:-$default_cidr}" if [[ "${HAS_VLANS:-n}" == "y" && -n "${VLAN_SUBNETS:-}" ]]; then for subnet in $VLAN_SUBNETS; do all_local_nets="${all_local_nets} local_net=${subnet}" 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}" fi +# ── pjsip.conf (only if empty/missing - preserves existing devices) ── if [[ ! -f /etc/asterisk/pjsip.conf ]] || [[ ! -s /etc/asterisk/pjsip.conf ]]; then log_info "Generating PJSIP configuration..." cat > /etc/asterisk/pjsip.conf << EOF -; Easy Asterisk (Docker) +; Easy Asterisk (Docker) - FQDN: ${DOMAIN_NAME:-none} [global] type=global user_agent=EasyAsterisk @@ -130,21 +179,21 @@ user_agent=EasyAsterisk type=transport protocol=udp bind=0.0.0.0:5060 -; Server IP: ${local_ip} +; Server IP: ${local_ip} | Public IP: ${PUBLIC_IP:-unknown} ${nat_settings} [transport-tcp] type=transport protocol=tcp bind=0.0.0.0:5060 -; Server IP: ${local_ip} +; Server IP: ${local_ip} | Public IP: ${PUBLIC_IP:-unknown} ${nat_settings} [transport-tls] type=transport protocol=tls 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 priv_key_file=/etc/asterisk/certs/server.key ca_list_file=/etc/ssl/certs/ca-certificates.crt @@ -153,8 +202,34 @@ ${nat_settings} EOF 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 +# ── 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 log_info "Generating dialplan..." cat > /etc/asterisk/extensions.conf << 'EOF' @@ -168,26 +243,7 @@ EOF chown asterisk:asterisk /etc/asterisk/extensions.conf fi -# RTP config with STUN/ICE support -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 +# ── Other core configs (only if missing) ── if [[ ! -f /etc/asterisk/asterisk.conf ]]; then cat > /etc/asterisk/asterisk.conf << 'EOF' [directories] @@ -208,14 +264,30 @@ fi if [[ ! -f /etc/asterisk/modules.conf ]]; then cat > /etc/asterisk/modules.conf << 'EOF' [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 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 -# ── 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 log_info "Starting Web Admin on 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" & fi -# ── 9. Trap signals for clean shutdown ──────────────────────── +# ── 11. Signal handling for clean shutdown ──────────────────── cleanup() { log_info "Shutting down..." - # Stop web admin pkill -f "easy-asterisk-webadmin" 2>/dev/null || true - # Graceful Asterisk shutdown asterisk -rx "core stop now" 2>/dev/null || true exit 0 } trap cleanup SIGTERM SIGINT -# ── 10. Start Asterisk in foreground ────────────────────────── +# ── 12. Start Asterisk ─────────────────────────────────────── log_info "Starting Asterisk PBX..." -echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" +echo "" +echo -e "${CYAN}══════════════════════════════════════════════════════════════${NC}" echo -e "${CYAN} Easy Asterisk (Docker)${NC}" -echo -e "${CYAN} Server IP: ${local_ip}${NC}" -[[ "${VPN_ICE_ENABLED:-n}" == "y" ]] && echo -e "${CYAN} STUN/ICE: ${CUSTOM_STUN_SERVER:-auto}${NC}" -echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" -echo -e "${CYAN} Management: docker exec -it easy-asterisk easy-asterisk${NC}" -echo -e "${CYAN} Diagnostics: docker exec -it easy-asterisk vpn-diagnostics${NC}" -echo -e "${CYAN} DNS Check: docker exec -it easy-asterisk dns-whitelist${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 " FQDN: ${GREEN}${DOMAIN_NAME:-not set}${NC}" +echo -e " Public IP: ${GREEN}${PUBLIC_IP:-unknown}${NC}" +echo -e " TURN/STUN: ${GREEN}${turn_server}${NC}" +echo -e " TLS: ${GREEN}Enabled (port 5061)${NC}" +echo -e " ICE: ${GREEN}Enabled${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 diff --git a/easy-asterisk-v0.10.0.sh b/easy-asterisk-v0.10.0.sh index 95eeb34..db983d5 100644 --- a/easy-asterisk-v0.10.0.sh +++ b/easy-asterisk-v0.10.0.sh @@ -283,6 +283,10 @@ load_config() { WEB_ADMIN_AUTH_DISABLED="${WEB_ADMIN_AUTH_DISABLED:-false}" VPN_ICE_ENABLED="${VPN_ICE_ENABLED:-n}" 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 } @@ -322,6 +326,10 @@ WEB_ADMIN_PORT="$WEB_ADMIN_PORT" WEB_ADMIN_AUTH_DISABLED="$WEB_ADMIN_AUTH_DISABLED" VPN_ICE_ENABLED="$VPN_ICE_ENABLED" CUSTOM_STUN_SERVER="$CUSTOM_STUN_SERVER" +TURN_ENABLED="$TURN_ENABLED" +TURN_SERVER="$TURN_SERVER" +TURN_USERNAME="$TURN_USERNAME" +TURN_PASSWORD="$TURN_PASSWORD" EOF chmod 644 "$CONFIG_FILE" @@ -736,46 +744,79 @@ add_device_menu() { local display_transport="UDP" local display_encryption="None" - echo "" - echo "═══════════════════════════════════════════════════════════════" - echo -e " HOW WILL THIS DEVICE CONNECT?" - echo "═══════════════════════════════════════════════════════════════" - echo "" - echo -e " 1) ${GREEN}LAN/VPN${NC} - Same network or VPN tunnel (UDP)" - if [[ "$ENABLE_TLS" == "y" && -n "$DOMAIN_NAME" ]]; then - echo -e " 2) ${CYAN}FQDN${NC} - Internet or cross-VLAN via ${DOMAIN_NAME} (TLS)" - else - echo -e " 2) ${YELLOW}FQDN${NC} - Not configured (run 'Setup Internet Access' first)" - fi - echo "" - read -p " Select [1]: " conn_choice - conn_choice="${conn_choice:-1}" + # 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" == "1" ]]; then - # LAN/VPN - UDP, no encryption (explicit transport prevents TLS fallback) - 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" - # Enable ICE for VPN devices if VPN ICE mode is active - if [[ "$VPN_ICE_ENABLED" == "y" ]]; then + 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 - elif [[ "$conn_choice" == "2" ]]; then - if [[ "$ENABLE_TLS" != "y" || -z "$DOMAIN_NAME" ]]; then - print_error "FQDN access not configured. Run 'Setup Internet Access' first." - return + else + echo "" + echo "═══════════════════════════════════════════════════════════════" + echo -e " HOW WILL THIS DEVICE CONNECT?" + echo "═══════════════════════════════════════════════════════════════" + echo "" + echo -e " 1) ${GREEN}LAN/VPN${NC} - Same network or VPN tunnel (UDP)" + if [[ "$ENABLE_TLS" == "y" && -n "$DOMAIN_NAME" ]]; then + echo -e " 2) ${CYAN}FQDN${NC} - Internet or cross-VLAN via ${DOMAIN_NAME} (TLS)" + else + echo -e " 2) ${YELLOW}FQDN${NC} - Not configured (run 'Setup Internet Access' first)" + fi + echo "" + read -p " Select [1]: " conn_choice + conn_choice="${conn_choice:-1}" + + if [[ "$conn_choice" == "1" ]]; then + # LAN/VPN - UDP, no encryption (explicit transport prevents TLS fallback) + 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" + # Enable ICE for VPN devices if VPN ICE mode is active + if [[ "$VPN_ICE_ENABLED" == "y" ]]; then + ice_block="ice_support=yes" + fi + elif [[ "$conn_choice" == "2" ]]; then + if [[ "$ENABLE_TLS" != "y" || -z "$DOMAIN_NAME" ]]; then + print_error "FQDN access not configured. Run 'Setup Internet Access' first." + return + fi + 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 - 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 backup_config "/etc/asterisk/pjsip.conf" @@ -2989,30 +3030,31 @@ transport=config,pjsip.conf,criteria=type=transport EOF fi - # ICE and STUN configuration + # ICE / STUN / TURN configuration # Enabled for: FQDN/internet mode OR VPN with ICE enabled load_config - local ice_stun_config="" - if [[ -n "$DOMAIN_NAME" ]]; then - # FQDN mode: always enable ICE - local stun_addr="${CUSTOM_STUN_SERVER:-stun.l.google.com:19302}" - 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 + local ice_config="" + if [[ -n "$DOMAIN_NAME" ]] || [[ "$VPN_ICE_ENABLED" == "y" ]] || [[ "$TURN_ENABLED" == "y" ]]; then + local stun_addr="${TURN_SERVER:-${CUSTOM_STUN_SERVER:-stun.l.google.com:19302}}" + ice_config="icesupport=yes 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 - ice_stun_config="# icesupport disabled - LAN only mode" + ice_config="# icesupport disabled - LAN only mode" fi cat > /etc/asterisk/rtp.conf << EOF [general] -rtpstart=10000 -rtpend=20000 +rtpstart=${RTP_START:-10000} +rtpend=${RTP_END:-20000} strictrtp=yes -${ice_stun_config} +${ice_config} EOF cat > /etc/asterisk/logger.conf << EOF @@ -3935,7 +3977,11 @@ show_main_menu() { echo -e " Web Admin: ${YELLOW}Stopped${NC}" fi [[ -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 "" declare -A menu_map @@ -4618,11 +4664,19 @@ def add_device(name, category, extension, conn_type='lan', auto_answer=None): # Determine transport and encryption # Check if VPN ICE mode is enabled (for third-party VPNs) vpn_ice = 'n' + turn_enabled = 'n' + is_container = os.path.exists('/.dockerenv') if os.path.exists(CONFIG_FILE): with open(CONFIG_FILE, 'r') as cf: for cline in cf: if cline.startswith('VPN_ICE_ENABLED='): 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': transport = 'transport=transport-tls' @@ -4631,7 +4685,7 @@ def add_device(name, category, extension, conn_type='lan', auto_answer=None): else: transport = 'transport=transport-udp' 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 = '' if auto_answer == 'yes':