#!/bin/bash # ================================================================ # Easy Asterisk - Interactive Installer v1.28 # # UPDATES in v1.28: # - FIXED: Device display now correctly shows UDP/5060 for LAN-only installs # - FIXED: Device display shows TLS/5061 only when internet/certs configured # - FIXED: SRTP shown as "Required" only with TLS, "Not required" for UDP # - FIXED: ICE support only enabled for FQDN/internet calling, not LAN-only # - FIXED: RTP config - icesupport and STUN only enabled for FQDN setups # - ADDED: Informative message after server install before internet setup prompt # - ADDED: load_config call in add_device_menu to properly read saved settings # # This fixes the issue where LAN-only servers incorrectly showed TLS/SRTP # requirements when adding devices, preventing proper UDP registration. # # RETAINED from v1.23: # - Router Guide separates VLAN (Allow) vs WAN (NAT) # - Caddy Helper generates snippets for both SIP and TURN domains # - PTT Mute-default, Audio Ducking, Device Management # ================================================================ set +e # Colors RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' CYAN='\033[0;36m' BOLD='\033[1m' NC='\033[0m' # Defaults DEFAULT_SIP_PORT="5060" DEFAULT_SIPS_PORT="5061" DEFAULT_TURN_PORT="3478" CONFIG_DIR="/etc/easy-asterisk" CONFIG_FILE="${CONFIG_DIR}/config" PTT_CONFIG_FILE="${CONFIG_DIR}/ptt-device" CATEGORIES_FILE="${CONFIG_DIR}/categories.conf" ROOMS_FILE="${CONFIG_DIR}/rooms.conf" COTURN_CONFIG="/etc/turnserver.conf" # ================================================================ # 1. CORE HELPER FUNCTIONS # ================================================================ print_header() { echo -e "\n${CYAN}╔══════════════════════════════════════════════════════════╗${NC}" echo -e "${CYAN} $1${NC}" echo -e "${CYAN}╚══════════════════════════════════════════════════════════╝${NC}\n" } print_info() { echo -e "${GREEN}[INFO]${NC} $1"; } print_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } print_error() { echo -e "${RED}[ERROR]${NC} $1"; } print_success() { echo -e "${GREEN}[OK]${NC} $1"; } check_root() { if [[ $EUID -ne 0 ]]; then print_error "This script must be run as root (use sudo)" exit 1 fi } generate_password() { tr -dc 'A-Za-z0-9' < /dev/urandom | head -c 16 } load_config() { if [[ -f "$CONFIG_FILE" ]]; then source "$CONFIG_FILE" 2>/dev/null || true fi INSTALLED_SERVER="${INSTALLED_SERVER:-n}" INSTALLED_CLIENT="${INSTALLED_CLIENT:-n}" INSTALLED_COTURN="${INSTALLED_COTURN:-n}" KIOSK_USER="${KIOSK_USER:-}" KIOSK_UID="${KIOSK_UID:-}" USE_COTURN="${USE_COTURN:-n}" TURN_SECRET="${TURN_SECRET:-}" TURN_USER="${TURN_USER:-kioskuser}" TURN_PASS="${TURN_PASS:-}" TURN_DOMAIN="${TURN_DOMAIN:-}" return 0 } backup_config() { local file=$1 if [[ -f "$file" ]]; then cp "$file" "${file}.backup-$(date +%s)" ls -tp "${file}.backup-"* 2>/dev/null | tail -n +6 | xargs -I {} rm -- {} 2>/dev/null fi } save_config() { mkdir -p "$CONFIG_DIR" cat > "$CONFIG_FILE" << EOF # Easy Asterisk Configuration - $(date) KIOSK_USER="$KIOSK_USER" KIOSK_UID="$KIOSK_UID" KIOSK_EXTENSION="$KIOSK_EXTENSION" KIOSK_NAME="$KIOSK_NAME" SIP_PASSWORD="$SIP_PASSWORD" ASTERISK_HOST="$ASTERISK_HOST" DOMAIN_NAME="$DOMAIN_NAME" TURN_DOMAIN="$TURN_DOMAIN" ENABLE_TLS="$ENABLE_TLS" CERT_PATH="$CERT_PATH" KEY_PATH="$KEY_PATH" INSTALLED_SERVER="$INSTALLED_SERVER" INSTALLED_CLIENT="$INSTALLED_CLIENT" INSTALLED_COTURN="$INSTALLED_COTURN" USE_COTURN="$USE_COTURN" TURN_SECRET="$TURN_SECRET" TURN_USER="$TURN_USER" TURN_PASS="$TURN_PASS" CURRENT_PUBLIC_IP="$CURRENT_PUBLIC_IP" PTT_DEVICE="$PTT_DEVICE" PTT_KEYCODE="$PTT_KEYCODE" LOCAL_CIDR="$LOCAL_CIDR" EOF chmod 600 "$CONFIG_FILE" # Save PTT config separately if [[ -n "$PTT_DEVICE" ]]; then cat > "$PTT_CONFIG_FILE" << EOF PTT_DEVICE="$PTT_DEVICE" PTT_KEYCODE="$PTT_KEYCODE" EOF chmod 600 "$PTT_CONFIG_FILE" fi } open_firewall_ports() { print_info "Configuring firewall ports..." if command -v ufw &>/dev/null; then if ufw status 2>/dev/null | grep -q "Status: active"; then ufw allow 5060/udp comment "SIP UDP" 2>/dev/null || true ufw allow 5061/tcp comment "SIP TLS" 2>/dev/null || true ufw allow 10000:20000/udp comment "RTP Media" 2>/dev/null || true if [[ "$USE_COTURN" == "y" ]]; then ufw allow ${DEFAULT_TURN_PORT}/udp comment "TURN UDP" 2>/dev/null || true ufw allow ${DEFAULT_TURN_PORT}/tcp comment "TURN TCP" 2>/dev/null || true # Allow relay range for TURN ufw allow 49152:65535/udp comment "TURN Relay" 2>/dev/null || true fi ufw reload 2>/dev/null || true print_success "UFW firewall ports opened" fi fi } # ================================================================ # 2. COTURN SETUP & DYNAMIC IP # ================================================================ get_public_ip() { local ip=$(curl -s -4 --connect-timeout 5 ifconfig.me 2>/dev/null || curl -s -4 --connect-timeout 5 icanhazip.com 2>/dev/null || echo "") echo "$ip" } install_coturn() { print_header "Installing COTURN" apt update apt install -y coturn if [[ -z "$TURN_PASS" ]]; then TURN_PASS=$(generate_password) fi local public_ip=$(get_public_ip) CURRENT_PUBLIC_IP="$public_ip" if [[ -z "$public_ip" ]]; then print_error "Could not detect public IP" return 1 fi print_info "Public IP: $public_ip" # Enable coturn sed -i 's/#TURNSERVER_ENABLED=1/TURNSERVER_ENABLED=1/' /etc/default/coturn 2>/dev/null || true # Configure coturn backup_config "$COTURN_CONFIG" cat > "$COTURN_CONFIG" << EOF # Easy Asterisk COTURN Configuration listening-port=${DEFAULT_TURN_PORT} fingerprint lt-cred-mech realm=${TURN_DOMAIN:-${DOMAIN_NAME:-turn.local}} total-quota=100 stale-nonce=600 cert=/etc/asterisk/certs/server.crt pkey=/etc/asterisk/certs/server.key no-tlsv1 no-tlsv1_1 cipher-list="ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-SHA384" dh2066 no-stdout-log log-file=/var/log/turnserver.log simple-log external-ip=${public_ip} min-port=49152 max-port=65535 user-quota=12 no-multicast-peers no-cli user=${TURN_USER}:${TURN_PASS} EOF chmod 600 "$COTURN_CONFIG" systemctl enable coturn systemctl restart coturn if systemctl is-active coturn >/dev/null; then print_success "COTURN installed and running" INSTALLED_COTURN="y" USE_COTURN="y" save_config return 0 else print_error "COTURN failed to start" journalctl -u coturn -n 20 --no-pager return 1 fi } configure_coturn_credentials() { print_header "Configure TURN Credentials" echo "Current User: ${TURN_USER}" echo "Current Pass: ${TURN_PASS}" echo "" read -p "Enter Username [${TURN_USER}]: " t_user t_user="${t_user:-$TURN_USER}" read -p "Enter Password [generate]: " t_pass t_pass="${t_pass:-$(generate_password)}" TURN_USER="$t_user" TURN_PASS="$t_pass" if [[ -f "$COTURN_CONFIG" ]]; then # Remove old user lines and add new one sed -i '/^user=/d' "$COTURN_CONFIG" echo "user=${TURN_USER}:${TURN_PASS}" >> "$COTURN_CONFIG" systemctl restart coturn print_success "Credentials updated and service restarted" else print_error "COTURN not installed. Run install first." fi save_config } update_coturn_ip() { local new_ip=$(get_public_ip) if [[ -z "$new_ip" ]]; then print_warn "Could not detect public IP" return 1 fi if [[ "$new_ip" == "$CURRENT_PUBLIC_IP" ]]; then print_info "IP unchanged: $new_ip" return 0 fi print_info "IP changed: $CURRENT_PUBLIC_IP -> $new_ip" # Update coturn config if [[ -f "$COTURN_CONFIG" ]]; then sed -i "s/^external-ip=.*/external-ip=${new_ip}/" "$COTURN_CONFIG" systemctl restart coturn print_success "COTURN updated" fi # Update pjsip config if [[ -f /etc/asterisk/pjsip.conf ]]; then sed -i "s/^external_media_address=.*/external_media_address=${new_ip}/" /etc/asterisk/pjsip.conf sed -i "s/^external_signaling_address=.*/external_signaling_address=${new_ip}/" /etc/asterisk/pjsip.conf asterisk -rx "pjsip reload" 2>/dev/null print_success "Asterisk updated" fi CURRENT_PUBLIC_IP="$new_ip" save_config return 0 } create_ip_update_script() { cat > /usr/local/bin/easy-asterisk-update-ip << 'IPSCRIPT' #!/bin/bash CONFIG_FILE="/etc/easy-asterisk/config" [[ -f "$CONFIG_FILE" ]] && source "$CONFIG_FILE" get_public_ip() { curl -s -4 --connect-timeout 5 ifconfig.me 2>/dev/null || curl -s -4 --connect-timeout 5 icanhazip.com 2>/dev/null } NEW_IP=$(get_public_ip) [[ -z "$NEW_IP" ]] && exit 0 [[ "$NEW_IP" == "$CURRENT_PUBLIC_IP" ]] && exit 0 # Update COTURN if [[ -f /etc/turnserver.conf ]]; then sed -i "s/^external-ip=.*/external-ip=${NEW_IP}/" /etc/turnserver.conf systemctl restart coturn fi # Update Asterisk if [[ -f /etc/asterisk/pjsip.conf ]]; then sed -i "s/^external_media_address=.*/external_media_address=${NEW_IP}/" /etc/asterisk/pjsip.conf sed -i "s/^external_signaling_address=.*/external_signaling_address=${NEW_IP}/" /etc/asterisk/pjsip.conf asterisk -rx "pjsip reload" 2>/dev/null fi # Update config file sed -i "s/^CURRENT_PUBLIC_IP=.*/CURRENT_PUBLIC_IP=\"${NEW_IP}\"/" "$CONFIG_FILE" logger "Easy Asterisk: Updated IP to ${NEW_IP}" IPSCRIPT chmod +x /usr/local/bin/easy-asterisk-update-ip # Create systemd timer cat > /etc/systemd/system/easy-asterisk-ip-update.service << 'EOF' [Unit] Description=Easy Asterisk IP Update After=network-online.target [Service] Type=oneshot ExecStart=/usr/local/bin/easy-asterisk-update-ip EOF cat > /etc/systemd/system/easy-asterisk-ip-update.timer << 'EOF' [Unit] Description=Easy Asterisk IP Update Timer [Timer] OnBootSec=2min OnUnitActiveSec=10min [Install] WantedBy=timers.target EOF systemctl daemon-reload systemctl enable easy-asterisk-ip-update.timer systemctl start easy-asterisk-ip-update.timer print_success "IP update timer installed (checks every 10 minutes)" } configure_coturn_menu() { print_header "Configure COTURN (TURN Server)" if [[ "$INSTALLED_COTURN" == "y" ]]; then echo -e "Status: ${GREEN}Installed${NC}" echo "User: ${TURN_USER}" echo "Pass: ${TURN_PASS}" echo "" echo " 1) Update Credentials (User/Pass)" echo " 2) Reinstall/Reconfigure" echo " 3) Update IP manually" echo " 4) Show configuration" echo " 5) Uninstall" echo " 0) Back" read -p "Select: " choice case $choice in 1) configure_coturn_credentials ;; 2) install_coturn ;; 3) update_coturn_ip ;; 4) show_coturn_config ;; 5) uninstall_coturn ;; esac else echo "COTURN is not installed." read -p "Install now? [Y/n]: " install if [[ ! "$install" =~ ^[Nn]$ ]]; then install_coturn if [[ "$INSTALLED_COTURN" == "y" ]]; then create_ip_update_script fi fi fi } show_coturn_config() { print_header "COTURN Configuration" echo "Status: $(systemctl is-active coturn)" echo "Public IP: $CURRENT_PUBLIC_IP" echo "Port: ${DEFAULT_TURN_PORT}" echo "Credentials: ${TURN_USER} : ${TURN_PASS}" echo "" echo "Client Config String:" echo "turn:${TURN_USER}:${TURN_PASS}@${TURN_DOMAIN:-${DOMAIN_NAME:-$CURRENT_PUBLIC_IP}}:${DEFAULT_TURN_PORT}" echo "" echo "Logs:" tail -n 20 /var/log/turnserver.log 2>/dev/null || echo " No logs found" } uninstall_coturn() { systemctl stop coturn 2>/dev/null || true systemctl disable coturn 2>/dev/null || true apt purge -y coturn 2>/dev/null || true rm -f /etc/turnserver.conf rm -f /usr/local/bin/easy-asterisk-update-ip systemctl stop easy-asterisk-ip-update.timer 2>/dev/null || true systemctl disable easy-asterisk-ip-update.timer 2>/dev/null || true rm -f /etc/systemd/system/easy-asterisk-ip-update.* systemctl daemon-reload INSTALLED_COTURN="n" USE_COTURN="n" save_config print_success "COTURN uninstalled" } # ================================================================ # 3. DEVICE MANAGEMENT # ================================================================ initialize_default_categories() { mkdir -p "$CONFIG_DIR" if [[ ! -f "$CATEGORIES_FILE" ]]; then cat > "$CATEGORIES_FILE" << 'EOF' # Format: id|name|auto_answer(yes/no)|description kiosk|Kiosks|yes|Fixed auto-answer intercoms mobile|Mobile Devices|no|Phones and mobile devices EOF chmod 600 "$CATEGORIES_FILE" fi if [[ ! -f "$ROOMS_FILE" ]]; then cat > "$ROOMS_FILE" << 'EOF' # Format: ext|name|members|timeout|type(ring/page) 199|All Kiosks|101,102,103,104,105|60|page 299|All Mobile|201,202,203,204,205|60|ring EOF chmod 600 "$ROOMS_FILE" fi } list_categories() { initialize_default_categories local index=1 while IFS='|' read -r cat_id cat_name auto_answer description; do [[ "$cat_id" =~ ^# ]] && continue [[ -z "$cat_id" ]] && continue local auto_text="${RED}Ring${NC}" [[ "$auto_answer" == "yes" ]] && auto_text="${GREEN}Auto-answer${NC}" echo -e " ${CYAN}$index)${NC} ${BOLD}$cat_name${NC} ($cat_id) - $auto_text" ((index++)) done < "$CATEGORIES_FILE" } get_category_by_index() { local target_index=$1 local index=1 while IFS='|' read -r cat_id cat_name auto_answer description; do [[ "$cat_id" =~ ^# ]] && continue [[ -z "$cat_id" ]] && continue if [[ $index -eq $target_index ]]; then echo "$cat_id|$cat_name|$auto_answer" return 0 fi ((index++)) done < "$CATEGORIES_FILE" } manage_categories() { print_header "Manage Categories" list_categories echo "" echo " 1) Add Category" echo " 2) Delete Category" echo " 0) Back" read -p "Select: " choice case $choice in 1) read -p "ID (lowercase): " cid read -p "Display Name: " cname read -p "Auto Answer? [y/N]: " ca local ans="no" [[ "$ca" =~ ^[Yy]$ ]] && ans="yes" echo "${cid}|${cname}|${ans}|Custom category" >> "$CATEGORIES_FILE" print_success "Category added" rebuild_dialplan ;; 2) read -p "Number to delete: " num local data=$(get_category_by_index "$num") local cid=$(echo "$data" | cut -d'|' -f1) [[ -n "$cid" ]] && sed -i "/^${cid}|/d" "$CATEGORIES_FILE" print_success "Deleted" rebuild_dialplan ;; esac } manage_rooms() { print_header "Manage Rooms" initialize_default_categories echo "Current Rooms:" local index=1 while IFS='|' read -r rext rname rmem rtime rtype; do [[ "$rext" =~ ^# ]] && continue [[ -z "$rext" ]] && continue local type_text="Ring Group" [[ "$rtype" == "page" ]] && type_text="${GREEN}PAGE/INTERCOM${NC}" echo -e " ${CYAN}$index)${NC} ${BOLD}$rname${NC} ($rext) - $type_text" echo -e " Members: $rmem" ((index++)) done < "$ROOMS_FILE" echo "" echo " 1) Add Room" echo " 2) Edit Room Members" echo " 3) Delete Room" echo " 0) Back" read -p "Select: " choice case $choice in 1) read -p "Room Extension: " new_ext read -p "Room Name: " new_name echo " 1) Ring Group (Phones ring)" echo " 2) Page/Intercom (Auto-answer)" read -p "Select [1]: " type_sel local rtype="ring" [[ "$type_sel" == "2" ]] && rtype="page" read -p "Members (e.g. 101,102): " members echo "${new_ext}|${new_name}|${members}|60|${rtype}" >> "$ROOMS_FILE" rebuild_dialplan print_success "Room Created" ;; 2) read -p "Select Room #: " rnum local target_line="" local count=0 while IFS= read -r line; do if [[ ! "$line" =~ ^# ]] && [[ -n "$line" ]]; then ((count++)) if [[ $count -eq $rnum ]]; then target_line="$line"; break; fi fi done < "$ROOMS_FILE" if [[ -n "$target_line" ]]; then IFS='|' read -r rext rname rmem rtime rtype <<< "$target_line" echo "Current members: $rmem" read -p "New members: " new_mem sed -i "/^${rext}|/d" "$ROOMS_FILE" echo "${rext}|${rname}|${new_mem}|${rtime}|${rtype}" >> "$ROOMS_FILE" rebuild_dialplan print_success "Room Updated" fi ;; 3) read -p "Select Room #: " rnum local count=0 local target_ext="" while IFS='|' read -r rext rrest; do if [[ ! "$rext" =~ ^# ]] && [[ -n "$rext" ]]; then ((count++)) if [[ $count -eq $rnum ]]; then target_ext="$rext"; break; fi fi done < "$ROOMS_FILE" if [[ -n "$target_ext" ]]; then sed -i "/^${target_ext}|/d" "$ROOMS_FILE" rebuild_dialplan print_success "Room Deleted" fi ;; esac } add_device_menu() { print_header "Add Device" load_config # Load saved configuration to check ENABLE_TLS, DOMAIN_NAME, etc. list_categories read -p "Category number: " cat_num local cat_data=$(get_category_by_index "$cat_num") if [[ -z "$cat_data" ]]; then print_error "Invalid"; return; fi local cat_id=$(echo "$cat_data" | cut -d'|' -f1) local cat_name=$(echo "$cat_data" | cut -d'|' -f2) local auto_answer=$(echo "$cat_data" | cut -d'|' -f3) local start_range=101 end_range=199 case "$cat_id" in kiosk) start_range=101; end_range=199 ;; mobile) start_range=201; end_range=299 ;; *) start_range=301; end_range=399 ;; esac local suggested_ext="" for ext in $(seq $start_range $end_range); do if ! grep -q "^\[${ext}\]" /etc/asterisk/pjsip.conf 2>/dev/null; then suggested_ext=$ext; break fi done read -p "Extension [$suggested_ext]: " ext ext="${ext:-$suggested_ext}" if grep -q "^\[${ext}\]" /etc/asterisk/pjsip.conf 2>/dev/null; then print_error "Extension exists!"; return fi read -p "Name: " name name="${name:-Device $ext}" local pass=$(generate_password) local override_tag="" if [[ "$auto_answer" == "no" ]]; then read -p "Force AUTO-ANSWER? [y/N]: " force_aa [[ "$force_aa" =~ ^[Yy]$ ]] && override_tag="[AA:yes]" && auto_answer="yes" elif [[ "$auto_answer" == "yes" ]]; then read -p "Force RING? [y/N]: " force_ring [[ "$force_ring" =~ ^[Yy]$ ]] && override_tag="[AA:no]" && auto_answer="no" fi # SDES-SRTP for compatibility local encryption_block="" if [[ "$ENABLE_TLS" == "y" ]]; then encryption_block="media_encryption=sdes transport=transport-tls" fi # ICE support only for FQDN/internet calling local ice_block="" if [[ -n "$DOMAIN_NAME" ]]; then ice_block="ice_support=yes" fi backup_config "/etc/asterisk/pjsip.conf" cat >> /etc/asterisk/pjsip.conf << EOF ; === Device: $name ($cat_id) $override_tag === [${ext}] type=endpoint context=intercom disallow=all allow=opus allow=ulaw allow=alaw allow=g722 ${encryption_block} direct_media=no rtp_symmetric=yes force_rport=yes rewrite_contact=yes ${ice_block} auth=${ext} aors=${ext} callerid="${name}" <${ext}> [${ext}] type=auth auth_type=userpass username=${ext} password=${pass} [${ext}] type=aor max_contacts=5 remove_existing=yes qualify_frequency=60 EOF chown -R asterisk:asterisk /etc/asterisk asterisk -rx "pjsip reload" >/dev/null 2>&1 rebuild_dialplan echo "════════════════════════════════════════════════════════" echo " DEVICE ADDED" echo "════════════════════════════════════════════════════════" echo " Server: ${DOMAIN_NAME:-$(hostname -I | awk '{print $1}')}" echo " Extension: $ext" echo " Password: $pass" if [[ "$ENABLE_TLS" == "y" ]]; then echo " Transport: TLS (port 5061)" echo " SRTP: Required" else echo " Transport: UDP (port 5060)" echo " SRTP: Not required" fi if [[ "$USE_COTURN" == "y" ]]; then echo " TURN: ${TURN_DOMAIN:-${DOMAIN_NAME:-$CURRENT_PUBLIC_IP}}:${DEFAULT_TURN_PORT}" fi echo "════════════════════════════════════════════════════════" } remove_device() { print_header "Remove Device" declare -A REMOVE_MAP local count=1 echo "Select device to remove:" echo "" while IFS= read -r line; do if [[ "$line" == *"; === Device:"* ]]; then local temp="${line#*; === Device: }" temp="${temp% ===}" temp="${temp% \[AA:*\]}" local name="${temp% (*)}" fi if [[ "$line" =~ ^\[([0-9]+)\] ]]; then local ext="${BASH_REMATCH[1]}" if [[ -n "$name" ]]; then echo " ${count}) Ext ${ext} - ${name}" REMOVE_MAP[$count]=$ext ((count++)) name="" fi fi done < /etc/asterisk/pjsip.conf echo "" echo " 98) DELETE ALL DEVICES" echo " 0) Cancel" echo "" read -p "Select: " choice if [[ "$choice" == "98" ]]; then echo "" print_warn "This will DELETE ALL DEVICES!" read -p "Type 'DELETE ALL' to confirm: " confirm if [[ "$confirm" == "DELETE ALL" ]]; then backup_config "/etc/asterisk/pjsip.conf" # Remove all device sections sed -i '/^; === Device:/,/^$/d' /etc/asterisk/pjsip.conf sed -i '/^\[[0-9]\{3\}\]/,/^$/d' /etc/asterisk/pjsip.conf asterisk -rx "pjsip reload" 2>/dev/null rebuild_dialplan print_success "All devices deleted" else print_error "Cancelled" fi return fi [[ "$choice" == "0" || -z "${REMOVE_MAP[$choice]}" ]] && return local ext="${REMOVE_MAP[$choice]}" read -p "Confirm removal of $ext? [y/N]: " confirm if [[ "$confirm" =~ ^[Yy]$ ]]; then backup_config "/etc/asterisk/pjsip.conf" sed -i "/^; === Device:.*${ext}.*/,/^$/d" /etc/asterisk/pjsip.conf sed -i "/^\[${ext}\]/,/^$/d" /etc/asterisk/pjsip.conf asterisk -rx "pjsip reload" 2>/dev/null rebuild_dialplan print_success "Removed" fi } show_registered_devices() { print_header "Device Status" printf "${CYAN}%-6s %-20s %-15s %-15s %-15s${NC}\n" "Ext" "Name" "Category" "Status" "Password" echo "------------------------------------------------------------------------------" local dev_name="" dev_cat="" while IFS= read -r line; do if [[ "$line" == *"; === Device:"* ]]; then local temp="${line#*; === Device: }" temp="${temp% ===}" [[ "$temp" == *"[AA:"* ]] && temp="${temp% \[AA:*\]}" dev_cat="${temp##* (}"; dev_cat="${dev_cat%)}" dev_name="${temp% (*)}" fi if [[ "$line" =~ ^\[([0-9]+)\] ]]; then local ext="${BASH_REMATCH[1]}" if [[ -n "$dev_name" ]]; then local status="${RED}Offline${NC}" local avail=$(asterisk -rx "pjsip show endpoint ${ext}" 2>/dev/null | grep -E "Contact:.*(Avail|NonQual)" || true) [[ -n "$avail" ]] && status="${GREEN}Online${NC}" local password=$(grep -A 10 "^\[$ext\]" /etc/asterisk/pjsip.conf | grep "password=" | head -1 | cut -d= -f2) printf "%-6s %-20s %-15s %b %-15s\n" "$ext" "${dev_name:0:18}" "$dev_cat" "$status" "$password" dev_name="" fi fi done < /etc/asterisk/pjsip.conf echo "" echo "Connection Details:" echo " Domain: ${DOMAIN_NAME:-$(hostname -I | awk '{print $1}')}" echo " Port: ${DEFAULT_SIP_PORT}/udp (LAN) or ${DEFAULT_SIPS_PORT}/tcp (TLS)" } # ================================================================ # 4. PTT WIZARD (Fixed: Mute by default) # ================================================================ configure_ptt_menu() { print_header "Configure PTT Button" detect_ptt_button } detect_ptt_button() { # Ensure evtest is installed if ! command -v evtest &>/dev/null; then apt install -y evtest >/dev/null 2>&1 fi # Add user to input group [[ -n "$KIOSK_USER" ]] && usermod -aG input "$KIOSK_USER" 2>/dev/null || true print_info "Scanning input devices..." echo "" declare -a SUGGESTED_DEVICES SUGGESTED_NAMES OTHER_DEVICES OTHER_NAMES for dev in /dev/input/event*; do [[ -e "$dev" ]] || continue local name=$(cat "/sys/class/input/$(basename $dev)/device/name" 2>/dev/null || echo "Unknown") local lname=$(echo "$name" | tr '[:upper:]' '[:lower:]') # Filter out system devices that aren't PTT candidates if [[ "$lname" =~ (power.button|sleep.button|lid.switch|virtual|video.bus|hdmi|dp,pcm|hotkey|touchpad|touchscreen) ]]; then OTHER_DEVICES+=("$dev") OTHER_NAMES+=("$name") # Prioritize keyboards, USB HID devices, pedals elif [[ "$lname" =~ (keyboard|sayo.*nano$|pedal|foot|^hid) ]]; then SUGGESTED_DEVICES+=("$dev") SUGGESTED_NAMES+=("$name") else OTHER_DEVICES+=("$dev") OTHER_NAMES+=("$name") fi done # Display suggested devices first if [[ ${#SUGGESTED_DEVICES[@]} -gt 0 ]]; then echo -e "${GREEN}Keyboards and USB buttons:${NC}" for i in "${!SUGGESTED_DEVICES[@]}"; do printf " ${CYAN}%2d)${NC} %s - %s\n" "$((i+1))" "$(basename ${SUGGESTED_DEVICES[$i]})" "${SUGGESTED_NAMES[$i]}" done echo "" fi # Display other devices if [[ ${#OTHER_DEVICES[@]} -gt 0 ]]; then echo -e "${YELLOW}Other devices:${NC}" local offset=${#SUGGESTED_DEVICES[@]} for i in "${!OTHER_DEVICES[@]}"; do printf " ${CYAN}%2d)${NC} %s - %s\n" "$((offset+i+1))" "$(basename ${OTHER_DEVICES[$i]})" "${OTHER_NAMES[$i]}" done echo "" fi local ALL_DEVICES=("${SUGGESTED_DEVICES[@]}" "${OTHER_DEVICES[@]}") local total=${#ALL_DEVICES[@]} if [[ $total -eq 0 ]]; then print_error "No input devices found" return 1 fi echo " 0) Back" echo "" read -p "Select device [1]: " selection selection="${selection:-1}" [[ "$selection" == "0" ]] && return 0 [[ "$selection" -lt 1 || "$selection" -gt "$total" ]] && { print_error "Invalid selection"; return 1; } PTT_DEVICE="${ALL_DEVICES[$((selection-1))]}" local dev_name=$(cat "/sys/class/input/$(basename $PTT_DEVICE)/device/name" 2>/dev/null || echo "Unknown") echo "" print_success "Selected: $dev_name" echo " ($PTT_DEVICE)" echo "" # Key detection loop while true; do echo -e "${YELLOW}══════════════════════════════════════════════════${NC}" echo -e "${YELLOW} DO NOT PRESS YET - wait for countdown${NC}" echo -e "${YELLOW}══════════════════════════════════════════════════${NC}" for i in 5 4 3 2 1; do echo -ne "\r Waiting... $i " sleep 1 done echo "" echo "" echo -e "${GREEN}>>> NOW PRESS YOUR PTT BUTTON <<<${NC}" echo "" local detected_code=$(timeout 10 evtest "$PTT_DEVICE" 2>/dev/null | grep -m1 "value 1$" | grep -oP 'code \K[0-9]+' || echo "") if [[ -n "$detected_code" && "$detected_code" -gt 0 ]]; then # Map common key codes to friendly names local key_name="Key $detected_code" case "$detected_code" in 1) key_name="Escape" ;; 28) key_name="Enter" ;; 57) key_name="Spacebar" ;; 69) key_name="Num Lock" ;; 113) key_name="Mute" ;; 114) key_name="Volume Down" ;; 115) key_name="Volume Up" ;; 116) key_name="Power" ;; 142) key_name="Sleep" ;; 272) key_name="Left Click" ;; 273) key_name="Right Click" ;; esac print_success "Detected: $key_name (code $detected_code)" echo "" read -p "Use this key? [Y/n]: " use_key if [[ ! "$use_key" =~ ^[Nn]$ ]]; then PTT_KEYCODE="$detected_code" PTT_KEYNAME="$key_name" break fi else print_warn "No button press detected" fi echo "" echo " 1) Try again" echo " 2) Enter key code manually" echo " 3) Cancel" read -p "Select [1]: " retry case "${retry:-1}" in 2) read -p "Enter key code: " PTT_KEYCODE PTT_KEYNAME="Manual" break ;; 3) return 1 ;; esac done # Save configuration mkdir -p "$(dirname "$PTT_CONFIG_FILE")" cat > "$PTT_CONFIG_FILE" << EOF PTT_DEVICE="$PTT_DEVICE" PTT_KEYCODE="$PTT_KEYCODE" PTT_KEYNAME="$PTT_KEYNAME" EOF chmod 600 "$PTT_CONFIG_FILE" # Also save to main config save_config print_success "PTT configured: $PTT_KEYNAME on $(basename $PTT_DEVICE)" # Restart PTT service if client is installed if [[ "$INSTALLED_CLIENT" == "y" && -n "$KIOSK_USER" ]]; then local user_dbus="XDG_RUNTIME_DIR=/run/user/${KIOSK_UID}" sudo -u "$KIOSK_USER" $user_dbus systemctl --user restart kiosk-ptt 2>/dev/null || true fi return 0 } create_ptt_handler() { cat > /usr/local/bin/kiosk-ptt << 'PTTSCRIPT' #!/bin/bash CONFIG="/etc/easy-asterisk/config" PTT_CONFIG="/etc/easy-asterisk/ptt-device" [[ -f "$CONFIG" ]] && source "$CONFIG" [[ -f "$PTT_CONFIG" ]] && source "$PTT_CONFIG" [[ -z "$PTT_DEVICE" ]] && exit 0 # Default: Mute audio source on start pactl set-source-mute @DEFAULT_SOURCE@ 1 # Unmute on press, mute on release evtest --grab "$PTT_DEVICE" 2>/dev/null | while read -r line; do if [[ "$line" =~ "value 1" ]]; then pactl set-source-mute @DEFAULT_SOURCE@ 0 fi if [[ "$line" =~ "value 0" ]]; then pactl set-source-mute @DEFAULT_SOURCE@ 1 fi done PTTSCRIPT chmod +x /usr/local/bin/kiosk-ptt } # ================================================================ # 5. AUDIO DUCKING # ================================================================ configure_audio_ducking() { [[ -z "$KIOSK_USER" ]] && return local wp_dir="/home/${KIOSK_USER}/.config/wireplumber/wireplumber.conf.d" mkdir -p "$wp_dir" cat > "${wp_dir}/50-intercom-ducking.conf" << 'EOF' wireplumber.settings = { linking.allow-moving-streams = true } EOF chown -R ${KIOSK_USER}:${KIOSK_USER} "/home/${KIOSK_USER}/.config" } # ================================================================ # 6. DIAGNOSTICS & FIREWALL # ================================================================ show_port_requirements() { print_header "Port / Firewall Requirements" echo "This server needs traffic to pass from your Clients (Kiosks/Phones)." echo "" echo "Does your Asterisk server have a PUBLIC IP (VPS/Cloud)?" echo " -> YES: You must use 'Forwarding' (DNAT) rules on your router." echo " -> NO: You must use 'Allow/Pass' rules on your VLAN interfaces." echo "" echo "Required Ports:" echo "┌──────────────────┬──────────┬───────────────────────────────┐" echo "│ Port │ Protocol │ Purpose │" echo "├──────────────────┼──────────┼───────────────────────────────┤" echo "│ 5060 │ UDP │ SIP Signaling (Registration) │" echo "│ 5061 │ TCP │ SIP-TLS Signaling (Secure) │" echo "│ 10000-20000 │ UDP │ RTP Media (Audio/Video) │" if [[ "$USE_COTURN" == "y" ]]; then echo "│ ${DEFAULT_TURN_PORT} │ UDP/TCP │ TURN Signaling (Handshake) │" echo "│ 49152-65535 │ UDP │ TURN Relay (Actual Media Path)│" fi echo "└──────────────────┴──────────┴───────────────────────────────┘" echo "" echo "NOTE: VPN Users" echo "If ALL clients and server are on a VPN (Tailscale/Wireguard), you DO NOT" echo "need port forwarding or COTURN. Just bind Asterisk to the VPN IP." } show_firewall_guide() { print_header "Interactive Firewall Guide (Hand-holding Mode)" echo "For: OPNsense, pfSense, or Advanced Routers" echo "" echo "=== SCENARIO A: INTERNAL ONLY (VLAN to VLAN) ===" echo "Example: Kiosks on VLAN 10, Server on VLAN 20" echo "GOAL: Allow Kiosks to talk to Server." echo "" echo "STEP 1: Log in to Router. Go to Firewall > Rules > VLAN 10 Interface." echo " (Do NOT use 'Port Forwarding' for internal VLANs!)" echo "" echo "STEP 2: Create Rule 1 (Signaling)" echo " - Action: Pass (Allow)" echo " - Protocol: UDP/TCP" echo " - Source: VLAN 10 Net" echo " - Dest: ${CURRENT_PUBLIC_IP:-Server_IP}" echo " - Port: 3478" echo "" echo "STEP 3: Create Rule 2 (The Relay Range - CRITICAL)" echo " - Action: Pass (Allow)" echo " - Protocol: UDP" echo " - Source: VLAN 10 Net" echo " - Dest: ${CURRENT_PUBLIC_IP:-Server_IP}" echo " - Port Range:" echo " From: 49152" echo " To: 65535" echo " (Note: Type these numbers in the Start/End boxes)" echo "" echo "================================================" echo "" echo "=== SCENARIO B: EXTERNAL ACCESS (Internet to LAN) ===" echo "Example: Remote phone connecting from a hotel." echo "GOAL: Forward traffic from Internet to Server." echo "" echo "STEP 1: Go to Firewall > NAT > Port Forwarding." echo "STEP 2: Create Rule." echo " - Interface: WAN" echo " - Protocol: UDP" echo " - Dest. Port: 3478 (and 49152-65535)" echo " - Redirect IP: ${CURRENT_PUBLIC_IP:-Server_IP}" echo "" read -p "Press Enter to return..." } show_preflight_check() { print_header "Pre-Flight Requirements Check" echo "Modern browsers (Chrome, Safari, Kiosk Mode) have strict security settings." echo "" echo "1. HTTPS / SSL Certificate (Required for Camera/Mic)" echo " - Browsers block Mic/Cam on 'Insecure Origins' (HTTP)." echo " - Exception: http://localhost is allowed." echo " - Solution: You NEED a domain (FQDN) and SSL Cert (LetsEncrypt)." echo " - Workaround: Use the 'Caddy Cert Sync' option in this script." echo "" echo "2. Static vs Dynamic IP" echo " - If your Public IP changes, COTURN will break." echo " - Solution: Use the 'Update IP manually' or auto-script in the menu." echo "" echo "3. VPN Alternative" echo " - A VPN (Tailscale) negates the need for COTURN and Port Forwarding." echo " - It treats all devices as if they are on the same flat network." echo "" read -p "Press Enter to return..." } test_sip_connectivity() { print_header "SIP Connectivity Test" if systemctl is-active asterisk >/dev/null; then print_success "Asterisk Running" else print_error "Asterisk Down" fi echo "" echo "Listening ports:" ss -ulnp | grep 5060 || echo " UDP 5060: Not listening" ss -tlnp | grep 5061 || echo " TCP 5061: Not listening" if [[ -n "$DOMAIN_NAME" ]]; then echo "" echo "TLS Certificate check:" timeout 5 openssl s_client -connect localhost:5061 -servername "$DOMAIN_NAME" 2>/dev/null | grep "Verify return code" || echo " TLS test failed" fi } verify_cidr_config() { print_header "CIDR Configuration" local my_ip=$(hostname -I | cut -d' ' -f1) echo "Server IP: $my_ip" echo "" echo "Current NAT settings in pjsip.conf:" grep -E "external_|local_net" /etc/asterisk/pjsip.conf 2>/dev/null || echo " No NAT settings found" } watch_live_logs() { print_header "Live Debugging" echo "Enabling PJSIP Logger..." asterisk -rx "module load res_pjsip_logger.so" 2>/dev/null || true asterisk -rx "pjsip set logger on" 2>/dev/null echo "" echo "Options:" echo " 1) Asterisk Console (verbose)" echo " 2) Packet Capture (tcpdump)" read -p "Select [1]: " pcap if [[ "$pcap" == "2" ]]; then echo "Starting tcpdump. Press CTRL+C to stop." tcpdump -i any port 5060 or port 5061 -nn -v else echo "Starting Console. Press CTRL+C to exit." asterisk -rvvv fi asterisk -rx "pjsip set logger off" 2>/dev/null } router_doctor() { print_header "Router Traffic Doctor" if ! systemctl is-active asterisk >/dev/null; then print_error "Asterisk is NOT RUNNING" restart_asterisk_safe return fi print_success "Asterisk is UP" echo "" echo "Server Listening IPs:" ip -o -4 addr show | awk '{print " " $2 ": " $4}' echo "" echo "Instructions:" echo " 1. Take out your phone/laptop" echo " 2. Attempt to REGISTER or CALL" echo " 3. I will listen for 15 seconds" echo "" read -p "Press Enter to start listening..." if timeout 15 tcpdump -i any -c 1 "port 5060 or port 5061" 2>/dev/null; then echo "" print_success "PACKET RECEIVED! Router forwarding is working." else echo "" print_error "NO PACKETS RECEIVED." echo "Your router or firewall is blocking the connection." fi } configure_local_client() { print_header "Configure Local Client" load_config if [[ -z "$KIOSK_USER" ]]; then local default_user="${SUDO_USER:-$USER}" read -p "User [$default_user]: " KIOSK_USER KIOSK_USER="${KIOSK_USER:-$default_user}" KIOSK_UID=$(id -u "$KIOSK_USER" 2>/dev/null) fi if [[ ! -d "/home/${KIOSK_USER}/.baresip" ]]; then print_error "Baresip config not found for $KIOSK_USER" return fi read -p "Extension: " ext read -p "Password: " pass read -p "Server Domain/IP: " server local transport_str="udp" local media_enc="" if [[ "$server" =~ [a-zA-Z] ]]; then print_info "Domain detected. Using TLS." transport_str="tls" media_enc=";mediaenc=srtp" fi echo "" echo "Answer Mode:" echo " 1) Manual (ring on incoming)" echo " 2) Auto (auto-answer)" read -p "Select [1]: " amode local answermode="manual" [[ "$amode" == "2" ]] && answermode="auto" echo "" echo "Enable TURN? (Required if behind NAT/VLAN without VPN)" read -p "Use TURN server? [y/N]: " use_turn local turn_config="" if [[ "$use_turn" =~ ^[Yy]$ ]]; then read -p "TURN User [${TURN_USER}]: " t_user t_user="${t_user:-$TURN_USER}" read -p "TURN Pass [${TURN_PASS}]: " t_pass t_pass="${t_pass:-$TURN_PASS}" local turn_host="${server}" if [[ ! "$turn_host" =~ [a-zA-Z] ]]; then # If server is IP, ask if TURN host is different read -p "TURN Host [${server}]: " th turn_host="${th:-$server}" fi turn_config="turn_server turn:${t_user}:${t_pass}@${turn_host}:3478" fi # Update config file for TURN local conf_file="/home/${KIOSK_USER}/.baresip/config" if [[ -f "$conf_file" ]]; then sed -i '/^turn_server/d' "$conf_file" if [[ -n "$turn_config" ]]; then echo "$turn_config" >> "$conf_file" print_success "TURN configuration added" fi fi cat > "/home/${KIOSK_USER}/.baresip/accounts" << EOF ;auth_pass=${pass};answermode=${answermode}${media_enc} EOF chown ${KIOSK_USER}:${KIOSK_USER} "/home/${KIOSK_USER}/.baresip/accounts" local user_dbus="XDG_RUNTIME_DIR=/run/user/${KIOSK_UID}" sudo -u "${KIOSK_USER}" $user_dbus systemctl --user restart baresip 2>/dev/null print_success "Client Updated & Restarted" } run_client_diagnostics() { print_header "Client Diagnostics" load_config local t_user="${KIOSK_USER:-$SUDO_USER}" t_user="${t_user:-$USER}" local t_uid=$(id -u "$t_user" 2>/dev/null) echo -e "User: ${BOLD}$t_user${NC}" echo "---------------------------------------------------" if sudo -u "$t_user" XDG_RUNTIME_DIR=/run/user/$t_uid systemctl --user is-active baresip >/dev/null 2>&1; then print_success "Baresip RUNNING" else print_error "Baresip STOPPED/FAILED" fi echo "---------------------------------------------------" echo "Network Interface:" grep "^net_interface" "/home/$t_user/.baresip/config" 2>/dev/null || echo " Not set" echo "---------------------------------------------------" echo "Account Config:" cat "/home/$t_user/.baresip/accounts" 2>/dev/null | sed 's/auth_pass=[^;]*/auth_pass=***/' || echo " Not found" echo "---------------------------------------------------" if [[ -n "$ASTERISK_HOST" ]]; then echo -n "Server ($ASTERISK_HOST): " if ping -c 1 -W 2 "$ASTERISK_HOST" >/dev/null 2>&1; then print_success "Reachable" else print_error "Unreachable" fi fi echo "---------------------------------------------------" echo "Last 15 lines of log:" sudo -u "$t_user" journalctl --user -u baresip -n 15 --no-pager 2>/dev/null || echo " No logs" echo "---------------------------------------------------" } run_audio_test() { print_header "Audio Test" echo "Playing test tone..." speaker-test -t sine -f 440 -c 2 -l 1 >/dev/null 2>&1 echo "" read -p "Did you hear audio? [y/N]: " res if [[ "$res" =~ ^[Yy]$ ]]; then print_success "Audio OK" else print_error "Check volume/connections" fi } verify_audio_setup() { print_header "Audio Verification" echo "=== Codecs ===" asterisk -rx "core show codecs" 2>/dev/null | grep -E "(opus|ulaw|alaw|g722)" || echo " N/A" echo "" echo "=== PJSIP Modules ===" asterisk -rx "module show like pjsip" 2>/dev/null | head -10 || echo " N/A" echo "" echo "=== Certificate ===" if [[ -f /etc/asterisk/certs/server.crt ]]; then openssl x509 -in /etc/asterisk/certs/server.crt -noout -subject -dates 2>/dev/null else echo " None" fi } # ================================================================ # 7. ASTERISK CONFIG # ================================================================ fix_asterisk_systemd() { print_info "Configuring systemd..." mkdir -p /etc/systemd/system/asterisk.service.d/ cat > /etc/systemd/system/asterisk.service.d/override.conf << 'SVCEOF' [Unit] Wants=network-online.target After=network-online.target [Service] ExecStart= ExecStart=/usr/sbin/asterisk -f -U asterisk -G asterisk RuntimeDirectory=asterisk RuntimeDirectoryMode=0750 MemoryMax=infinity TasksMax=infinity KillMode=mixed KillSignal=SIGTERM TimeoutStartSec=60 TimeoutStopSec=30 SendSIGKILL=no Restart=always RestartSec=10 Type=simple SVCEOF systemctl daemon-reload } recover_xml_docs() { mkdir -p /var/lib/asterisk/documentation/thirdparty chown -R asterisk:asterisk /var/lib/asterisk/documentation 2>/dev/null } repair_core_configs() { print_info "Repairing configs..." # Copy modules (not symlink - AppArmor blocks symlinks) if [[ -d "/usr/lib/x86_64-linux-gnu/asterisk/modules" ]]; then mkdir -p /usr/lib/asterisk/modules cp -rn /usr/lib/x86_64-linux-gnu/asterisk/modules/* /usr/lib/asterisk/modules/ 2>/dev/null || true fi mkdir -p /etc/asterisk /var/lib/asterisk /var/log/asterisk /var/spool/asterisk /var/run/asterisk recover_xml_docs if [[ ! -f /etc/asterisk/asterisk.conf ]]; then cat > /etc/asterisk/asterisk.conf << EOF [directories] astetcdir => /etc/asterisk astmoddir => /usr/lib/asterisk/modules astvarlibdir => /var/lib/asterisk astdbdir => /var/lib/asterisk astkeydir => /var/lib/asterisk astdatadir => /var/lib/asterisk astagidir => /var/lib/asterisk/agi-bin astspooldir => /var/spool/asterisk astrundir => /var/run/asterisk astlogdir => /var/log/asterisk EOF fi 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 for conf in stasis ari http manager geolocation; do cat > "/etc/asterisk/${conf}.conf" << EOF [general] enabled = no EOF done if [[ ! -f /etc/asterisk/sorcery.conf ]]; then cat > /etc/asterisk/sorcery.conf << EOF [res_pjsip] endpoint=config,pjsip.conf,criteria=type=endpoint auth=config,pjsip.conf,criteria=type=auth aor=config,pjsip.conf,criteria=type=aor transport=config,pjsip.conf,criteria=type=transport EOF fi # ICE and STUN only for FQDN/internet calling load_config local ice_stun_config="" if [[ -n "$DOMAIN_NAME" ]]; then ice_stun_config="icesupport=yes stunaddr=stun.l.google.com:19302" else ice_stun_config="# icesupport disabled - LAN only mode" fi cat > /etc/asterisk/rtp.conf << EOF [general] rtpstart=10000 rtpend=20000 strictrtp=yes ${ice_stun_config} EOF cat > /etc/asterisk/logger.conf << EOF [general] [logfiles] console => notice,warning,error EOF rm -f /var/lib/asterisk/.asterisk_history chown -R asterisk:asterisk /etc/asterisk /var/lib/asterisk /var/log/asterisk /var/spool/asterisk 2>/dev/null || true chown -R asterisk:asterisk /usr/lib/asterisk/modules 2>/dev/null || true } generate_pjsip_conf() { print_info "Generating PJSIP..." load_config local conf_file="/etc/asterisk/pjsip.conf" backup_config "$conf_file" # Prioritize CURRENT_PUBLIC_IP from coturn/updater if available, else detect local public_ip="${CURRENT_PUBLIC_IP}" if [[ -z "$public_ip" ]]; then public_ip=$(curl -s -4 --connect-timeout 5 ifconfig.me 2>/dev/null || echo "") fi local raw_cidr=$(ip -o -f inet addr show | awk '/scope global/ {print $4}' | head -1) local default_cidr="$raw_cidr" if [[ "$raw_cidr" =~ \.([0-9]+)/24$ ]]; then default_cidr="${raw_cidr%.*}.0/24"; fi # Use stored CIDR if available local local_net="${LOCAL_CIDR:-$default_cidr}" local nat_settings="" if [[ -n "$public_ip" && -n "$DOMAIN_NAME" ]]; then nat_settings="external_media_address=$public_ip external_signaling_address=$public_ip local_net=$local_net" print_info "NAT: Public IP=$public_ip, Local=$local_net" fi cat > "$conf_file" << EOF ; Easy Asterisk v1.23 [global] type=global user_agent=EasyAsterisk [transport-udp] type=transport protocol=udp bind=0.0.0.0:${DEFAULT_SIP_PORT} ${nat_settings} [transport-tcp] type=transport protocol=tcp bind=0.0.0.0:${DEFAULT_SIP_PORT} ${nat_settings} [transport-tls] type=transport protocol=tls bind=0.0.0.0:${DEFAULT_SIPS_PORT} 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 local backup_file=$(ls -t "${conf_file}.backup-"* 2>/dev/null | head -1) if [[ -f "$backup_file" ]]; then awk '/^; === Device:/{flag=1} flag' "$backup_file" >> "$conf_file" print_success "Restored devices from backup" fi chown asterisk:asterisk "$conf_file" } rebuild_dialplan() { local quiet=$1 [[ "$quiet" != "quiet" ]] && print_info "Rebuilding dialplan..." local conf_file="/etc/asterisk/extensions.conf" backup_config "$conf_file" cat > "$conf_file" << EOF [general] static=yes writeprotect=no [default] exten => _X.,1,Hangup() [intercom] EOF local dev_name="" dev_cat="" dev_auto="" dev_aa_override="" while IFS= read -r line; do if [[ "$line" == *"; === Device:"* ]]; then dev_aa_override="" local temp="${line#*; === Device: }" temp="${temp% ===}" if [[ "$temp" == *"[AA:yes]"* ]]; then dev_aa_override="yes"; temp="${temp% [AA:yes]}" elif [[ "$temp" == *"[AA:no]"* ]]; then dev_aa_override="no"; temp="${temp% [AA:no]}" fi dev_cat="${temp##* (}"; dev_cat="${dev_cat%)}" dev_name="${temp% (*)}" dev_auto="no" local cat_data=$(grep "^${dev_cat}|" "$CATEGORIES_FILE" 2>/dev/null || true) if [[ -n "$cat_data" ]]; then local is_auto=$(echo "$cat_data" | cut -d'|' -f3) [[ "$is_auto" == "yes" ]] && dev_auto="yes" fi [[ "$dev_aa_override" == "yes" ]] && dev_auto="yes" [[ "$dev_aa_override" == "no" ]] && dev_auto="no" fi if [[ "$line" =~ ^\[([0-9]+)\] ]]; then local ext="${BASH_REMATCH[1]}" if [[ -n "$dev_name" ]]; then if [[ "$dev_auto" == "yes" ]]; then cat >> "$conf_file" << EOF exten => ${ext},1,NoOp(Auto-Answer ${ext}) same => n,Set(PJSIP_HEADER(add,Call-Info)=\;answer-after=0) same => n,Set(PJSIP_HEADER(add,Alert-Info)=auto-answer) same => n,Dial(PJSIP/${ext},60) same => n,Hangup() EOF else cat >> "$conf_file" << EOF exten => ${ext},1,NoOp(Call ${ext}) same => n,Dial(PJSIP/${ext},60) same => n,Hangup() EOF fi dev_name="" fi fi done < /etc/asterisk/pjsip.conf # Add rooms if [[ -f "$ROOMS_FILE" ]]; then while IFS='|' read -r rext rname rmem rtime rtype; do [[ "$rext" =~ ^# ]] && continue [[ -z "$rext" ]] && continue local dial_list="" IFS=',' read -ra EXTS <<< "$rmem" for ext in "${EXTS[@]}"; do ext=$(echo "$ext" | tr -d ' ') [[ -n "$dial_list" ]] && dial_list="${dial_list}&" dial_list="${dial_list}PJSIP/${ext}" done if [[ "$rtype" == "page" ]]; then cat >> "$conf_file" << EOF ; Room: ${rname} (Page) exten => ${rext},1,NoOp(Page ${rname}) same => n,Set(PJSIP_HEADER(add,Call-Info)=\;answer-after=0) same => n,Page(${dial_list},i,${rtime}) same => n,Hangup() EOF else cat >> "$conf_file" << EOF ; Room: ${rname} (Ring) exten => ${rext},1,NoOp(Call ${rname}) same => n,Dial(${dial_list},${rtime}) same => n,Hangup() EOF fi done < "$ROOMS_FILE" fi chown -R asterisk:asterisk /etc/asterisk asterisk -rx "dialplan reload" &>/dev/null || true } configure_asterisk() { if ! id asterisk >/dev/null 2>&1; then useradd -r -s /bin/false -d /var/lib/asterisk asterisk 2>/dev/null || true fi print_info "Configuring Asterisk..." fix_asterisk_systemd initialize_default_categories repair_core_configs mkdir -p /etc/asterisk/certs if [[ ! -f /etc/asterisk/certs/server.crt ]]; then 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 fi chown asterisk:asterisk /etc/asterisk/certs/server.* 2>/dev/null || true chmod 644 /etc/asterisk/certs/server.crt 2>/dev/null || true chmod 600 /etc/asterisk/certs/server.key 2>/dev/null || true generate_pjsip_conf 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 fi } # ================================================================ # 8. CLIENT CONFIG # ================================================================ configure_baresip() { local baresip_dir="/home/${KIOSK_USER}/.baresip" mkdir -p "$baresip_dir" # Detect network interface local found_iface="" for target in 8.8.8.8 1.1.1.1 9.9.9.9; do local iface=$(ip route get "$target" 2>/dev/null | awk '{for(i=1;i<=NF;i++) if($i=="dev") print $(i+1)}' | head -1) if [[ -n "$iface" ]]; then found_iface="$iface" print_success "Network interface: $found_iface" break fi done cat > "${baresip_dir}/config" << EOF poll_method epoll audio_player pulse audio_source pulse audio_alert pulse sip_autoanswer yes sip_cafile /etc/ssl/certs/ca-certificates.crt rtp_timeout 0 net_af ipv4 module_path /usr/lib/baresip/modules module srtp.so module stdio.so module pulse.so module g711.so module opus.so module account.so module stun.so module ice.so module turn.so EOF [[ -n "$found_iface" ]] && echo "net_interface $found_iface" >> "${baresip_dir}/config" local transport="udp" local mediaenc="" if [[ "$ENABLE_TLS" == "y" ]]; then transport="tls" mediaenc=";mediaenc=srtp" fi local amode="${CLIENT_ANSWERMODE:-auto}" cat > "${baresip_dir}/accounts" << EOF ;auth_pass=${SIP_PASSWORD};answermode=${amode}${mediaenc} EOF chown -R ${KIOSK_USER}:${KIOSK_USER} "$baresip_dir" chmod 700 "$baresip_dir" configure_audio_ducking create_ptt_handler create_baresip_launcher } create_baresip_launcher() { local launcher_user="${KIOSK_USER}" cat > /usr/local/bin/easy-asterisk-launcher << LAUNCHER #!/bin/bash CONFIG_FILE="/home/${launcher_user}/.baresip/config" TARGETS=("8.8.8.8" "1.1.1.1" "9.9.9.9") FOUND_IFACE="" for i in {1..6}; do for target in "\${TARGETS[@]}"; do IFACE=\$(ip route get "\$target" 2>/dev/null | awk '{for(i=1;i<=NF;i++) if(\$i=="dev") print \$(i+1)}' | head -1) if [[ -n "\$IFACE" ]]; then FOUND_IFACE="\$IFACE" break 2 fi done sleep 5 done if [[ -f "\$CONFIG_FILE" && -n "\$FOUND_IFACE" ]]; then sed -i '/^#*net_interface/d' "\$CONFIG_FILE" echo "net_interface \${FOUND_IFACE}" >> "\$CONFIG_FILE" fi exec /usr/bin/baresip -f "/home/${launcher_user}/.baresip" LAUNCHER chmod +x /usr/local/bin/easy-asterisk-launcher } enable_client_services() { local systemd_dir="/home/${KIOSK_USER}/.config/systemd/user" mkdir -p "$systemd_dir" # Baresip service cat > "${systemd_dir}/baresip.service" << EOF [Unit] Description=Baresip SIP Client After=pipewire.service network-online.target Wants=network-online.target pipewire.service [Service] Type=simple ExecStart=/usr/local/bin/easy-asterisk-launcher Restart=always RestartSec=5 Environment=XDG_RUNTIME_DIR=/run/user/${KIOSK_UID} [Install] WantedBy=default.target EOF # PTT service cat > "${systemd_dir}/kiosk-ptt.service" << EOF [Unit] Description=PTT Button Handler After=pipewire.service [Service] Type=simple ExecStartPre=/bin/sleep 3 ExecStart=/usr/local/bin/kiosk-ptt Restart=always RestartSec=5 Environment=XDG_RUNTIME_DIR=/run/user/${KIOSK_UID} [Install] WantedBy=default.target EOF chown -R ${KIOSK_USER}:${KIOSK_USER} "/home/${KIOSK_USER}/.config" if [[ -n "$KIOSK_USER" ]]; then loginctl enable-linger $KIOSK_USER 2>/dev/null || true local user_dbus="XDG_RUNTIME_DIR=/run/user/${KIOSK_UID}" sudo -u "$KIOSK_USER" $user_dbus systemctl --user daemon-reload sudo -u "$KIOSK_USER" $user_dbus systemctl --user enable baresip kiosk-ptt sudo -u "$KIOSK_USER" $user_dbus systemctl --user restart baresip kiosk-ptt fi } # ================================================================ # 9. CERTIFICATE HANDLING # ================================================================ check_cert_coverage() { local cert_file=$1 target_domain=$2 base_domain=$3 [[ ! -f "$cert_file" ]] && return 1 local sans=$(openssl x509 -in "$cert_file" -text -noout 2>/dev/null | grep -A1 "Subject Alternative Name" | tail -1) echo "$sans" | grep -q "DNS:${target_domain}" && return 0 echo "$sans" | grep -q "DNS:\*.${base_domain}" && return 0 return 1 } setup_caddy_cert_sync() { local mode=$1 [[ "$mode" == "force" ]] && print_header "Caddy Cert Sync" load_config local domain=${DOMAIN_NAME:-sip.example.com} if [[ "$mode" == "force" ]]; then read -p "Domain [$domain]: " input_domain domain="${input_domain:-$domain}" fi local actual_user="${SUDO_USER:-$USER}" local actual_home=$(eval echo ~"$actual_user") local base_domain=$(echo "$domain" | awk -F. '{print $(NF-1)"."$NF}') local search_paths=( "${actual_home}/docker/caddy/ssl" "${actual_home}/docker/caddy/caddy_data" "${actual_home}/docker/caddy/caddy_data/caddy/certificates/acme-v02.api.letsencrypt.org-directory" "/var/lib/caddy" "/var/lib/caddy/.local/share/caddy/certificates/acme-v02.api.letsencrypt.org-directory" "/data/caddy" "/root/.local/share/caddy/certificates" ) local caddy_cert="" caddy_key="" [[ "$mode" == "force" ]] && echo "Searching for certificates..." for base_path in "${search_paths[@]}"; do if ! sudo test -d "$base_path" 2>/dev/null; then continue; fi [[ "$mode" == "force" ]] && echo " Checking: $base_path" local candidates=$(sudo find "$base_path" -maxdepth 5 -type f \( -name "fullchain.pem" -o -name "*.crt" \) 2>/dev/null) for cert in $candidates; do sudo cp "$cert" /tmp/cert_check.pem 2>/dev/null || continue if check_cert_coverage "/tmp/cert_check.pem" "$domain" "$base_domain"; then [[ "$mode" == "force" ]] && print_success "Found matching cert: $cert" caddy_cert="$cert" local dir=$(dirname "$cert") local name=$(basename "$cert") if [[ "$name" == "fullchain.pem" ]]; then caddy_key="${dir}/privkey.pem" else caddy_key=$(echo "$cert" | sed 's/\.crt/\.key/') fi if sudo test -f "$caddy_key"; then rm -f /tmp/cert_check.pem break 2 fi fi rm -f /tmp/cert_check.pem done done if [[ -n "$caddy_cert" && -n "$caddy_key" ]]; then mkdir -p /etc/asterisk/certs sudo cat "$caddy_cert" > /etc/asterisk/certs/server.crt sudo cat "$caddy_key" > /etc/asterisk/certs/server.key chown asterisk:asterisk /etc/asterisk/certs/server.* chmod 644 /etc/asterisk/certs/server.crt chmod 600 /etc/asterisk/certs/server.key DOMAIN_NAME="$domain" ENABLE_TLS="y" ASTERISK_HOST="$domain" save_config generate_pjsip_conf restart_asterisk_safe [[ "$mode" == "force" ]] && print_success "Certificates installed for $domain" return 0 else [[ "$mode" == "force" ]] && print_warn "No matching certificates found" return 1 fi } setup_internet_access() { print_header "Setup Internet Access" echo "Select Certificate Source:" echo " 1) Auto-Sync from Caddy (Docker/Native)" echo " 2) Standalone Certbot (Requires Port 80 open)" echo " 3) Self-Signed (Internal testing only)" echo " 4) Manual Path" echo " 0) Cancel" read -p "Select: " cert_opt [[ "$cert_opt" == "0" ]] && return # Show port requirements show_preflight_check show_port_requirements echo "" read -p "Continue? [Y/n]: " cont [[ "$cont" =~ ^[Nn]$ ]] && return load_config read -p "FQDN [${DOMAIN_NAME:-sip.example.com}]: " fqdn DOMAIN_NAME="${fqdn:-${DOMAIN_NAME:-sip.example.com}}" ASTERISK_HOST="$DOMAIN_NAME" echo "" echo "Do you have a separate domain for TURN? (e.g., turn.example.com)" read -p "Enter TURN domain (leave empty to use $DOMAIN_NAME): " t_dom TURN_DOMAIN="${t_dom:-$DOMAIN_NAME}" # CIDR Prompt echo "" print_header "Local Network CIDR" local raw_cidr=$(ip -o -f inet addr show | awk '/scope global/ {print $4}' | head -1) local default_cidr="$raw_cidr" if [[ "$raw_cidr" =~ \.([0-9]+)/24$ ]]; then default_cidr="${raw_cidr%.*}.0/24"; fi echo "This helps Asterisk distinguish local vs external traffic." read -p "Local network CIDR [$default_cidr]: " local_net LOCAL_CIDR="${local_net:-$default_cidr}" save_config case "$cert_opt" in 1) # Caddy # Show Caddy Helper text echo "---------------------------------------------------------" echo "CADDY HELPER: Ensure these are in your Caddyfile to get certs:" echo "" echo "${DOMAIN_NAME} {" echo " respond \"Asterisk Cert Placeholder\" 200" echo "}" if [[ "$TURN_DOMAIN" != "$DOMAIN_NAME" ]]; then echo "" echo "${TURN_DOMAIN} {" echo " respond \"TURN Cert Placeholder\" 200" echo "}" fi echo "" echo "Restart Caddy, wait 30s, then press Enter." echo "---------------------------------------------------------" read -p "Press Enter to sync..." if setup_caddy_cert_sync "auto"; then print_success "Setup complete using Caddy certificates!" else print_error "Caddy sync failed. Ensure Caddy is running." return fi ;; 2) # Certbot print_info "Installing Certbot..." apt install -y certbot certbot certonly --standalone -d "$DOMAIN_NAME" --non-interactive --agree-tos --register-unsafely-without-email if [[ -f "/etc/letsencrypt/live/$DOMAIN_NAME/fullchain.pem" ]]; then mkdir -p /etc/asterisk/certs cat "/etc/letsencrypt/live/$DOMAIN_NAME/fullchain.pem" > /etc/asterisk/certs/server.crt cat "/etc/letsencrypt/live/$DOMAIN_NAME/privkey.pem" > /etc/asterisk/certs/server.key chown asterisk:asterisk /etc/asterisk/certs/server.* print_success "Certbot Success" else print_error "Certbot failed" return fi ;; 3) # Self-Signed 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=$DOMAIN_NAME" 2>/dev/null chown asterisk:asterisk /etc/asterisk/certs/server.* chmod 644 /etc/asterisk/certs/server.crt chmod 600 /etc/asterisk/certs/server.key print_success "Self-signed certificate generated" print_warn "Clients will need to trust this certificate" ;; 4) # Manual read -p "Certificate Path: " cp read -p "Private Key Path: " kp if [[ -f "$cp" && -f "$kp" ]]; then mkdir -p /etc/asterisk/certs cat "$cp" > /etc/asterisk/certs/server.crt cat "$kp" > /etc/asterisk/certs/server.key chown asterisk:asterisk /etc/asterisk/certs/server.* print_success "Certificates installed" else print_error "Files not found!" return fi ;; esac ENABLE_TLS="y" save_config generate_pjsip_conf restart_asterisk_safe print_success "Internet access configuration complete" } # ================================================================ # 10. INSTALLATION # ================================================================ install_full() { 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 INSTALLED_SERVER="y" INSTALLED_CLIENT="y" configure_asterisk configure_baresip enable_client_services open_firewall_ports save_config echo "" echo "════════════════════════════════════════════════════════" print_success "Local network install complete" echo "" echo "Server and devices are reachable over internal LAN network only." echo "To add internet calling capability, continue with the setup below." echo "════════════════════════════════════════════════════════" echo "" read -p "Run Internet/Certificate Setup wizard now? [Y/n]: " run_setup [[ ! "$run_setup" =~ ^[Nn]$ ]] && setup_internet_access print_success "Installation complete" } install_server_only() { print_header "Server Installation" ASTERISK_HOST="127.0.0.1" ENABLE_TLS="y" install_asterisk_packages configure_asterisk open_firewall_ports INSTALLED_SERVER="y" save_config echo "" echo "════════════════════════════════════════════════════════" print_success "Local network install complete" echo "" echo "Server and devices are reachable over internal LAN network only." echo "To add internet calling capability, continue with the setup below." echo "════════════════════════════════════════════════════════" echo "" read -p "Run Internet/Certificate Setup wizard now? [Y/n]: " run_setup [[ ! "$run_setup" =~ ^[Nn]$ ]] && setup_internet_access print_success "Server installed" } install_client_only() { print_header "Client Installation" local default_user="${SUDO_USER:-$USER}" read -p "User [$default_user]: " target_user KIOSK_USER="${target_user:-$default_user}" KIOSK_UID=$(id -u "$KIOSK_USER") if ! id -nG "$KIOSK_USER" | grep -qw "audio"; then usermod -aG audio "$KIOSK_USER" fi read -p "Server (IP or domain): " ASTERISK_HOST read -p "SIP Password: " SIP_PASSWORD if [[ "$ASTERISK_HOST" =~ [a-zA-Z] ]]; then ENABLE_TLS="y" else ENABLE_TLS="n" fi echo "" echo "Answer Mode:" echo " 1) Auto (auto-answer incoming calls)" echo " 2) Manual (ring on incoming)" read -p "Select [1]: " aa_sel CLIENT_ANSWERMODE="auto" [[ "$aa_sel" == "2" ]] && CLIENT_ANSWERMODE="manual" collect_client_config install_baresip_packages INSTALLED_CLIENT="y" configure_baresip enable_client_services save_config print_success "Client installed" } collect_common_config() { SIP_PASSWORD="${SIP_PASSWORD:-$(generate_password)}" ASTERISK_HOST="127.0.0.1" return 0 } collect_client_config() { read -p "Extension [101]: " KIOSK_EXTENSION KIOSK_EXTENSION="${KIOSK_EXTENSION:-101}" KIOSK_NAME="kiosk-${KIOSK_EXTENSION}" } install_dependencies() { install_asterisk_packages install_baresip_packages } install_asterisk_packages() { echo "exit 101" > /usr/sbin/policy-rc.d chmod +x /usr/sbin/policy-rc.d apt update # asterisk-opus removed (included in asterisk-modules on Ubuntu 24.04+) apt install -y asterisk asterisk-core-sounds-en-gsm asterisk-modules openssl curl tcpdump sngrep || true mkdir -p /var/lib/asterisk /var/log/asterisk /var/spool/asterisk /var/run/asterisk ldconfig update-ca-certificates 2>/dev/null || true rm -f /usr/sbin/policy-rc.d fix_asterisk_systemd } install_baresip_packages() { apt update apt install -y baresip baresip-core pipewire pipewire-alsa pipewire-pulse wireplumber alsa-utils evtest || true } uninstall_menu() { print_header "Uninstall" echo " 1) Remove Everything" echo " 2) Asterisk Only" echo " 3) Baresip Only" echo " 0) Cancel" read -p "Select: " ch case $ch in 1) systemctl stop asterisk 2>/dev/null || true apt purge -y asterisk* baresip baresip-core 2>/dev/null || true rm -rf /etc/asterisk /var/lib/asterisk /var/log/asterisk /var/spool/asterisk /usr/lib/asterisk rm -rf /etc/systemd/system/asterisk.service.d /etc/easy-asterisk [[ -n "$KIOSK_USER" ]] && rm -rf "/home/${KIOSK_USER}/.baresip" systemctl daemon-reload INSTALLED_SERVER="n" INSTALLED_CLIENT="n" rm -f "$CONFIG_FILE" print_success "Removed all" ;; 2) systemctl stop asterisk 2>/dev/null || true apt purge -y asterisk* 2>/dev/null || true rm -rf /etc/asterisk /var/lib/asterisk INSTALLED_SERVER="n" save_config print_success "Removed Asterisk" ;; 3) apt purge -y baresip baresip-core 2>/dev/null || true [[ -n "$KIOSK_USER" ]] && rm -rf "/home/${KIOSK_USER}/.baresip" INSTALLED_CLIENT="n" save_config print_success "Removed Baresip" ;; esac } # ================================================================ # 11. MENU SYSTEM (Reordered: Server #2, Devices #3) # ================================================================ show_main_menu() { clear print_header "Easy Asterisk v1.23" 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}" [[ "$INSTALLED_COTURN" == "y" ]] && echo -e " COTURN: ${GREEN}Installed${NC}" || echo -e " COTURN: ${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" echo "" read -p " Select: " choice [[ "$choice" == "0" ]] && exit 0 local action=${menu_map[$choice]} [[ -n "$action" ]] && $action show_main_menu } submenu_install() { clear print_header "Install" echo " 1) Full (server + client)" echo " 2) Server only" echo " 3) Client only" echo " 4) Uninstall" echo " 0) Back" read -p " Select: " choice case $choice in 1) install_full; read -p "Press Enter..." ;; 2) install_server_only; read -p "Press Enter..." ;; 3) install_client_only; read -p "Press Enter..." ;; 4) uninstall_menu; read -p "Press Enter..." ;; esac } submenu_server() { clear print_header "Server Settings" echo " 1) Setup Internet Access (TLS/Certs/NAT)" echo " 2) Force re-sync Caddy certs" echo " 3) Show port/firewall requirements" echo " 4) Interactive Firewall Guide (OPNsense/pfSense)" echo " 5) Test SIP connectivity" echo " 6) Verify CIDR/NAT config" echo " 7) Watch Live Logs" echo " 8) Router Doctor" echo " 9) Configure TURN Server (COTURN)" echo " 0) Back" read -p " Select: " choice case $choice in 1) setup_internet_access ;; 2) setup_caddy_cert_sync "force" ;; 3) show_port_requirements ;; 4) show_firewall_guide ;; 5) test_sip_connectivity ;; 6) verify_cidr_config ;; 7) watch_live_logs ;; 8) router_doctor ;; 9) configure_coturn_menu ;; 0) return ;; esac [[ "$choice" != "0" ]] && read -p "Press Enter..." [[ "$choice" != "0" ]] && submenu_server } submenu_devices() { clear print_header "Device Management" echo " 1) Add device" echo " 2) Remove device" echo " 3) List devices" echo " 4) Manage categories" echo " 5) Manage rooms" echo " 0) Back" read -p " Select: " choice case $choice in 1) add_device_menu ;; 2) remove_device ;; 3) show_registered_devices ;; 4) manage_categories ;; 5) manage_rooms ;; 0) return ;; esac [[ "$choice" != "0" ]] && read -p "Press Enter..." [[ "$choice" != "0" ]] && submenu_devices } submenu_client() { clear print_header "Client Settings" echo " 1) Configure Local Client" echo " 2) Configure PTT Button" echo " 3) Run Diagnostics" echo " 0) Back" read -p " Select: " choice case $choice in 1) configure_local_client ;; 2) configure_ptt_menu ;; 3) run_client_diagnostics ;; 0) return ;; esac [[ "$choice" != "0" ]] && read -p "Press Enter..." [[ "$choice" != "0" ]] && submenu_client } submenu_tools() { clear print_header "Tools" echo " 1) Audio Test" echo " 2) Verify Audio/Codec Setup" echo " 0) Back" read -p " Select: " choice case $choice in 1) run_audio_test ;; 2) verify_audio_setup ;; 0) return ;; esac [[ "$choice" != "0" ]] && read -p "Press Enter..." [[ "$choice" != "0" ]] && submenu_tools } main() { check_root load_config show_main_menu } main "$@"