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..c97e871 --- /dev/null +++ b/.env.example @@ -0,0 +1,92 @@ +# ================================================================ +# Easy Asterisk - Environment Configuration +# +# 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 +# +# 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 +# ================================================================ + +# ── 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= + +# ── 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= + +# ── 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: 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= + +# ── 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. +# +# These credentials are shared between coturn and Asterisk. +# SIP clients do NOT need these - only the server uses them. +TURN_USERNAME=easyasterisk +TURN_PASSWORD= + +# ── 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: 10000-10200 +RTP_START=10000 +RTP_END=20000 + +# ── 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_AUTH_DISABLED=false diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..220f5e4 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,95 @@ +# ================================================================ +# 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 +# docker exec -it easy-asterisk vpn-diagnostics # VPN diagnostics +# docker exec -it easy-asterisk dns-whitelist # DNS whitelist check +# ================================================================ + +FROM ubuntu:24.04 + +ENV DEBIAN_FRONTEND=noninteractive +ENV LANG=C.UTF-8 + +# Install Asterisk and all 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 \ + lsof \ + && 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 + +# Docker detection marker (used by is_docker() in the script) +RUN touch /.dockerenv + +# 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 + +# STUN (if running coturn in same container) +EXPOSE 3478/udp + +# 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/README.md b/README.md index c554d84..69721c7 100644 --- a/README.md +++ b/README.md @@ -599,6 +599,70 @@ nc -v pbx.yourhouse.com 5061 If this fails, your port forwarding isn't set up correctly on your router. +### "Mobile devices on VPN can't reach Asterisk" (VPN Issues) + +When mobile devices connect through a VPN (Tailscale, WireGuard, etc.), Asterisk needs to know about the VPN subnet. Without this, VPN-connected devices appear offline. + +**Fix:** + +1. Run the installer: `sudo ./easy-asterisk-v0.10.0.sh` +2. Go to: **Server Settings > Configure VLAN/VPN Subnets** +3. Answer "y" when asked about VLANs/VPNs +4. Add your VPN subnet(s): + - **Tailscale**: `100.64.0.0/10` + - **WireGuard**: Usually `10.x.x.x/24` (check your WireGuard config) + - **OpenVPN**: Check your VPN config for the tunnel subnet +5. The script will auto-detect VPN interfaces on the server and suggest subnets + +**Important:** The Asterisk server itself must also be on the VPN. If using Tailscale, install Tailscale on the server too. Mobile devices should connect to the server's **VPN IP** (e.g., `100.x.x.x` for Tailscale), not its LAN IP. + +**Verify VPN connectivity:** +```bash +# On the mobile device (or from another VPN device), ping the server's VPN IP +ping 100.x.x.x + +# Test SIP port through VPN +nc -u -v 100.x.x.x 5060 +``` + +### "One-way audio when switching from WiFi to mobile data" + +This is a known issue with SIP clients on mobile devices. When the phone switches networks (WiFi to cellular or vice versa), the phone's IP address changes but the active audio stream may not update properly. + +**What happens:** +- The phone switches to mobile data and gets a new IP +- SIP signaling may update, but the audio (RTP) stream still uses the old path +- Result: the caller can't be heard by the receiving person + +**Server-side fixes (already applied for mobile devices in v0.10.0):** +- `rtp_symmetric=yes` - Asterisk sends audio back to wherever it receives audio from +- `rtp_keepalive=15` - Asterisk sends periodic keepalive packets to maintain NAT mappings +- `rtp_timeout=120` - Detects dead audio streams after 120 seconds +- `qualify_frequency=30` - Checks device availability every 30 seconds + +**Client-side fixes (on your phone):** + +For **Sipnetic**: +- Settings > Network > Enable "ICE" (if available) +- Settings > Network > Enable "STUN" (if available) +- Settings > Network > Keep-alive interval: 15-30 seconds +- Make sure "Background mode" is enabled + +For **Linphone**: +- Settings > Network > Enable ICE +- Settings > Network > STUN server: `stun.l.google.com:19302` +- Settings > Network > Enable TURN (if behind strict NAT) + +For **any SIP app**: +- Disable WiFi sleep / battery optimization for the app +- Enable "Keep WiFi on during sleep" in Android settings +- After switching networks, hang up and redial - this forces a clean reconnection + +**If the problem persists:** +- Consider using FQDN mode with TLS/SRTP instead of LAN/VPN mode +- FQDN mode enables ICE (Interactive Connectivity Establishment) which handles network changes better +- Alternatively, keep your phone on one network type (WiFi or mobile data) during calls + ### "My IP changed and FQDN stopped working" See [Dynamic IP Handling](#dynamic-ip-handling) section. You need to set up DDNS. diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..52d8031 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,114 @@ +# ================================================================ +# Easy Asterisk - Docker Compose +# +# Usage: +# docker compose up -d # Start everything +# docker exec -it easy-asterisk easy-asterisk # Interactive management +# docker exec -it easy-asterisk vpn-diagnostics # VPN diagnostics +# +# 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: + + # ── Asterisk PBX ─────────────────────────────────────────── + asterisk: + build: . + container_name: easy-asterisk + # Host networking required for: + # - RTP media ports (10000-20000 UDP) - too many to map individually + # - 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 + - asterisk-logs:/var/log/asterisk + - asterisk-spool:/var/spool/asterisk + - asterisk-lib:/var/lib/asterisk + environment: + # ── 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:-} + + # ── 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:-} + + # ── 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 ── + - 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 + + # ── 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.) + # + # Without TURN, calls work "sometimes" - with TURN, they always work. + coturn: + image: coturn/coturn:latest + container_name: easy-asterisk-coturn + network_mode: host + 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 + 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..ec73fa3 --- /dev/null +++ b/docker/entrypoint.sh @@ -0,0 +1,327 @@ +#!/bin/bash +# ================================================================ +# Easy Asterisk Docker Entrypoint +# +# 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 + +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. 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=${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 + +# ── 6. Write config file ───────────────────────────────────── +mkdir -p "$CONFIG_DIR" + +# 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="${DOMAIN_NAME:-$local_ip}" +DOMAIN_NAME="${DOMAIN_NAME:-}" +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="${PUBLIC_IP}" +PTT_DEVICE="" +PTT_KEYCODE="" +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="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" + +# ── 7. Initialize categories & rooms if missing ────────────── +CATEGORIES_FILE="${CONFIG_DIR}/categories.conf" +if [[ ! -f "$CATEGORIES_FILE" ]]; then + log_info "Creating default device categories..." + cat > "$CATEGORIES_FILE" << 'EOF' +kiosks|Kiosks|yes|Fixed wall-mount tablets & intercoms +mobile|Mobile|no|Phones & tablets (ring normally) +custom|Custom|no|Custom configuration +EOF +fi + +ROOMS_FILE="${CONFIG_DIR}/rooms.conf" +if [[ ! -f "$ROOMS_FILE" ]]; then + cat > "$ROOMS_FILE" << 'EOF' +# ext|name|members|timeout|type +EOF +fi + +# ── 8. Generate Asterisk configs ───────────────────────────── + +# 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) - FQDN: ${DOMAIN_NAME:-none} +[global] +type=global +user_agent=EasyAsterisk + +[transport-udp] +type=transport +protocol=udp +bind=0.0.0.0:5060 +; 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} | Public IP: ${PUBLIC_IP:-unknown} +${nat_settings} + +[transport-tls] +type=transport +protocol=tls +bind=0.0.0.0:5061 +; 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 +method=tlsv1_2 +${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' +[general] +static=yes +writeprotect=no +[default] +exten => _X.,1,Hangup() +[intercom] +EOF + chown asterisk:asterisk /etc/asterisk/extensions.conf +fi + +# ── Other core configs (only if missing) ── +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 + +if [[ ! -f /etc/asterisk/modules.conf ]]; then + cat > /etc/asterisk/modules.conf << 'EOF' +[modules] +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 + +# ── 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 + +# ── 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}" \ + WEBADMIN_AUTH_DISABLED="${WEB_ADMIN_AUTH_DISABLED:-false}" \ + python3 "$WEB_ADMIN_SCRIPT" & +fi + +# ── 11. Signal handling for clean shutdown ──────────────────── +cleanup() { + log_info "Shutting down..." + pkill -f "easy-asterisk-webadmin" 2>/dev/null || true + asterisk -rx "core stop now" 2>/dev/null || true + exit 0 +} +trap cleanup SIGTERM SIGINT + +# ── 12. Start Asterisk ─────────────────────────────────────── +log_info "Starting Asterisk PBX..." +echo "" +echo -e "${CYAN}══════════════════════════════════════════════════════════════${NC}" +echo -e "${CYAN} Easy Asterisk (Docker)${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-readme.md b/easy-asterisk-readme.md index c554d84..69721c7 100644 --- a/easy-asterisk-readme.md +++ b/easy-asterisk-readme.md @@ -599,6 +599,70 @@ nc -v pbx.yourhouse.com 5061 If this fails, your port forwarding isn't set up correctly on your router. +### "Mobile devices on VPN can't reach Asterisk" (VPN Issues) + +When mobile devices connect through a VPN (Tailscale, WireGuard, etc.), Asterisk needs to know about the VPN subnet. Without this, VPN-connected devices appear offline. + +**Fix:** + +1. Run the installer: `sudo ./easy-asterisk-v0.10.0.sh` +2. Go to: **Server Settings > Configure VLAN/VPN Subnets** +3. Answer "y" when asked about VLANs/VPNs +4. Add your VPN subnet(s): + - **Tailscale**: `100.64.0.0/10` + - **WireGuard**: Usually `10.x.x.x/24` (check your WireGuard config) + - **OpenVPN**: Check your VPN config for the tunnel subnet +5. The script will auto-detect VPN interfaces on the server and suggest subnets + +**Important:** The Asterisk server itself must also be on the VPN. If using Tailscale, install Tailscale on the server too. Mobile devices should connect to the server's **VPN IP** (e.g., `100.x.x.x` for Tailscale), not its LAN IP. + +**Verify VPN connectivity:** +```bash +# On the mobile device (or from another VPN device), ping the server's VPN IP +ping 100.x.x.x + +# Test SIP port through VPN +nc -u -v 100.x.x.x 5060 +``` + +### "One-way audio when switching from WiFi to mobile data" + +This is a known issue with SIP clients on mobile devices. When the phone switches networks (WiFi to cellular or vice versa), the phone's IP address changes but the active audio stream may not update properly. + +**What happens:** +- The phone switches to mobile data and gets a new IP +- SIP signaling may update, but the audio (RTP) stream still uses the old path +- Result: the caller can't be heard by the receiving person + +**Server-side fixes (already applied for mobile devices in v0.10.0):** +- `rtp_symmetric=yes` - Asterisk sends audio back to wherever it receives audio from +- `rtp_keepalive=15` - Asterisk sends periodic keepalive packets to maintain NAT mappings +- `rtp_timeout=120` - Detects dead audio streams after 120 seconds +- `qualify_frequency=30` - Checks device availability every 30 seconds + +**Client-side fixes (on your phone):** + +For **Sipnetic**: +- Settings > Network > Enable "ICE" (if available) +- Settings > Network > Enable "STUN" (if available) +- Settings > Network > Keep-alive interval: 15-30 seconds +- Make sure "Background mode" is enabled + +For **Linphone**: +- Settings > Network > Enable ICE +- Settings > Network > STUN server: `stun.l.google.com:19302` +- Settings > Network > Enable TURN (if behind strict NAT) + +For **any SIP app**: +- Disable WiFi sleep / battery optimization for the app +- Enable "Keep WiFi on during sleep" in Android settings +- After switching networks, hang up and redial - this forces a clean reconnection + +**If the problem persists:** +- Consider using FQDN mode with TLS/SRTP instead of LAN/VPN mode +- FQDN mode enables ICE (Interactive Connectivity Establishment) which handles network changes better +- Alternatively, keep your phone on one network type (WiFi or mobile data) during calls + ### "My IP changed and FQDN stopped working" See [Dynamic IP Handling](#dynamic-ip-handling) section. You need to set up DDNS. diff --git a/easy-asterisk-v0.10.0.sh b/easy-asterisk-v0.10.0.sh index e885257..db983d5 100644 --- a/easy-asterisk-v0.10.0.sh +++ b/easy-asterisk-v0.10.0.sh @@ -11,12 +11,22 @@ # - FIXED: Extension renaming now preserves AA tags correctly # - FIXED: LAN/VPN devices now explicitly use UDP transport (prevents TLS fallback) # - FIXED: LAN devices now have media_encryption=no to prevent SRTP issues +# - FIXED: VPN subnets now included as local_net in LAN mode (fixes VPN mobile offline) +# - FIXED: One-way audio on WiFi-to-mobile-data handoff (rtp_keepalive + timers) # - ADDED: Web Admin interface for browser-based client management # - View device status (online/offline) in real-time # - Add/delete devices via web interface # - View rooms and categories # - 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 # @@ -74,6 +84,113 @@ check_root() { fi } +# ── Docker / Container Detection ───────────────────────────── +# Returns 0 (true) if running inside a Docker/container environment + +is_docker() { + [[ -f /.dockerenv ]] || grep -qsE "docker|containerd|lxc" /proc/1/cgroup 2>/dev/null +} + +# Check if Asterisk process is running (works in both Docker and bare metal) +asterisk_running() { + if is_docker; then + pgrep -x asterisk >/dev/null 2>&1 + else + systemctl is-active asterisk >/dev/null 2>&1 + fi +} + +# Start/restart Asterisk (Docker-aware) +restart_asterisk_safe() { + print_info "Restarting Asterisk..." + if is_docker; then + # In Docker: use Asterisk CLI to restart, or restart the process + if pgrep -x asterisk >/dev/null 2>&1; then + asterisk -rx "core restart now" 2>/dev/null || true + sleep 3 + fi + # If not running, start it in the background + if ! pgrep -x asterisk >/dev/null 2>&1; then + rm -f /var/run/asterisk/asterisk.pid 2>/dev/null || true + asterisk -U asterisk -G asterisk & + sleep 3 + fi + if pgrep -x asterisk >/dev/null 2>&1; then + print_success "Asterisk running" + else + print_error "Asterisk failed to start" + fi + else + systemctl stop asterisk 2>/dev/null || true + sleep 2 + pkill -9 -x asterisk 2>/dev/null || true + rm -f /var/run/asterisk/asterisk.pid 2>/dev/null || true + rm -f /var/lib/asterisk/.asterisk_history 2>/dev/null || true + systemctl start asterisk + sleep 3 + if systemctl is-active asterisk >/dev/null; then + print_success "Asterisk running" + else + print_error "Asterisk failed to start" + journalctl -u asterisk -n 15 --no-pager + fi + fi +} + +# Web admin process management (Docker-aware) +webadmin_running() { + pgrep -f "easy-asterisk-webadmin" >/dev/null 2>&1 +} + +start_webadmin() { + load_config + if webadmin_running; then + print_warn "Web admin already running" + return + fi + create_web_admin_script + if [[ ! -f "$WEB_ADMIN_HTPASSWD" ]] && [[ "${WEB_ADMIN_AUTH_DISABLED:-}" != "true" ]]; then + setup_web_admin_auth + fi + WEBADMIN_PORT="${WEB_ADMIN_PORT:-8080}" \ + WEBADMIN_AUTH_DISABLED="${WEB_ADMIN_AUTH_DISABLED:-false}" \ + nohup python3 "$WEB_ADMIN_SCRIPT" >/dev/null 2>&1 & + sleep 2 + if webadmin_running; then + print_success "Web Admin started on port ${WEB_ADMIN_PORT}" + else + print_error "Web Admin failed to start" + fi +} + +stop_webadmin() { + if webadmin_running; then + pkill -f "easy-asterisk-webadmin" 2>/dev/null || true + sleep 1 + # Force kill if still running + if webadmin_running; then + pkill -9 -f "easy-asterisk-webadmin" 2>/dev/null || true + sleep 1 + fi + fi + # Also kill anything on the port + local port_pids=$(lsof -ti ":${WEB_ADMIN_PORT}" 2>/dev/null) + if [[ -n "$port_pids" ]]; then + echo "$port_pids" | xargs kill -9 2>/dev/null || true + sleep 1 + fi + if ! webadmin_running; then + print_success "Web Admin stopped" + else + print_error "Web Admin could not be stopped" + fi +} + +restart_webadmin() { + stop_webadmin 2>/dev/null + start_webadmin +} + generate_password() { tr -dc 'A-Za-z0-9' < /dev/urandom | head -c 16 } @@ -164,6 +281,12 @@ 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:-}" + TURN_ENABLED="${TURN_ENABLED:-n}" + TURN_SERVER="${TURN_SERVER:-}" + TURN_USERNAME="${TURN_USERNAME:-}" + TURN_PASSWORD="${TURN_PASSWORD:-}" return 0 } @@ -201,6 +324,12 @@ 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" +TURN_ENABLED="$TURN_ENABLED" +TURN_SERVER="$TURN_SERVER" +TURN_USERNAME="$TURN_USERNAME" +TURN_PASSWORD="$TURN_PASSWORD" EOF chmod 644 "$CONFIG_FILE" @@ -215,6 +344,12 @@ EOF } open_firewall_ports() { + if is_docker; then + # In Docker, firewall is managed on the host, not inside the container + # With network_mode: host, all ports are directly accessible + print_info "Docker mode: firewall is managed on the host" + return + fi print_info "Configuring firewall ports..." if command -v ufw &>/dev/null; then if ufw status 2>/dev/null | grep -q "Status: active"; then @@ -609,46 +744,92 @@ 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" - 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 + 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 -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" + # Mobile devices benefit from keepalive to maintain NAT mappings + # during WiFi/mobile data transitions + local keepalive_block="" + if [[ "$cat_id" == "mobile" ]]; then + keepalive_block="rtp_keepalive=15 +rtp_timeout=120 +rtp_timeout_hold=120" + fi + cat >> /etc/asterisk/pjsip.conf << EOF ; === Device: $name ($cat_id) $override_tag === @@ -666,6 +847,7 @@ direct_media=no rtp_symmetric=yes force_rport=yes rewrite_contact=yes +${keepalive_block} ${ice_block} auth=${ext} aors=${ext} @@ -681,7 +863,7 @@ password=${pass} type=aor max_contacts=5 remove_existing=yes -qualify_frequency=60 +qualify_frequency=30 EOF chown -R asterisk:asterisk /etc/asterisk 2>/dev/null || true @@ -1263,8 +1445,8 @@ detect_ptt_button() { echo " - Log out and log back in, or reboot" echo "═══════════════════════════════════════════════════════" - # Restart PTT service if client is installed - if [[ "$INSTALLED_CLIENT" == "y" && -n "$KIOSK_USER" ]]; then + # Restart PTT service if client is installed (bare metal only) + if [[ "$INSTALLED_CLIENT" == "y" && -n "$KIOSK_USER" ]] && ! is_docker; then local user_dbus="XDG_RUNTIME_DIR=/run/user/${KIOSK_UID}" echo "" print_info "Restarting PTT service..." @@ -1468,7 +1650,7 @@ show_preflight_check() { test_sip_connectivity() { print_header "SIP Connectivity Test" - if systemctl is-active asterisk >/dev/null; then + if asterisk_running; then print_success "Asterisk Running" else print_error "Asterisk Down" @@ -1494,35 +1676,81 @@ verify_cidr_config() { } configure_vlan_subnets() { - print_header "VLAN Configuration" + print_header "VLAN / VPN Subnet Configuration" load_config - echo "VLAN Support for Asterisk Easy" + echo "Additional Subnet Support for Easy Asterisk" echo "================================================" echo "" - echo "If your network uses VLANs (Virtual LANs), you need to" - echo "tell Asterisk about all the local subnets to prevent" - echo "calls from dropping after 30 seconds." + echo "If your network uses VLANs or VPNs, you need to tell" + echo "Asterisk about all the local subnets so that:" + echo " - Calls don't drop after 30 seconds (VLAN issue)" + echo " - VPN-connected mobile devices can register" + echo " - Audio works correctly for VPN users" echo "" echo "Example subnets:" echo " 192.168.1.0/24 - Main network" echo " 192.168.10.0/24 - IoT VLAN" - echo " 192.168.20.0/24 - Guest VLAN" - echo " 10.0.0.0/8 - Large private network" + echo " 100.64.0.0/10 - Tailscale VPN" + echo " 10.0.0.0/8 - WireGuard/OpenVPN" echo "" - read -p "Does your network use VLANs? (y/n) [${HAS_VLANS}]: " has_vlans + # Auto-detect VPN interfaces and their subnets + local detected_vpn_subnets="" + local vpn_info="" + while IFS= read -r line; do + local iface=$(echo "$line" | awk '{print $2}' | tr -d ':') + local addr=$(echo "$line" | awk '{print $4}') + if [[ -n "$addr" && -n "$iface" ]]; then + case "$iface" in + tailscale*|ts*) + vpn_info="${vpn_info} Detected: ${iface} -> ${addr} (Tailscale)\n" + detected_vpn_subnets="${detected_vpn_subnets} 100.64.0.0/10" + ;; + wg*) + vpn_info="${vpn_info} Detected: ${iface} -> ${addr} (WireGuard)\n" + detected_vpn_subnets="${detected_vpn_subnets} ${addr}" + ;; + tun*|tap*) + vpn_info="${vpn_info} Detected: ${iface} -> ${addr} (OpenVPN/VPN tunnel)\n" + detected_vpn_subnets="${detected_vpn_subnets} ${addr}" + ;; + nordlynx*|proton*) + vpn_info="${vpn_info} Detected: ${iface} -> ${addr} (VPN)\n" + detected_vpn_subnets="${detected_vpn_subnets} ${addr}" + ;; + esac + fi + done < <(ip -o -f inet addr show 2>/dev/null | grep -vE 'lo |docker|br-|veth') + detected_vpn_subnets=$(echo "$detected_vpn_subnets" | xargs -n1 2>/dev/null | sort -u | xargs 2>/dev/null) + + if [[ -n "$vpn_info" ]]; then + echo -e "${GREEN}VPN interfaces detected on this server:${NC}" + echo -e "$vpn_info" + echo " Suggested VPN subnets: ${detected_vpn_subnets}" + echo "" + echo " NOTE: If mobile devices connect via VPN (e.g., Tailscale on phones)," + echo " you MUST add the VPN subnet here for them to reach Asterisk." + echo "" + fi + + read -p "Does your network use VLANs or VPNs? (y/n) [${HAS_VLANS}]: " has_vlans has_vlans=${has_vlans:-$HAS_VLANS} if [[ "$has_vlans" =~ ^[Yy] ]]; then HAS_VLANS="y" echo "" - echo "Current VLAN Subnets: ${VLAN_SUBNETS:-none}" + echo "Current Subnets: ${VLAN_SUBNETS:-none}" + if [[ -n "$detected_vpn_subnets" ]]; then + echo "Detected VPN Subnets: ${detected_vpn_subnets}" + fi echo "" - echo "Enter VLAN subnets in CIDR notation, separated by spaces." - echo "Example: 192.168.1.0/24 192.168.10.0/24 192.168.20.0/24" + echo "Enter ALL additional subnets (VLAN + VPN) in CIDR notation, separated by spaces." + echo "Example: 192.168.10.0/24 100.64.0.0/10" echo "" - read -p "VLAN Subnets: " vlan_input + local default_subnets="${VLAN_SUBNETS:-$detected_vpn_subnets}" + read -p "Subnets [${default_subnets}]: " vlan_input + vlan_input="${vlan_input:-$default_subnets}" if [[ -n "$vlan_input" ]]; then VLAN_SUBNETS="$vlan_input" @@ -2260,6 +2488,21 @@ provisioning_manager_menu() { # ================================================================ manual_update_asterisk() { + if is_docker; then + print_header "Update Asterisk (Docker)" + echo " In Docker, Asterisk is updated by rebuilding the container image." + echo "" + echo " Steps:" + echo " 1. docker compose down" + echo " 2. docker compose build --no-cache" + echo " 3. docker compose up -d" + echo "" + echo " Your configuration is preserved in Docker volumes." + echo " Current version:" + asterisk -V 2>/dev/null || echo " Asterisk not running" + return + fi + print_header "Manual Asterisk Update" echo "WARNING: This will update Asterisk from the repository." echo "A backup will be created automatically." @@ -2291,12 +2534,9 @@ manual_update_asterisk() { # Restart echo "" - print_info "Restarting Asterisk..." - systemctl restart asterisk + restart_asterisk_safe - sleep 3 - - if systemctl is-active asterisk >/dev/null; then + if asterisk_running; then print_success "Asterisk updated successfully" asterisk -V echo "" @@ -2311,7 +2551,7 @@ manual_update_asterisk() { echo "" echo "Rolling back..." cp -r "$backup_dir/asterisk/"* /etc/asterisk/ - systemctl restart asterisk + restart_asterisk_safe print_info "Rollback complete" fi } @@ -2390,7 +2630,7 @@ watch_live_logs() { router_doctor() { print_header "Router Traffic Doctor" - if ! systemctl is-active asterisk >/dev/null; then + if ! asterisk_running; then print_error "Asterisk is NOT RUNNING" restart_asterisk_safe return @@ -2419,6 +2659,10 @@ router_doctor() { } configure_local_client() { + if is_docker; then + print_error "Local client not available in Docker. Use Sipnetic, Linphone, or Baresip on your phone/tablet." + return + fi print_header "Configure Local Client" load_config @@ -2552,6 +2796,10 @@ EOF } run_client_diagnostics() { + if is_docker; then + print_error "Client diagnostics not available in Docker. Run vpn-diagnostics for server-side checks." + return + fi print_header "Client Diagnostics" load_config local t_user="${KIOSK_USER:-$SUDO_USER}" @@ -2676,6 +2924,10 @@ verify_audio_setup() { # ================================================================ fix_asterisk_systemd() { + if is_docker; then + # No systemd in Docker - Asterisk runs as the main container process + return + fi print_info "Configuring systemd..." mkdir -p /etc/systemd/system/asterisk.service.d/ cat > /etc/systemd/system/asterisk.service.d/override.conf << 'SVCEOF' @@ -2778,22 +3030,31 @@ transport=config,pjsip.conf,criteria=type=transport EOF fi - # ICE and STUN only for FQDN/internet calling + # 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 - ice_stun_config="icesupport=yes -stunaddr=stun.l.google.com:19302" + 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 @@ -2841,10 +3102,17 @@ local_net=${vlan_subnet}" local nat_settings="" if [[ -n "$public_ip" && -n "$DOMAIN_NAME" ]]; then + # FQDN mode: full NAT settings with external addresses nat_settings="external_media_address=$public_ip external_signaling_address=$public_ip ${all_local_nets}" print_info "NAT: Public IP=$public_ip, Server IP=$server_ip" + elif [[ "$HAS_VLANS" == "y" && -n "$VLAN_SUBNETS" ]]; then + # LAN/VPN mode with VLAN/VPN subnets: include local_net entries + # so Asterisk recognizes VPN traffic as local (prevents VPN devices + # appearing offline and fixes media routing for VPN-connected mobiles) + nat_settings="${all_local_nets}" + print_info "LAN mode with additional subnets: $VLAN_SUBNETS" fi cat > "$conf_file" << EOF @@ -3013,25 +3281,8 @@ configure_asterisk() { rebuild_dialplan "quiet" restart_asterisk_safe - systemctl enable asterisk -} - -restart_asterisk_safe() { - print_info "Restarting Asterisk..." - systemctl stop asterisk 2>/dev/null || true - sleep 2 - # Use -x for exact match to avoid killing this script - pkill -9 -x asterisk 2>/dev/null || true - rm -f /var/run/asterisk/asterisk.pid 2>/dev/null || true - rm -f /var/lib/asterisk/.asterisk_history 2>/dev/null || true - systemctl start asterisk - sleep 3 - - if systemctl is-active asterisk >/dev/null; then - print_success "Asterisk running" - else - print_error "Asterisk failed to start" - journalctl -u asterisk -n 15 --no-pager + if ! is_docker; then + systemctl enable asterisk fi } @@ -3040,6 +3291,7 @@ restart_asterisk_safe() { # ================================================================ configure_baresip() { + if is_docker; then return; fi local baresip_dir="/home/${KIOSK_USER}/.baresip" mkdir -p "$baresip_dir" @@ -3151,6 +3403,10 @@ LAUNCHER } enable_client_services() { + if is_docker; then + # No local audio client in Docker containers + return + fi local systemd_dir="/home/${KIOSK_USER}/.config/systemd/user" mkdir -p "$systemd_dir" @@ -3453,12 +3709,24 @@ setup_internet_access() { # ================================================================ install_full() { + if is_docker; then + # In Docker: server is pre-installed, just configure + print_header "Server Configuration" + install_asterisk_packages + configure_asterisk + INSTALLED_SERVER="y" + ENABLE_TLS="n" + save_config + print_success "Server configured" + return + fi + print_header "Full Installation" local default_user="${SUDO_USER:-$USER}" read -p "Client User [$default_user]: " target_user KIOSK_USER="${target_user:-$default_user}" KIOSK_UID=$(id -u "$KIOSK_USER") - + if ! collect_common_config; then return; fi collect_client_config install_dependencies @@ -3486,6 +3754,11 @@ install_full() { } install_server_only() { + if is_docker; then + install_full + return + fi + print_header "Server Installation" ASTERISK_HOST="127.0.0.1" ENABLE_TLS="n" # LAN-only by default, set to "y" only if internet/certs setup is run @@ -3578,10 +3851,18 @@ collect_client_config() { install_dependencies() { install_asterisk_packages - install_baresip_packages + if ! is_docker; then + install_baresip_packages + fi } install_asterisk_packages() { + if is_docker; then + # In Docker, packages are pre-installed via Dockerfile + print_info "Docker mode: packages pre-installed" + mkdir -p /var/lib/asterisk /var/log/asterisk /var/spool/asterisk /var/run/asterisk + return + fi echo "exit 101" > /usr/sbin/policy-rc.d chmod +x /usr/sbin/policy-rc.d apt update @@ -3595,11 +3876,45 @@ install_asterisk_packages() { } install_baresip_packages() { + if is_docker; then + # Baresip (local SIP client) is not used inside the container + print_info "Docker mode: Baresip not applicable (use mobile/desktop SIP clients)" + return + fi apt update apt install -y baresip baresip-core pipewire pipewire-alsa pipewire-pulse wireplumber alsa-utils evtest || true } uninstall_menu() { + if is_docker; then + print_header "Reset Configuration" + echo " In Docker, the container is ephemeral." + echo " To fully uninstall: docker compose down -v" + echo "" + echo " 1) Reset all configs (keep container)" + echo " 2) Reset devices only" + echo " 0) Cancel" + read -p "Select: " ch + case $ch in + 1) + rm -rf /etc/easy-asterisk/* + print_success "Configuration reset. Restart container to regenerate defaults." + ;; + 2) + if [[ -f /etc/asterisk/pjsip.conf ]]; then + # Remove device sections, keep transport config + local temp="/tmp/pjsip_base_$$.conf" + awk '/^; === Device:/{exit} {print}' /etc/asterisk/pjsip.conf > "$temp" + mv "$temp" /etc/asterisk/pjsip.conf + chown asterisk:asterisk /etc/asterisk/pjsip.conf + asterisk -rx "pjsip reload" >/dev/null 2>&1 || true + fi + print_success "All devices removed" + ;; + esac + return + fi + print_header "Uninstall" echo " 1) Remove Everything" echo " 2) Asterisk Only" @@ -3646,29 +3961,65 @@ show_main_menu() { print_header "Easy Asterisk v${SCRIPT_VERSION}" load_config - echo " Status:" - if [[ -f "$CONFIG_FILE" ]]; then - [[ "$INSTALLED_SERVER" == "y" ]] && echo -e " Server: ${GREEN}Installed${NC}" || echo -e " Server: ${YELLOW}Not installed${NC}" - [[ "$INSTALLED_CLIENT" == "y" ]] && echo -e " Client: ${GREEN}Installed${NC}" || echo -e " Client: ${YELLOW}Not installed${NC}" + + if is_docker; then + # Docker status display + echo " Status:" + echo -e " Mode: ${CYAN}Docker Container${NC}" + if asterisk_running; then + echo -e " Asterisk: ${GREEN}Running${NC}" + else + echo -e " Asterisk: ${RED}Not running${NC}" + fi + if webadmin_running; then + echo -e " Web Admin: ${GREEN}Running${NC} (port ${WEB_ADMIN_PORT})" + else + echo -e " Web Admin: ${YELLOW}Stopped${NC}" + fi [[ -n "$DOMAIN_NAME" ]] && echo -e " Domain: ${DOMAIN_NAME}" - else - echo -e " ${YELLOW}Not configured${NC}" - fi - echo "" - - declare -A menu_map - local count=1 - - echo " ${count}) Install/Configure"; menu_map[$count]="submenu_install"; ((count++)) - if [[ "$INSTALLED_SERVER" == "y" ]]; then + 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 + local count=1 + + if [[ "$INSTALLED_SERVER" != "y" ]]; then + echo " ${count}) Configure Server"; menu_map[$count]="submenu_install"; ((count++)) + fi echo " ${count}) Server Settings"; menu_map[$count]="submenu_server"; ((count++)) echo " ${count}) Device Management"; menu_map[$count]="submenu_devices"; ((count++)) + echo " ${count}) Tools"; menu_map[$count]="submenu_tools"; ((count++)) + echo " 0) Exit" + else + # Bare metal status display + echo " Status:" + if [[ -f "$CONFIG_FILE" ]]; then + [[ "$INSTALLED_SERVER" == "y" ]] && echo -e " Server: ${GREEN}Installed${NC}" || echo -e " Server: ${YELLOW}Not installed${NC}" + [[ "$INSTALLED_CLIENT" == "y" ]] && echo -e " Client: ${GREEN}Installed${NC}" || echo -e " Client: ${YELLOW}Not installed${NC}" + [[ -n "$DOMAIN_NAME" ]] && echo -e " Domain: ${DOMAIN_NAME}" + else + echo -e " ${YELLOW}Not configured${NC}" + fi + echo "" + + declare -A menu_map + local count=1 + + echo " ${count}) Install/Configure"; menu_map[$count]="submenu_install"; ((count++)) + if [[ "$INSTALLED_SERVER" == "y" ]]; then + echo " ${count}) Server Settings"; menu_map[$count]="submenu_server"; ((count++)) + echo " ${count}) Device Management"; menu_map[$count]="submenu_devices"; ((count++)) + fi + echo " ${count}) Client Settings"; menu_map[$count]="submenu_client"; ((count++)) + echo " ${count}) Tools"; menu_map[$count]="submenu_tools"; ((count++)) + echo " 0) Exit" fi - echo " ${count}) Client Settings"; menu_map[$count]="submenu_client"; ((count++)) - echo " ${count}) Tools"; menu_map[$count]="submenu_tools"; ((count++)) - echo " 0) Exit" echo "" - + read -p " Select: " choice [[ "$choice" == "0" ]] && exit 0 local action=${menu_map[$choice]} @@ -3677,6 +4028,20 @@ show_main_menu() { } submenu_install() { + if is_docker; then + clear + print_header "Configure Server" + echo " 1) Configure/Reconfigure Server" + echo " 2) Reset Configuration" + echo " 0) Back" + read -p " Select: " choice + case $choice in + 1) install_full; read -p "Press Enter..." ;; + 2) uninstall_menu; read -p "Press Enter..." ;; + esac + return + fi + clear print_header "Install" echo " 1) Full (server + client)" @@ -4297,6 +4662,22 @@ 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' + 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' encryption = 'media_encryption=sdes' @@ -4304,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 = 'ice_support=yes' if (vpn_ice == 'y' or turn_enabled == 'y') else '' aa_tag = '' if auto_answer == 'yes': @@ -4312,6 +4693,11 @@ def add_device(name, category, extension, conn_type='lan', auto_answer=None): elif auto_answer == 'no': aa_tag = '[AA:no] ' + # Mobile devices get keepalive settings for NAT traversal + keepalive = '' + if category == 'mobile': + keepalive = 'rtp_keepalive=15\nrtp_timeout=120\nrtp_timeout_hold=120' + device_config = f''' ; === Device: {name} ({category}) {aa_tag}=== [{extension}] @@ -4328,6 +4714,7 @@ direct_media=no rtp_symmetric=yes force_rport=yes rewrite_contact=yes +{keepalive} {ice} auth={extension} aors={extension} @@ -4343,7 +4730,7 @@ password={password} type=aor max_contacts=5 remove_existing=yes -qualify_frequency=60 +qualify_frequency=30 ''' with open(PJSIP_CONF, 'a') as f: @@ -5606,6 +5993,11 @@ WEBADMIN } create_web_admin_service() { + if is_docker; then + # In Docker, web admin is managed as a background process, not a systemd service + print_success "Web admin service configured (Docker process mode)" + return + fi cat > "$WEB_ADMIN_SERVICE" << EOF [Unit] Description=Easy Asterisk Web Admin @@ -5664,9 +6056,9 @@ web_admin_menu() { print_header "Web Admin Management" - # Check current status + # Check current status (Docker-aware) local status="stopped" - if systemctl is-active --quiet easy-asterisk-webadmin 2>/dev/null; then + if webadmin_running; then status="running" fi @@ -5695,103 +6087,28 @@ web_admin_menu() { case $choice in 1) # Stop any existing instance first - if systemctl is-active --quiet easy-asterisk-webadmin 2>/dev/null; then - print_info "Stopping existing instance..." - systemctl stop easy-asterisk-webadmin 2>/dev/null || true - sleep 1 - fi - # Kill anything on our port - local port_pids=$(lsof -ti ":${WEB_ADMIN_PORT}" 2>/dev/null) - if [[ -n "$port_pids" ]]; then - echo "$port_pids" | xargs kill -9 2>/dev/null || true - sleep 1 - fi - - # Always regenerate script to ensure latest version + stop_webadmin 2>/dev/null print_info "Installing/updating web admin..." - create_web_admin_script - create_web_admin_service - if [[ ! -f "$WEB_ADMIN_HTPASSWD" ]] && [[ "${WEB_ADMIN_AUTH_DISABLED:-}" != "true" ]]; then - setup_web_admin_auth - fi - systemctl daemon-reload - systemctl enable easy-asterisk-webadmin - systemctl start easy-asterisk-webadmin - sleep 2 - if systemctl is-active --quiet easy-asterisk-webadmin; then - print_success "Web Admin started" + start_webadmin + if webadmin_running; then echo "" echo " Access at: http://${server_ip}:${WEB_ADMIN_PORT}/clients" [[ -n "$DOMAIN_NAME" ]] && echo " Or: http://${DOMAIN_NAME}:${WEB_ADMIN_PORT}/clients" - else - print_error "Failed to start. Check: journalctl -u easy-asterisk-webadmin" fi ;; 2) print_info "Stopping web admin..." - echo "" - - # Check what's on the port first - echo " $ netstat -tlnp | grep ${WEB_ADMIN_PORT}" - netstat -tlnp 2>/dev/null | grep "${WEB_ADMIN_PORT}" || echo " (nothing found)" - echo "" - - # First stop attempt - echo " $ systemctl stop easy-asterisk-webadmin" - systemctl stop easy-asterisk-webadmin 2>&1 || true - sleep 1 - - # Check port again - echo "" - echo " $ netstat -tlnp | grep ${WEB_ADMIN_PORT}" - netstat -tlnp 2>/dev/null | grep "${WEB_ADMIN_PORT}" || echo " (nothing found)" - - # If still in use, stop again - if netstat -tlnp 2>/dev/null | grep -q ":${WEB_ADMIN_PORT}"; then - echo "" - echo " Port still in use, running stop again..." - echo " $ systemctl stop easy-asterisk-webadmin" - systemctl stop easy-asterisk-webadmin 2>&1 || true - sleep 1 - - echo "" - echo " $ netstat -tlnp | grep ${WEB_ADMIN_PORT}" - netstat -tlnp 2>/dev/null | grep "${WEB_ADMIN_PORT}" || echo " (nothing found)" - fi - - # If STILL in use, kill processes - if netstat -tlnp 2>/dev/null | grep -q ":${WEB_ADMIN_PORT}"; then - echo "" - echo " Still in use, killing processes..." - local pid=$(netstat -tlnp 2>/dev/null | grep ":${WEB_ADMIN_PORT}" | awk '{print $7}' | cut -d'/' -f1 | head -1) - if [[ -n "$pid" ]]; then - echo " $ kill -9 $pid" - kill -9 "$pid" 2>&1 || true - sleep 1 - fi - fi - - # Disable the service - systemctl disable easy-asterisk-webadmin 2>/dev/null || true - - # Final verification - echo "" - echo " Final check:" - echo " $ netstat -tlnp | grep ${WEB_ADMIN_PORT}" - if netstat -tlnp 2>/dev/null | grep ":${WEB_ADMIN_PORT}"; then - print_error "Port ${WEB_ADMIN_PORT} still in use!" - else - echo " (nothing found)" - print_success "Web Admin stopped" + stop_webadmin + if ! is_docker; then + systemctl disable easy-asterisk-webadmin 2>/dev/null || true fi ;; 3) - systemctl restart easy-asterisk-webadmin - print_success "Web Admin restarted" + restart_webadmin ;; 4) setup_web_admin_auth - systemctl restart easy-asterisk-webadmin 2>/dev/null + restart_webadmin ;; 5) read -p "New port [${WEB_ADMIN_PORT}]: " new_port @@ -5799,15 +6116,18 @@ web_admin_menu() { if [[ "$new_port" =~ ^[0-9]+$ ]] && [[ "$new_port" -ge 1024 ]] && [[ "$new_port" -le 65535 ]]; then WEB_ADMIN_PORT="$new_port" save_config - create_web_admin_service - systemctl restart easy-asterisk-webadmin 2>/dev/null + restart_webadmin print_success "Port changed to $new_port" else print_error "Invalid port (must be 1024-65535)" fi ;; 6) - journalctl -u easy-asterisk-webadmin -n 50 --no-pager + if is_docker; then + echo " In Docker, check logs with: docker logs easy-asterisk" + else + journalctl -u easy-asterisk-webadmin -n 50 --no-pager + fi ;; 7) print_header "Reverse Proxy Setup (Caddy)" @@ -5828,7 +6148,7 @@ web_admin_menu() { WEB_ADMIN_AUTH_DISABLED="true" save_config create_web_admin_script - systemctl restart easy-asterisk-webadmin 2>/dev/null || true + restart_webadmin print_success "Internal auth disabled. Use Caddy basic_auth for security." ;; 2) @@ -5838,7 +6158,7 @@ web_admin_menu() { if [[ ! -f "$WEB_ADMIN_HTPASSWD" ]]; then setup_web_admin_auth fi - systemctl restart easy-asterisk-webadmin 2>/dev/null || true + restart_webadmin print_success "Internal auth enabled" ;; 3) @@ -5867,6 +6187,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" @@ -5878,9 +6398,10 @@ submenu_server() { echo " 6) Verify CIDR/NAT config" echo " 7) Watch Live Logs" echo " 8) Router Doctor" - echo " 9) Configure VLAN Subnets" + 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 @@ -5895,6 +6416,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..." @@ -6235,6 +6757,10 @@ submenu_client() { } fix_audio_manually() { + if is_docker; then + print_error "Audio management not available in Docker (no local audio hardware)" + return + fi print_header "Manual Audio Fix" load_config local t_user="${KIOSK_USER:-$SUDO_USER}" @@ -6275,21 +6801,50 @@ fix_audio_manually() { submenu_tools() { clear print_header "Tools" - echo " 1) Audio Test" - echo " 2) Verify Audio/Codec Setup" - echo " 3) Fix Audio (Unmute & Restart)" - echo " 4) Room Directory" - echo " 5) Manual Update Asterisk" - echo " 0) Back" - read -p " Select: " choice - case $choice in - 1) run_audio_test ;; - 2) verify_audio_setup ;; - 3) fix_audio_manually ;; - 4) show_room_directory ;; - 5) manual_update_asterisk ;; - 0) return ;; - esac + + if is_docker; then + echo " 1) Room Directory" + echo " 2) Update Asterisk (Docker)" + echo " 3) VPN Diagnostics" + echo " 4) DNS Whitelist Check" + echo " 0) Back" + read -p " Select: " choice + case $choice in + 1) show_room_directory ;; + 2) manual_update_asterisk ;; + 3) + if [[ -f /usr/local/bin/vpn-diagnostics ]]; then + bash /usr/local/bin/vpn-diagnostics + else + print_error "vpn-diagnostics not found" + fi + ;; + 4) + if [[ -f /usr/local/bin/dns-whitelist ]]; then + bash /usr/local/bin/dns-whitelist --check + else + print_error "dns-whitelist not found" + fi + ;; + 0) return ;; + esac + else + echo " 1) Audio Test" + echo " 2) Verify Audio/Codec Setup" + echo " 3) Fix Audio (Unmute & Restart)" + echo " 4) Room Directory" + echo " 5) Manual Update Asterisk" + echo " 0) Back" + read -p " Select: " choice + case $choice in + 1) run_audio_test ;; + 2) verify_audio_setup ;; + 3) fix_audio_manually ;; + 4) show_room_directory ;; + 5) manual_update_asterisk ;; + 0) return ;; + esac + fi [[ "$choice" != "0" ]] && read -p "Press Enter..." [[ "$choice" != "0" ]] && submenu_tools } 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 ""