diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..22cad6a --- /dev/null +++ b/.dockerignore @@ -0,0 +1,5 @@ +.git +.gitignore +.env +*.md +LICENSE diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..944d583 --- /dev/null +++ b/.env.example @@ -0,0 +1,57 @@ +# ================================================================ +# Easy Asterisk - Environment Configuration +# +# Copy this file to .env and modify as needed: +# cp .env.example .env +# +# Then start with: +# docker compose up -d # Asterisk only +# docker compose --profile stun up -d # Asterisk + STUN server +# ================================================================ + +# ── 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= +ENABLE_TLS=n + +# Primary network 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 +# +# 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 +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 +# +# 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= + +# ── RTP Port Range ─────────────────────────────────────────── +# Default: 10000-20000 (10,000 ports) +# For constrained environments, reduce to e.g. 10000-10100 +RTP_START=10000 +RTP_END=20000 + +# ── Web Admin ──────────────────────────────────────────────── +WEB_ADMIN_PORT=8080 +# Set to true if using a reverse proxy with its own auth +WEB_ADMIN_AUTH_DISABLED=false diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..a9567f7 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,86 @@ +# ================================================================ +# Easy Asterisk - Docker Container +# Asterisk PBX with web admin and optional STUN support +# +# 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 +# ================================================================ + +FROM ubuntu:24.04 + +ENV DEBIAN_FRONTEND=noninteractive +ENV LANG=C.UTF-8 + +# Install Asterisk and dependencies (matches install_asterisk_packages) +RUN echo "exit 101" > /usr/sbin/policy-rc.d && chmod +x /usr/sbin/policy-rc.d && \ + apt-get update && \ + apt-get install -y --no-install-recommends \ + asterisk \ + asterisk-core-sounds-en-gsm \ + asterisk-modules \ + openssl \ + curl \ + tcpdump \ + sngrep \ + python3 \ + iproute2 \ + net-tools \ + dnsutils \ + iputils-ping \ + procps \ + && rm -rf /var/lib/apt/lists/* \ + && rm -f /usr/sbin/policy-rc.d \ + && ldconfig \ + && update-ca-certificates 2>/dev/null || true + +# Create required directories +RUN mkdir -p \ + /etc/easy-asterisk \ + /etc/asterisk/certs \ + /var/lib/asterisk/static-http \ + /var/log/asterisk \ + /var/spool/asterisk \ + /var/run/asterisk \ + && chown -R asterisk:asterisk \ + /etc/asterisk \ + /var/lib/asterisk \ + /var/log/asterisk \ + /var/spool/asterisk \ + /var/run/asterisk + +# Copy the main management script +COPY easy-asterisk-v0.10.0.sh /usr/local/bin/easy-asterisk +RUN chmod +x /usr/local/bin/easy-asterisk + +# Copy diagnostic and utility scripts +COPY scripts/vpn-diagnostics.sh /usr/local/bin/vpn-diagnostics +COPY scripts/dns-whitelist.sh /usr/local/bin/dns-whitelist +RUN chmod +x /usr/local/bin/vpn-diagnostics /usr/local/bin/dns-whitelist + +# Copy entrypoint +COPY docker/entrypoint.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh + +# SIP signaling +EXPOSE 5060/udp +EXPOSE 5060/tcp +EXPOSE 5061/tcp + +# Web admin + provisioning +EXPOSE 8080/tcp +EXPOSE 8088/tcp +EXPOSE 8089/tcp + +# RTP media range (use --network host in production for full range) +# Docker port-mapping 10000 ports is impractical; host networking recommended +EXPOSE 10000-10100/udp + +# Persistent data +VOLUME ["/etc/asterisk", "/etc/easy-asterisk", "/var/log/asterisk"] + +HEALTHCHECK --interval=30s --timeout=5s --retries=3 \ + CMD asterisk -rx "core show version" >/dev/null 2>&1 || exit 1 + +ENTRYPOINT ["/entrypoint.sh"] diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..c49410c --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,110 @@ +# ================================================================ +# 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 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 +# ================================================================ + +services: + + # ── Asterisk PBX ─────────────────────────────────────────── + asterisk: + build: . + 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 + network_mode: host + volumes: + - asterisk-config:/etc/asterisk + - easy-asterisk-config:/etc/easy-asterisk + - asterisk-logs:/var/log/asterisk + - 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} + - 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) + - 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:-} + + # ── RTP Port Range ── + # Reduce range for constrained environments + - RTP_START=${RTP_START:-10000} + - RTP_END=${RTP_END:-20000} + + # ── Web Admin ── + - WEB_ADMIN_PORT=${WEB_ADMIN_PORT:-8080} + - WEB_ADMIN_AUTH_DISABLED=${WEB_ADMIN_AUTH_DISABLED:-false} + restart: unless-stopped + healthcheck: + test: ["CMD", "asterisk", "-rx", "core show version"] + interval: 30s + timeout: 5s + retries: 3 + + # ── Self-Hosted STUN Server (coturn) ─────────────────────── + # Activate with: docker compose --profile stun up -d + # + # 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. + coturn: + image: coturn/coturn:latest + container_name: easy-asterisk-stun + 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 + restart: unless-stopped + profiles: + - stun + healthcheck: + test: ["CMD-SHELL", "ss -uln | grep -q ':3478'"] + interval: 30s + timeout: 5s + retries: 3 + +volumes: + asterisk-config: + easy-asterisk-config: + asterisk-logs: + asterisk-spool: + asterisk-lib: diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh new file mode 100644 index 0000000..b5ce97a --- /dev/null +++ b/docker/entrypoint.sh @@ -0,0 +1,209 @@ +#!/bin/bash +# ================================================================ +# Easy Asterisk Docker Entrypoint +# +# Starts Asterisk + Web Admin, applies environment configuration +# ================================================================ + +set -e + +# Colors for output +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +CYAN='\033[0;36m' +NC='\033[0m' + +log_info() { echo -e "${GREEN}[entrypoint]${NC} $1"; } +log_warn() { echo -e "${YELLOW}[entrypoint]${NC} $1"; } + +# ── 1. 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 + 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 + chown asterisk:asterisk /etc/asterisk/certs/server.* + chmod 644 /etc/asterisk/certs/server.crt + chmod 600 /etc/asterisk/certs/server.key +fi + +# ── 2. Apply environment-based configuration ────────────────── +CONFIG_DIR="/etc/easy-asterisk" +CONFIG_FILE="${CONFIG_DIR}/config" +mkdir -p "$CONFIG_DIR" + +# If no config exists, create from environment variables +if [[ ! -f "$CONFIG_FILE" ]]; then + log_info "Creating initial configuration from environment..." + cat > "$CONFIG_FILE" << EOF +# Easy Asterisk Configuration (Docker) +KIOSK_USER="" +KIOSK_UID="" +KIOSK_EXTENSION="" +KIOSK_NAME="" +SIP_PASSWORD="" +ASTERISK_HOST="${ASTERISK_HOST:-}" +DOMAIN_NAME="${DOMAIN_NAME:-}" +ENABLE_TLS="${ENABLE_TLS:-n}" +HAS_VLANS="${HAS_VLANS:-n}" +VLAN_SUBNETS="${VLAN_SUBNETS:-}" +CERT_PATH="" +KEY_PATH="" +INSTALLED_SERVER="y" +INSTALLED_CLIENT="n" +CURRENT_PUBLIC_IP="" +PTT_DEVICE="" +PTT_KEYCODE="" +LOCAL_CIDR="${LOCAL_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:-}" +EOF + chmod 644 "$CONFIG_FILE" +fi + +# ── 3. Configure STUN if self-hosted coturn is available ────── +if [[ -n "$STUN_SERVER" ]]; then + log_info "Using STUN server: ${STUN_SERVER}" + source "$CONFIG_FILE" 2>/dev/null || true + + # Update rtp.conf with custom STUN + cat > /etc/asterisk/rtp.conf << EOF +[general] +rtpstart=${RTP_START:-10000} +rtpend=${RTP_END:-20000} +strictrtp=yes +icesupport=yes +stunaddr=${STUN_SERVER} +EOF + chown asterisk:asterisk /etc/asterisk/rtp.conf +fi + +# ── 4. Generate default Asterisk configs if missing ─────────── +if [[ ! -f /etc/asterisk/pjsip.conf ]] || [[ ! -s /etc/asterisk/pjsip.conf ]]; then + log_info "Generating default PJSIP configuration..." + local_ip=$(hostname -I | awk '{print $1}') + raw_cidr=$(ip -o -f inet addr show | 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 + + nat_settings="" + source "$CONFIG_FILE" 2>/dev/null || true + if [[ "$HAS_VLANS" == "y" && -n "$VLAN_SUBNETS" ]]; then + nat_settings="local_net=${default_cidr}" + for subnet in $VLAN_SUBNETS; do + nat_settings="${nat_settings} +local_net=${subnet}" + done + fi + + cat > /etc/asterisk/pjsip.conf << EOF +; Easy Asterisk (Docker) +[global] +type=global +user_agent=EasyAsterisk + +[transport-udp] +type=transport +protocol=udp +bind=0.0.0.0:5060 +; Server IP: ${local_ip} +${nat_settings} + +[transport-tcp] +type=transport +protocol=tcp +bind=0.0.0.0:5060 +; Server IP: ${local_ip} +${nat_settings} + +[transport-tls] +type=transport +protocol=tls +bind=0.0.0.0:5061 +; Server IP: ${local_ip} +cert_file=/etc/asterisk/certs/server.crt +priv_key_file=/etc/asterisk/certs/server.key +ca_list_file=/etc/ssl/certs/ca-certificates.crt +method=tlsv1_2 +${nat_settings} + +EOF + chown asterisk:asterisk /etc/asterisk/pjsip.conf +fi + +if [[ ! -f /etc/asterisk/extensions.conf ]] || [[ ! -s /etc/asterisk/extensions.conf ]]; then + log_info "Generating default dialplan..." + cat > /etc/asterisk/extensions.conf << EOF +[general] +static=yes +writeprotect=no +[default] +exten => _X.,1,Hangup() +[intercom] +EOF + chown asterisk:asterisk /etc/asterisk/extensions.conf +fi + +if [[ ! -f /etc/asterisk/rtp.conf ]]; then + log_info "Generating default RTP configuration..." + cat > /etc/asterisk/rtp.conf << EOF +[general] +rtpstart=${RTP_START:-10000} +rtpend=${RTP_END:-20000} +strictrtp=yes +# icesupport disabled - LAN only mode +EOF + chown asterisk:asterisk /etc/asterisk/rtp.conf +fi + +# Generate other required configs +for conf in asterisk.conf logger.conf modules.conf; do + if [[ ! -f "/etc/asterisk/$conf" ]]; then + log_info "Generating /etc/asterisk/$conf..." + fi +done + +if [[ ! -f /etc/asterisk/asterisk.conf ]]; then + cat > /etc/asterisk/asterisk.conf << EOF +[directories] +[options] +runuser = asterisk +rungroup = asterisk +EOF +fi + +if [[ ! -f /etc/asterisk/logger.conf ]]; then + cat > /etc/asterisk/logger.conf << EOF +[general] +[logfiles] +console => notice,warning,error +EOF +fi + +# ── 5. Fix permissions ─────────────────────────────────────── +chown -R asterisk:asterisk /etc/asterisk /var/lib/asterisk /var/log/asterisk /var/spool/asterisk /var/run/asterisk 2>/dev/null || true + +# ── 6. Start Web Admin in background (if script exists) ────── +WEB_ADMIN_SCRIPT="/usr/local/bin/easy-asterisk-webadmin" +if [[ -f "$WEB_ADMIN_SCRIPT" ]]; then + source "$CONFIG_FILE" 2>/dev/null || true + log_info "Starting Web Admin on port ${WEB_ADMIN_PORT:-8080}..." + WEBADMIN_PORT="${WEB_ADMIN_PORT:-8080}" \ + WEBADMIN_AUTH_DISABLED="${WEB_ADMIN_AUTH_DISABLED:-false}" \ + python3 "$WEB_ADMIN_SCRIPT" & +fi + +# ── 7. Start Asterisk in foreground ────────────────────────── +log_info "Starting Asterisk PBX..." +echo -e "${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" +echo -e "${CYAN} Easy Asterisk (Docker)${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}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" + +exec asterisk -f -U asterisk -G asterisk diff --git a/easy-asterisk-v0.10.0.sh b/easy-asterisk-v0.10.0.sh index 773a8ce..a5f2cd2 100644 --- a/easy-asterisk-v0.10.0.sh +++ b/easy-asterisk-v0.10.0.sh @@ -20,6 +20,13 @@ # - HTTP Basic authentication with SHA256 password hashing # - Access at http://server:8080/clients # - ADDED: VPN subnet auto-detection (Tailscale, WireGuard, OpenVPN) +# - ADDED: VPN STUN/ICE configuration for third-party VPNs +# - Self-hosted coturn STUN (no external DNS dependencies) +# - Custom STUN server support +# - Per-device ICE for LAN/VPN mode endpoints +# - ADDED: Docker container support (Dockerfile + docker-compose) +# - ADDED: VPN diagnostics tool (vpn-diagnostics) +# - ADDED: DNS whitelist checker for filtered networks (dns-whitelist) # - IMPROVED: Device deletion uses awk for reliable multi-section removal # - IMPROVED: Device renaming uses awk to handle all edge cases # @@ -167,6 +174,8 @@ load_config() { VLAN_SUBNETS="${VLAN_SUBNETS:-}" 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:-}" return 0 } @@ -204,6 +213,8 @@ PTT_KEYCODE="$PTT_KEYCODE" LOCAL_CIDR="$LOCAL_CIDR" 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" EOF chmod 644 "$CONFIG_FILE" @@ -635,6 +646,10 @@ add_device_menu() { 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." @@ -2837,12 +2852,20 @@ transport=config,pjsip.conf,criteria=type=transport EOF fi - # ICE and STUN only for FQDN/internet calling + # ICE and STUN 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.l.google.com:19302" +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}" else ice_stun_config="# icesupport disabled - LAN only mode" fi @@ -4363,6 +4386,14 @@ def add_device(name, category, extension, conn_type='lan', auto_answer=None): password = generate_password() # Determine transport and encryption + # Check if VPN ICE mode is enabled (for third-party VPNs) + vpn_ice = 'n' + 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('"') + if conn_type == 'fqdn': transport = 'transport=transport-tls' encryption = 'media_encryption=sdes' @@ -4370,7 +4401,7 @@ def add_device(name, category, extension, conn_type='lan', auto_answer=None): else: transport = 'transport=transport-udp' encryption = 'media_encryption=no' - ice = '' + ice = 'ice_support=yes' if vpn_ice == 'y' else '' aa_tag = '' if auto_answer == 'yes': @@ -5939,6 +5970,206 @@ web_admin_menu() { esac } +configure_vpn_stun_ice() { + load_config + clear + print_header "VPN STUN/ICE Configuration" + + echo " This configures ICE (Interactive Connectivity Establishment) and" + echo " STUN (Session Traversal Utilities for NAT) for third-party VPNs." + echo "" + echo " ─────────────────────────────────────────────────────────────" + echo " When do you need this?" + echo "" + echo " • Your VPN does NAT between endpoints (audio fails or is one-way)" + echo " • Caller and receiver are on different VPN segments" + echo " • Direct VPN routing doesn't work for UDP/RTP traffic" + echo "" + echo " When do you NOT need this?" + echo "" + echo " • VPN gives both sides IPs on the same subnet (direct routing)" + echo " • Audio works fine without STUN" + echo " ─────────────────────────────────────────────────────────────" + echo "" + + local current_stun="${CUSTOM_STUN_SERVER:-Not configured}" + local current_ice="${VPN_ICE_ENABLED:-n}" + echo -e " Current Status:" + echo -e " VPN ICE: $([[ "$current_ice" == "y" ]] && echo "${GREEN}Enabled${NC}" || echo "${YELLOW}Disabled${NC}")" + echo -e " STUN Server: ${CYAN}${current_stun}${NC}" + echo "" + + echo " 1) Enable VPN ICE + self-hosted STUN (recommended for DNS filtering)" + echo " 2) Enable VPN ICE + Google STUN (requires DNS access)" + echo " 3) Enable VPN ICE + custom STUN server" + echo " 4) Disable VPN ICE (standard LAN mode)" + echo " 5) Test current STUN server" + echo " 6) Run VPN diagnostics" + echo " 7) Check DNS whitelist" + echo " 0) Back" + echo "" + read -p " Select: " stun_choice + + case $stun_choice in + 1) + # Self-hosted STUN via coturn + local server_ip=$(hostname -I | awk '{print $1}') + echo "" + echo " Self-hosted STUN uses coturn on this server (port 3478)." + echo " No external DNS dependencies - everything by IP." + echo "" + + # Detect VPN IPs for suggestion + local vpn_ip="" + while IFS= read -r line; do + local iface=$(echo "$line" | awk '{print $2}' | tr -d ':') + local ip_addr=$(echo "$line" | awk '{print $4}' | cut -d'/' -f1) + if [[ "$iface" =~ ^(tun|tap|wg|tailscale|utun|ppp|nordlynx) ]]; then + vpn_ip="$ip_addr" + break + fi + done < <(ip -o -f inet addr show scope global 2>/dev/null) + + local suggested_ip="${vpn_ip:-$server_ip}" + read -p " STUN server IP [${suggested_ip}]: " stun_ip + stun_ip="${stun_ip:-$suggested_ip}" + + read -p " STUN port [3478]: " stun_port + stun_port="${stun_port:-3478}" + + VPN_ICE_ENABLED="y" + CUSTOM_STUN_SERVER="${stun_ip}:${stun_port}" + save_config + repair_core_configs + generate_pjsip_conf + asterisk -rx "core reload" >/dev/null 2>&1 || true + + print_success "VPN ICE enabled with self-hosted STUN: ${CUSTOM_STUN_SERVER}" + echo "" + echo " Make sure coturn is running on port ${stun_port}:" + echo " Docker: docker compose --profile stun up -d" + echo " Manual: apt install coturn && systemctl start coturn" + echo "" + echo " Configure Sipnetic STUN server: ${CUSTOM_STUN_SERVER}" + ;; + 2) + # Google STUN + echo "" + echo -e " ${YELLOW}Requires DNS access to: stun.l.google.com${NC}" + echo " Add this domain to your DNS whitelist on all networks" + echo " (server, caller, and receiver)." + echo "" + read -p " Continue? [y/N]: " confirm + if [[ "$confirm" =~ ^[Yy]$ ]]; then + VPN_ICE_ENABLED="y" + CUSTOM_STUN_SERVER="stun.l.google.com:19302" + save_config + repair_core_configs + generate_pjsip_conf + asterisk -rx "core reload" >/dev/null 2>&1 || true + print_success "VPN ICE enabled with Google STUN" + echo "" + echo " DNS whitelist required: stun.l.google.com (UDP 19302)" + fi + ;; + 3) + # Custom STUN + echo "" + read -p " STUN server address (host:port): " custom_stun + if [[ -n "$custom_stun" ]]; then + VPN_ICE_ENABLED="y" + CUSTOM_STUN_SERVER="$custom_stun" + save_config + repair_core_configs + generate_pjsip_conf + asterisk -rx "core reload" >/dev/null 2>&1 || true + print_success "VPN ICE enabled with custom STUN: ${custom_stun}" + else + print_error "No STUN server specified" + fi + ;; + 4) + # Disable + VPN_ICE_ENABLED="n" + CUSTOM_STUN_SERVER="" + save_config + repair_core_configs + generate_pjsip_conf + asterisk -rx "core reload" >/dev/null 2>&1 || true + print_success "VPN ICE disabled (standard LAN mode)" + ;; + 5) + # Test STUN + echo "" + if [[ -n "$CUSTOM_STUN_SERVER" ]]; then + local stun_host=$(echo "$CUSTOM_STUN_SERVER" | cut -d: -f1) + local stun_port=$(echo "$CUSTOM_STUN_SERVER" | cut -d: -f2) + stun_port="${stun_port:-3478}" + + echo " Testing STUN server: ${CUSTOM_STUN_SERVER}" + echo "" + + # DNS test + if [[ "$stun_host" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + print_success "STUN server is an IP address (no DNS needed)" + else + if nslookup "$stun_host" >/dev/null 2>&1; then + print_success "DNS resolves: ${stun_host}" + else + print_error "DNS BLOCKED: ${stun_host}" + echo " Add to DNS whitelist or use IP address instead" + fi + fi + + # Connectivity test + if ping -c 2 -W 3 "$stun_host" >/dev/null 2>&1; then + print_success "STUN host reachable: ${stun_host}" + else + print_warn "STUN host not pingable (may still work if ICMP blocked)" + fi + + # Port test via Asterisk + if command -v asterisk &>/dev/null; then + local rtp_check=$(asterisk -rx "rtp show settings" 2>/dev/null | grep -i "stun\|ice" || echo "") + if [[ -n "$rtp_check" ]]; then + echo "" + echo " Asterisk RTP settings:" + echo "$rtp_check" | while IFS= read -r line; do + echo " $line" + done + fi + fi + else + print_warn "No STUN server configured" + echo " Configure one using options 1-3 above" + fi + ;; + 6) + # VPN diagnostics + if command -v vpn-diagnostics &>/dev/null; then + vpn-diagnostics + elif [[ -f /usr/local/bin/vpn-diagnostics ]]; then + bash /usr/local/bin/vpn-diagnostics + else + print_error "vpn-diagnostics not found" + echo " Install: copy scripts/vpn-diagnostics.sh to /usr/local/bin/vpn-diagnostics" + fi + ;; + 7) + # DNS whitelist + if command -v dns-whitelist &>/dev/null; then + dns-whitelist --check + elif [[ -f /usr/local/bin/dns-whitelist ]]; then + bash /usr/local/bin/dns-whitelist --check + else + print_error "dns-whitelist not found" + echo " Install: copy scripts/dns-whitelist.sh to /usr/local/bin/dns-whitelist" + fi + ;; + 0) return ;; + esac +} + submenu_server() { clear print_header "Server Settings" @@ -5953,6 +6184,7 @@ submenu_server() { echo " 9) Configure VLAN/VPN Subnets" echo " 10) Provisioning Manager" echo " 11) Web Admin (Client Management)" + echo " 12) VPN STUN/ICE Configuration" echo " 0) Back" read -p " Select: " choice case $choice in @@ -5967,6 +6199,7 @@ submenu_server() { 9) configure_vlan_subnets ;; 10) provisioning_manager_menu ;; 11) web_admin_menu ;; + 12) configure_vpn_stun_ice ;; 0) return ;; esac [[ "$choice" != "0" ]] && read -p "Press Enter..." diff --git a/scripts/dns-whitelist.sh b/scripts/dns-whitelist.sh new file mode 100644 index 0000000..61da578 --- /dev/null +++ b/scripts/dns-whitelist.sh @@ -0,0 +1,280 @@ +#!/bin/bash +# ================================================================ +# DNS Whitelist Checker for Easy Asterisk +# +# Checks which domains need to be whitelisted when DNS filtering +# is active on the server, caller, or receiver networks. +# +# Usage: dns-whitelist [--check] [--sipnetic] [--linphone] +# ================================================================ + +set -e + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +CYAN='\033[0;36m' +BOLD='\033[1m' +NC='\033[0m' + +CONFIG_FILE="/etc/easy-asterisk/config" +CHECK_MODE=false +SHOW_SIPNETIC=false +SHOW_LINPHONE=false +SHOW_ALL=true + +while [[ $# -gt 0 ]]; do + case "$1" in + --check) CHECK_MODE=true; shift ;; + --sipnetic) SHOW_SIPNETIC=true; SHOW_ALL=false; shift ;; + --linphone) SHOW_LINPHONE=true; SHOW_ALL=false; shift ;; + --help|-h) + echo "Usage: dns-whitelist [OPTIONS]" + echo "" + echo "Options:" + echo " --check Test reachability of each domain" + echo " --sipnetic Show Sipnetic-specific domains" + echo " --linphone Show Linphone-specific domains" + echo " --help Show this help" + exit 0 + ;; + *) shift ;; + esac +done + +print_header() { + echo "" + echo -e "${CYAN}╔══════════════════════════════════════════════════════════╗${NC}" + echo -e "${CYAN} $1${NC}" + echo -e "${CYAN}╚══════════════════════════════════════════════════════════╝${NC}" + echo "" +} + +check_dns() { + local domain="$1" + local port="$2" + local proto="${3:-tcp}" + + if $CHECK_MODE; then + # DNS resolution test + if nslookup "$domain" >/dev/null 2>&1; then + echo -e " ${GREEN}✓ DNS resolves${NC}" + else + echo -e " ${RED}✗ DNS BLOCKED - add to whitelist${NC}" + return 1 + fi + + # Connectivity test + if [[ "$proto" == "udp" ]]; then + # UDP - just check DNS resolution (can't reliably test UDP connectivity) + echo -e " ${CYAN}→ UDP port ${port} (cannot test remotely)${NC}" + else + if curl -s --connect-timeout 5 "https://${domain}" >/dev/null 2>&1 || \ + curl -s --connect-timeout 5 "http://${domain}" >/dev/null 2>&1; then + echo -e " ${GREEN}✓ Reachable${NC}" + else + echo -e " ${YELLOW}! Connection failed (may be expected)${NC}" + fi + fi + fi +} + +# Load config if available +source "$CONFIG_FILE" 2>/dev/null || true + +print_header "DNS Whitelist for Easy Asterisk" + +echo -e "${BOLD}Your Setup:${NC}" +if [[ -n "$DOMAIN_NAME" ]]; then + echo -e " Mode: FQDN/Internet (${DOMAIN_NAME})" +else + echo -e " Mode: LAN/VPN (no domain configured)" +fi +echo "" + +# ══════════════════════════════════════════════════════════════ +# SECTION 1: ASTERISK SERVER DOMAINS +# ══════════════════════════════════════════════════════════════ +if $SHOW_ALL; then + echo -e "${BOLD}━━━ 1. ASTERISK SERVER (whitelist on server's DNS filter) ━━━${NC}" + echo "" + + echo -e "${BOLD}Required for LAN/VPN mode:${NC}" + echo -e " ${GREEN}None${NC} - Asterisk needs no internet after installation" + echo -e " SIP operates over direct IP connections, no DNS involved" + echo "" + + echo -e "${BOLD}Required for FQDN/Internet mode only:${NC}" + echo "" + + echo -e " ${CYAN}ifconfig.me${NC} (HTTPS 443)" + echo -e " Purpose: Auto-detect public IP for NAT settings" + echo -e " When: Only during config regeneration" + check_dns "ifconfig.me" "443" + echo "" + + echo -e " ${CYAN}icanhazip.com${NC} (HTTPS 443)" + echo -e " Purpose: Fallback public IP detection" + check_dns "icanhazip.com" "443" + echo "" + + echo -e "${BOLD}Required if ICE/STUN enabled:${NC}" + echo "" + + # Check what STUN server is configured + stun_server="" + if [[ -f /etc/asterisk/rtp.conf ]]; then + stun_server=$(grep "^stunaddr=" /etc/asterisk/rtp.conf 2>/dev/null | cut -d= -f2) + fi + + if [[ -n "$stun_server" ]]; then + stun_host=$(echo "$stun_server" | cut -d: -f1) + stun_port=$(echo "$stun_server" | cut -d: -f2) + stun_port="${stun_port:-3478}" + echo -e " ${CYAN}${stun_host}${NC} (UDP ${stun_port})" + echo -e " Purpose: STUN NAT discovery" + echo -e " ${YELLOW}Tip: Use self-hosted coturn to avoid this dependency${NC}" + check_dns "$stun_host" "$stun_port" "udp" + else + echo -e " ${GREEN}No external STUN server configured${NC}" + echo -e " To use self-hosted: docker compose --profile stun up -d" + fi + echo "" + + echo -e "${BOLD}Required for package updates only:${NC}" + echo "" + echo -e " ${CYAN}archive.ubuntu.com${NC} / ${CYAN}security.ubuntu.com${NC} (HTTPS 443)" + echo -e " Purpose: apt package updates" + echo -e " When: Only during install/update (not runtime)" + echo "" + + echo -e "${BOLD}Required for TLS certificates:${NC}" + echo "" + echo -e " ${CYAN}acme-v02.api.letsencrypt.org${NC} (HTTPS 443)" + echo -e " Purpose: Let's Encrypt certificate issuance" + echo -e " When: Only if using Let's Encrypt / Certbot / Caddy" + if $CHECK_MODE; then + check_dns "acme-v02.api.letsencrypt.org" "443" + fi + echo "" +fi + +# ══════════════════════════════════════════════════════════════ +# SECTION 2: SIPNETIC (Mobile Client) DOMAINS +# ══════════════════════════════════════════════════════════════ +if $SHOW_ALL || $SHOW_SIPNETIC; then + echo -e "${BOLD}━━━ 2. SIPNETIC CLIENT (whitelist on caller/receiver DNS) ━━━${NC}" + echo "" + + echo -e "${BOLD}Required for SIP calls:${NC}" + echo -e " ${GREEN}None${NC} - Configure Sipnetic with the server's IP address directly" + echo -e " SIP registration and calls use IP:port, not DNS" + echo "" + + echo -e "${BOLD}Sipnetic app domains (for app functionality):${NC}" + echo "" + echo -e " ${CYAN}onesip.io${NC} / ${CYAN}api.onesip.io${NC}" + echo -e " Purpose: Sipnetic account/licensing (free tier works offline)" + echo -e " Required: Only for initial setup or account sync" + if $CHECK_MODE; then + check_dns "onesip.io" "443" + fi + echo "" + + echo -e " ${CYAN}play.google.com${NC} / ${CYAN}apps.apple.com${NC}" + echo -e " Purpose: App updates" + echo -e " Required: Only for installing/updating the app" + echo "" + + echo -e "${BOLD}If STUN configured in Sipnetic:${NC}" + echo "" + echo -e " The STUN server domain configured in Sipnetic's settings" + echo -e " needs to resolve on the mobile device's network." + echo "" + echo -e " ${YELLOW}Recommendation: Use the Asterisk server's VPN IP as STUN${NC}" + echo -e " ${YELLOW}server (if running self-hosted coturn), avoiding DNS entirely.${NC}" + echo "" + + echo -e "${BOLD}Sipnetic Configuration for DNS-Filtered Networks:${NC}" + echo "" + echo -e " Server: ${CYAN}${NC} (not a hostname)" + echo -e " Port: ${CYAN}5060${NC} (UDP, LAN/VPN mode)" + echo -e " Transport: ${CYAN}UDP${NC}" + echo -e " STUN: ${CYAN}:3478${NC} (if self-hosted coturn)" + echo -e " or leave blank if VPN provides direct routing" + echo "" +fi + +# ══════════════════════════════════════════════════════════════ +# SECTION 3: LINPHONE (Mobile Client) DOMAINS +# ══════════════════════════════════════════════════════════════ +if $SHOW_ALL || $SHOW_LINPHONE; then + echo -e "${BOLD}━━━ 3. LINPHONE CLIENT (whitelist on caller/receiver DNS) ━━━${NC}" + echo "" + + echo -e "${BOLD}Required for SIP calls:${NC}" + echo -e " ${GREEN}None${NC} - Same as Sipnetic, configure with server IP directly" + echo "" + + echo -e "${BOLD}Linphone app domains:${NC}" + echo "" + echo -e " ${CYAN}linphone.org${NC} / ${CYAN}sip.linphone.org${NC}" + echo -e " Purpose: Default Linphone SIP proxy (NOT needed for Easy Asterisk)" + echo -e " Required: ${GREEN}No${NC} - We use our own Asterisk server" + echo "" + echo -e " ${CYAN}subscribe.linphone.org${NC}" + echo -e " Purpose: Push notifications (may be needed for background calls)" + echo -e " Required: Only if you need calls to ring when app is backgrounded" + echo "" + + echo -e "${BOLD}For remote provisioning:${NC}" + echo "" + echo -e " If using Easy Asterisk's HTTP provisioning:" + echo -e " The phone must reach ${CYAN}http://:8088/static/linphone.xml${NC}" + echo -e " This is an IP address, so no DNS whitelist needed." + echo "" +fi + +# ══════════════════════════════════════════════════════════════ +# SECTION 4: SUMMARY +# ══════════════════════════════════════════════════════════════ +if $SHOW_ALL; then + print_header "Quick Reference - Minimum DNS Whitelist" + + echo -e "${BOLD}For LAN/VPN mode (no internet calling):${NC}" + echo "" + echo -e " Server DNS filter: ${GREEN}No domains needed${NC}" + echo -e " Client DNS filter: ${GREEN}No domains needed${NC}" + echo -e " (Configure everything by IP address)" + echo "" + + echo -e "${BOLD}For LAN/VPN + self-hosted STUN (coturn):${NC}" + echo "" + echo -e " Server DNS filter: ${GREEN}No domains needed${NC}" + echo -e " Client DNS filter: ${GREEN}No domains needed${NC}" + echo -e " (STUN server reached by VPN IP, not hostname)" + echo "" + + echo -e "${BOLD}For LAN/VPN + Google STUN:${NC}" + echo "" + echo -e " Server DNS filter: ${YELLOW}stun.l.google.com${NC}" + echo -e " Client DNS filter: ${YELLOW}stun.l.google.com${NC} (if also set in Sipnetic)" + echo "" + + echo -e "${BOLD}For FQDN/Internet mode:${NC}" + echo "" + echo -e " Server DNS filter: ${YELLOW}ifconfig.me, icanhazip.com, stun.l.google.com${NC}" + echo -e " ${YELLOW}acme-v02.api.letsencrypt.org${NC} (if using LE certs)" + echo -e " Client DNS filter: ${YELLOW}Your domain name (${DOMAIN_NAME:-yourdomain.com})${NC}" + echo "" + + print_header "Recommendation for DNS-Filtered Environments" + + echo -e " ${GREEN}Use LAN/VPN mode + self-hosted coturn (STUN-only)${NC}" + echo -e " ${GREEN}= Zero external DNS dependencies${NC}" + echo "" + echo -e " Setup: docker compose --profile stun up -d" + echo -e " Then configure STUN as your server's VPN IP:3478" + echo -e " No hostnames, no DNS, everything by IP." + echo "" +fi diff --git a/scripts/vpn-diagnostics.sh b/scripts/vpn-diagnostics.sh new file mode 100644 index 0000000..b928f82 --- /dev/null +++ b/scripts/vpn-diagnostics.sh @@ -0,0 +1,306 @@ +#!/bin/bash +# ================================================================ +# VPN Diagnostics for Easy Asterisk +# +# Tests whether your third-party VPN setup needs STUN/TURN +# and validates connectivity between Asterisk and VPN clients. +# +# Usage: vpn-diagnostics [--auto] [--client-ip ] +# ================================================================ + +set -e + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +CYAN='\033[0;36m' +BOLD='\033[1m' +NC='\033[0m' + +CONFIG_FILE="/etc/easy-asterisk/config" +RESULTS=() +WARNINGS=() +CLIENT_IP="" +AUTO_MODE=false + +# Parse arguments +while [[ $# -gt 0 ]]; do + case "$1" in + --auto) AUTO_MODE=true; shift ;; + --client-ip) CLIENT_IP="$2"; shift 2 ;; + --help|-h) + echo "Usage: vpn-diagnostics [OPTIONS]" + echo "" + echo "Options:" + echo " --auto Non-interactive mode" + echo " --client-ip Test connectivity to specific VPN client" + echo " --help Show this help" + exit 0 + ;; + *) shift ;; + esac +done + +print_header() { + echo "" + echo -e "${CYAN}╔══════════════════════════════════════════════════════════╗${NC}" + echo -e "${CYAN} $1${NC}" + echo -e "${CYAN}╚══════════════════════════════════════════════════════════╝${NC}" + echo "" +} + +pass() { echo -e " ${GREEN}✓${NC} $1"; RESULTS+=("PASS: $1"); } +fail() { echo -e " ${RED}✗${NC} $1"; RESULTS+=("FAIL: $1"); } +warn() { echo -e " ${YELLOW}!${NC} $1"; WARNINGS+=("$1"); } +info() { echo -e " ${CYAN}→${NC} $1"; } + +# ── Test 1: Detect network interfaces ──────────────────────── +print_header "VPN Diagnostics for Easy Asterisk" + +echo -e "${BOLD}1. Network Interface Detection${NC}" +echo "" + +# Detect primary LAN interface +primary_ip=$(hostname -I | awk '{print $1}') +info "Primary IP: ${primary_ip}" + +# Detect VPN interfaces (tun, tap, wg, tailscale, utun, ppp) +vpn_found=false +vpn_ips=() +vpn_ifaces=() + +while IFS= read -r line; do + iface=$(echo "$line" | awk '{print $2}' | tr -d ':') + ip_addr=$(echo "$line" | awk '{print $4}' | cut -d'/' -f1) + + # Check for VPN interface patterns + if [[ "$iface" =~ ^(tun|tap|wg|tailscale|utun|ppp|nordlynx|proton|mullvad) ]] || \ + [[ "$ip_addr" =~ ^(10\.|172\.(1[6-9]|2[0-9]|3[01])\.|100\.64\.|100\.96\.|100\.100\.) ]]; then + vpn_found=true + vpn_ips+=("$ip_addr") + vpn_ifaces+=("$iface") + pass "VPN interface detected: ${iface} (${ip_addr})" + fi +done < <(ip -o -f inet addr show scope global 2>/dev/null) + +if ! $vpn_found; then + warn "No VPN interface detected on server" + info "If your VPN runs on the router (not this server), that's expected" + info "The VPN subnet should be added via VLAN/VPN subnet configuration" +fi + +# ── Test 2: Check Asterisk PJSIP transport configuration ───── +echo "" +echo -e "${BOLD}2. Asterisk Transport Configuration${NC}" +echo "" + +if [[ -f /etc/asterisk/pjsip.conf ]]; then + # Check local_net entries + local_nets=$(grep "^local_net=" /etc/asterisk/pjsip.conf 2>/dev/null | sort -u) + if [[ -n "$local_nets" ]]; then + while IFS= read -r net; do + info "Transport local_net: ${net#local_net=}" + done <<< "$local_nets" + + # Check if VPN subnets are included + for vpn_ip in "${vpn_ips[@]}"; do + vpn_subnet=$(echo "$vpn_ip" | sed 's/\.[0-9]*$/.0\/24/') + if echo "$local_nets" | grep -q "$vpn_subnet"; then + pass "VPN subnet ${vpn_subnet} included in transport" + else + fail "VPN subnet ${vpn_subnet} NOT in transport local_net" + warn "Add via: Server Settings → Configure VLAN/VPN Subnets" + fi + done + else + warn "No local_net entries found in transport (basic LAN mode)" + fi + + # Check transport types + if grep -q "transport=transport-udp" /etc/asterisk/pjsip.conf; then + pass "UDP transport configured for LAN/VPN devices" + fi + if grep -q "transport=transport-tls" /etc/asterisk/pjsip.conf; then + pass "TLS transport configured for FQDN devices" + fi +else + fail "pjsip.conf not found" +fi + +# ── Test 3: Check RTP and ICE/STUN configuration ───────────── +echo "" +echo -e "${BOLD}3. RTP / ICE / STUN Configuration${NC}" +echo "" + +if [[ -f /etc/asterisk/rtp.conf ]]; then + rtp_start=$(grep "^rtpstart=" /etc/asterisk/rtp.conf | cut -d= -f2) + rtp_end=$(grep "^rtpend=" /etc/asterisk/rtp.conf | cut -d= -f2) + info "RTP port range: ${rtp_start:-10000}-${rtp_end:-20000}" + + if grep -q "^icesupport=yes" /etc/asterisk/rtp.conf; then + pass "ICE support enabled" + stun_addr=$(grep "^stunaddr=" /etc/asterisk/rtp.conf | cut -d= -f2) + if [[ -n "$stun_addr" ]]; then + info "STUN server: ${stun_addr}" + + # Test STUN server reachability + stun_host=$(echo "$stun_addr" | cut -d: -f1) + stun_port=$(echo "$stun_addr" | cut -d: -f2) + stun_port="${stun_port:-3478}" + + if command -v nslookup &>/dev/null && nslookup "$stun_host" >/dev/null 2>&1; then + pass "STUN server DNS resolves: ${stun_host}" + else + fail "Cannot resolve STUN server: ${stun_host}" + warn "Add ${stun_host} to DNS whitelist" + fi + fi + else + info "ICE support disabled (standard for LAN/VPN mode)" + warn "If audio fails over VPN, enable ICE via: Server Settings → VPN STUN/ICE" + fi +else + warn "rtp.conf not found" +fi + +# ── Test 4: Check endpoint ICE settings ─────────────────────── +echo "" +echo -e "${BOLD}4. Per-Device ICE Configuration${NC}" +echo "" + +if [[ -f /etc/asterisk/pjsip.conf ]]; then + device_count=$(grep -c "^; === Device:" /etc/asterisk/pjsip.conf 2>/dev/null || echo 0) + ice_device_count=$(grep -c "^ice_support=yes" /etc/asterisk/pjsip.conf 2>/dev/null || echo 0) + info "Total devices: ${device_count}" + info "Devices with ICE: ${ice_device_count}" + + if [[ "$device_count" -gt 0 && "$ice_device_count" -eq 0 ]]; then + warn "No devices have ICE enabled" + info "For third-party VPNs with NAT, enable ICE via VPN STUN/ICE menu" + fi +fi + +# ── Test 5: VPN client connectivity ────────────────────────── +echo "" +echo -e "${BOLD}5. VPN Client Connectivity${NC}" +echo "" + +if [[ -z "$CLIENT_IP" ]] && ! $AUTO_MODE; then + echo " Enter a VPN client IP to test connectivity (or press Enter to skip):" + read -p " Client VPN IP: " CLIENT_IP +fi + +if [[ -n "$CLIENT_IP" ]]; then + # Ping test + if ping -c 2 -W 3 "$CLIENT_IP" >/dev/null 2>&1; then + pass "Ping to ${CLIENT_IP} succeeded" + else + fail "Ping to ${CLIENT_IP} failed" + warn "VPN routing issue - client may not be reachable" + fi + + # SIP port test (UDP 5060) + if command -v nc &>/dev/null; then + if nc -z -u -w 3 "$CLIENT_IP" 5060 2>/dev/null; then + pass "UDP 5060 reachable on ${CLIENT_IP}" + else + info "UDP 5060 probe inconclusive (normal for filtered VPNs)" + fi + fi +else + info "Skipping client connectivity test (no IP provided)" +fi + +# ── Test 6: NAT type detection ─────────────────────────────── +echo "" +echo -e "${BOLD}6. NAT Type Analysis${NC}" +echo "" + +# Check if server is behind NAT +if [[ -n "$primary_ip" ]]; then + public_ip=$(curl -s -4 --connect-timeout 5 ifconfig.me 2>/dev/null || echo "") + if [[ -n "$public_ip" ]]; then + if [[ "$primary_ip" == "$public_ip" ]]; then + pass "Server has public IP (no NAT)" + else + info "Server behind NAT: ${primary_ip} → ${public_ip}" + info "This is normal for VPN setups where traffic stays on VPN" + fi + else + info "Cannot detect public IP (DNS filtering or no internet)" + info "Not needed for LAN/VPN mode" + fi +fi + +# ── Test 7: Asterisk registration status ───────────────────── +echo "" +echo -e "${BOLD}7. Asterisk Registration Status${NC}" +echo "" + +if command -v asterisk &>/dev/null; then + reg_output=$(asterisk -rx "pjsip show endpoints" 2>/dev/null || echo "") + if [[ -n "$reg_output" ]]; then + online_count=$(echo "$reg_output" | grep -c "Avail" 2>/dev/null || echo 0) + offline_count=$(echo "$reg_output" | grep -c "Unavail" 2>/dev/null || echo 0) + info "Endpoints online: ${online_count}" + info "Endpoints offline: ${offline_count}" + + if [[ "$offline_count" -gt 0 ]]; then + warn "Some endpoints are offline - check VPN connectivity" + echo "$reg_output" | grep "Unavail" | while IFS= read -r line; do + info " Offline: $line" + done + fi + else + info "Asterisk not running or no endpoints configured" + fi +else + info "Asterisk CLI not available" +fi + +# ── Summary ────────────────────────────────────────────────── +print_header "Diagnostic Summary" + +fail_count=0 +pass_count=0 +for result in "${RESULTS[@]}"; do + if [[ "$result" == FAIL* ]]; then + ((fail_count++)) + elif [[ "$result" == PASS* ]]; then + ((pass_count++)) + fi +done + +echo -e " Passed: ${GREEN}${pass_count}${NC}" +echo -e " Failed: ${RED}${fail_count}${NC}" +echo -e " Warnings: ${YELLOW}${#WARNINGS[@]}${NC}" + +if [[ ${#WARNINGS[@]} -gt 0 ]]; then + echo "" + echo -e "${BOLD}Recommendations:${NC}" + for w in "${WARNINGS[@]}"; do + echo -e " ${YELLOW}→${NC} $w" + done +fi + +# ── STUN Recommendation ───────────────────────────────────── +echo "" +echo -e "${BOLD}Do you need STUN?${NC}" +echo "" + +if $vpn_found; then + echo -e " VPN detected on this server." + echo -e " ${GREEN}If your VPN provides direct routing (both sides get VPN IPs),${NC}" + echo -e " ${GREEN}STUN is likely NOT needed.${NC}" + echo "" + echo -e " ${YELLOW}If audio works one-way or not at all, enable STUN:${NC}" + echo -e " 1. docker compose --profile stun up -d (self-hosted STUN)" + echo -e " 2. Or via easy-asterisk: Server Settings → VPN STUN/ICE" +else + echo -e " No VPN interface found on server." + echo -e " ${YELLOW}If VPN runs on router/firewall:${NC}" + echo -e " - Add VPN subnet via: Server Settings → VLAN/VPN Subnets" + echo -e " - If audio still fails, enable STUN for NAT traversal" +fi + +echo ""