The Digium precompiled codec_opus.so is ABI-incompatible with Ubuntu
24.04's Asterisk package build, causing "Module initialization failed.
ASTERISK EXITING!" on startup. The format_ogg_opus.so also conflicts
with the one already in asterisk-modules.
Opus pass-through (phone-to-phone when both support Opus) still works
via res_format_attr_opus.so from asterisk-modules. Only Opus<->ulaw
transcoding is unavailable, which is rarely needed since modern SIP
phones support the same codecs natively.
Changes:
- Remove Digium codec download from Dockerfile
- Add noload for codec_opus.so and format_ogg_opus.so in modules.conf
- Add startup cleanup to remove stale Digium .so files from Docker volume
https://claude.ai/code/session_01Vm6NLaQuzM4VosAotqS1q8
- Install Digium precompiled codec_opus.so in Docker image since Ubuntu
24.04's asterisk-modules package doesn't include it (bug #2044135).
Opus is the best codec for mobile VoIP (adaptive bitrate, packet loss
resilience).
- Rework entrypoint to print the management info banner AFTER Asterisk
finishes loading, so the CLI command isn't buried in startup log noise.
The management command is now highlighted in yellow.
- Remove explicit load directive for codec_opus.so from modules.conf;
autoload=yes handles it when the .so exists, and silently skips when
it doesn't (no more error log noise on systems without Opus).
https://claude.ai/code/session_01Vm6NLaQuzM4VosAotqS1q8
Three fixes:
- Always regenerate modules.conf on startup to ensure chan_sip stays disabled.
Previously it was only written if missing, so a persisted Docker volume with
an old modules.conf would load chan_sip, causing all pjsip registrations to
fail with "Wrong password".
- Add pjsip.conf sanitization on startup to remove endpoint-only options
(like direct_media) that end up in aor sections after a corrupted import.
- Fix dialplan rebuild to skip room extensions that conflict with device
extensions, preventing duplicate extension registration warnings.
- Fix import merge to not write orphaned device comment headers for skipped
(conflicting) devices.
https://claude.ai/code/session_01Vm6NLaQuzM4VosAotqS1q8
UniFi controller uses UDP 3478 for STUN, preventing coturn from
binding. Add TURN_PORT variable (default 3478) so users can set
TURN_PORT=3479 in .env to avoid the conflict.
https://claude.ai/code/session_01Vm6NLaQuzM4VosAotqS1q8
The coturn/coturn:latest image sets USER nobody:nogroup. On startup,
turnserver tries to create /var/run/turnserver.pid which nobody can't
write to, causing an immediate crash (the 2.2s "Error" in compose).
Fix: add user: root to the coturn service. Since we already use
network_mode: host, the container has broad host access anyway.
Also adds DETECT_EXTERNAL_IP=yes which is the image's native way
to auto-detect and set --external-ip, replacing our shell script.
https://claude.ai/code/session_01Vm6NLaQuzM4VosAotqS1q8
The coturn/coturn image has its own docker-entrypoint.sh that handles
arg parsing. Overriding it with /bin/sh -c adds a failure point.
Now passing args as a clean list directly to the native entrypoint.
Also drops --external-ip for now (added back once coturn starts).
https://claude.ai/code/session_01Vm6NLaQuzM4VosAotqS1q8
Three changes to fix coturn failing to start:
1. Use environment vars instead of mixing Compose interpolation
(${VAR}) with shell escaping ($$ARGS) in the command - the
old approach was fragile and could produce broken shell scripts
2. Remove --no-loopback-peers - since Asterisk runs on the same
host (network_mode: host), coturn must allow relay to localhost
otherwise it refuses to relay media to Asterisk
3. Health check: redirect output to /dev/null to avoid noise
https://claude.ai/code/session_01Vm6NLaQuzM4VosAotqS1q8
The coturn/coturn Alpine image doesn't include ss or netstat.
Use turnutils_stunclient (included in the coturn image) which
actually verifies the STUN server is responding, not just that
a port is open. Also add start_period to avoid false failures
during startup.
https://claude.ai/code/session_01Vm6NLaQuzM4VosAotqS1q8
The coturn/coturn Docker image is Alpine-based and doesn't include
iproute2 (which provides ss). This caused the healthcheck to fail
immediately, marking coturn as unhealthy and preventing the asterisk
container from starting due to its service_healthy dependency.
Alpine's busybox includes netstat, so use that instead.
https://claude.ai/code/session_01Vm6NLaQuzM4VosAotqS1q8
Problem: Calls via FQDN work "sometimes" because STUN-only mode fails
behind strict NAT (cellular, Proton VPN, hotel WiFi, corporate firewalls).
STUN tells clients their public IP, but can't relay media when direct
UDP paths are blocked. TURN relays media as a fallback.
Changes:
coturn (docker-compose.yml):
- Upgraded from STUN-only to full STUN+TURN relay
- Uses long-term credential mechanism (--lt-cred-mech)
- Credentials shared between coturn and Asterisk automatically
- Relay port range 49152-49252 (configurable, ~50 concurrent relayed calls)
- Always-on (removed --profile stun gate)
- Conditional --external-ip (only set when PUBLIC_IP is provided)
Entrypoint (docker/entrypoint.sh):
- Auto-detects public IP (ifconfig.me → icanhazip.com → api.ipify.org)
- Auto-generates TURN password on first startup (saved to config)
- Configures rtp.conf with icesupport + stunaddr + turnaddr + credentials
- Updates pjsip.conf external_*_address if public IP changes
- Always enables ICE, STUN, and TURN for Docker deployments
Main script (easy-asterisk-v0.10.0.sh):
- Added TURN_ENABLED, TURN_SERVER, TURN_USERNAME, TURN_PASSWORD to
load_config/save_config
- repair_core_configs: rtp.conf now includes turnaddr/turnusername/turnpassword
when TURN is enabled
- Bash device creation: Docker mode defaults to FQDN (TLS) for all new devices
- Python device creation: reads TURN_ENABLED, auto-selects FQDN in Docker
- Main menu shows TURN status
.env.example:
- Comprehensive documentation for every setting
- DOMAIN_NAME is the only required setting
- Port forwarding requirements clearly listed
- TURN credentials and relay port range documented
The result: `docker compose up -d` gives you a fully working PBX where
any SIP client on any network can connect reliably via FQDN:5061.
https://claude.ai/code/session_01Vm6NLaQuzM4VosAotqS1q8
Complete Docker-first refactor of the 6,800-line management script:
Core Architecture:
- Added is_docker() detection (/.dockerenv + /proc/1/cgroup check)
- Added asterisk_running() helper replacing all systemctl is-active calls
- Moved restart_asterisk_safe() to top-level with Docker/bare-metal branches
- Added webadmin_running(), start_webadmin(), stop_webadmin(), restart_webadmin()
for process-based web admin management (replaces systemd service)
Functions Refactored (Docker-aware):
- fix_asterisk_systemd(): no-op in Docker (no systemd)
- install_asterisk_packages(): skips apt in Docker (pre-installed)
- install_baresip_packages(): skips in Docker (no local audio client)
- open_firewall_ports(): skips ufw in Docker (host responsibility)
- configure_asterisk(): skips systemctl enable in Docker
- enable_client_services(): skips entirely in Docker (no kiosk client)
- configure_baresip(): skips entirely in Docker
- configure_local_client(): shows error with guidance to use mobile clients
- run_client_diagnostics(): redirects to vpn-diagnostics
- fix_audio_manually(): not available in Docker (no audio hardware)
- uninstall_menu(): shows Docker-specific reset options
- manual_update_asterisk(): shows Docker rebuild instructions
- create_web_admin_service(): no-op in Docker (process-managed)
- web_admin_menu(): uses start/stop/restart_webadmin() instead of systemctl
Menu System:
- show_main_menu(): Docker-specific status display (Asterisk, Web Admin,
VPN ICE status) with streamlined menu (no Client Settings option)
- submenu_install(): Docker shows Configure/Reset instead of Install/Uninstall
- submenu_tools(): Docker shows Room Directory, Update, VPN Diagnostics,
DNS Whitelist (hides audio tools that need hardware)
Entrypoint:
- Proper signal trapping (SIGTERM/SIGINT) for clean shutdown
- Generates all Asterisk configs with STUN/ICE support from env vars
- Creates default device categories on first run
- Starts web admin as background process with env-based config
Dockerfile:
- Added lsof dependency (needed for port management)
- Added STUN port 3478 exposure
- Ensured /.dockerenv marker exists
Backward compatible: bare-metal installs work exactly as before.
https://claude.ai/code/session_01Vm6NLaQuzM4VosAotqS1q8
- Dockerfile: Containerized Asterisk PBX with Ubuntu 24.04 base,
all dependencies pre-installed, health checks, and volume persistence
- docker-compose.yml: Asterisk service with host networking (required for
RTP port range) + optional self-hosted coturn STUN server via --profile stun
- docker/entrypoint.sh: Auto-generates configs, certs, and starts
Asterisk in foreground with web admin in background
- scripts/vpn-diagnostics.sh: Detects VPN interfaces, checks PJSIP
transport config, tests STUN reachability, analyzes NAT type, and
provides STUN/TURN recommendations for third-party VPNs
- scripts/dns-whitelist.sh: Documents all domains needed per network mode
(LAN/VPN vs FQDN), per component (server, Sipnetic, Linphone), with
--check mode to test DNS resolution and reachability
- easy-asterisk script: Added VPN STUN/ICE menu (option 12 in Server
Settings) with self-hosted coturn, Google STUN, or custom STUN server
options. LAN/VPN devices now get ice_support=yes when VPN ICE is
enabled. Web admin Python code also respects VPN_ICE_ENABLED config.
Self-hosted coturn in STUN-only mode eliminates all external DNS
dependencies - everything operates by IP address, ideal for
DNS-filtered environments.
https://claude.ai/code/session_01Vm6NLaQuzM4VosAotqS1q8
- Add local_net entries to PJSIP transports in LAN/VPN mode when
VLAN/VPN subnets are configured, so Asterisk recognizes VPN traffic
as local and mobile devices no longer appear offline on VPN
- Auto-detect VPN interfaces (Tailscale, WireGuard, OpenVPN) and
suggest their subnets in the VLAN/VPN configuration menu
- Add rtp_keepalive=15, rtp_timeout=120, and rtp_timeout_hold=120
to mobile device endpoints to maintain NAT mappings during
WiFi-to-cellular transitions (fixes one-way audio)
- Reduce qualify_frequency from 60s to 30s for faster re-registration
detection when mobile devices change networks
- Update README with VPN troubleshooting and one-way audio guidance
including client-side fixes for Sipnetic and Linphone
https://claude.ai/code/session_01Vm6NLaQuzM4VosAotqS1q8
Stop command improvements:
- Shows actual netstat and systemctl commands being run
- Displays output from each command
- Runs netstat checks between stop attempts
- More transparent debugging
UI improvements:
- Consistent 100px width for action selects and buttons
- Sort dropdown visible in devices header
New features:
- Rename button for rooms (uses prompt dialog)
- Rename button for categories (uses prompt dialog)
- PUT /api/rooms/{ext} endpoint for renaming
- PUT /api/categories/{id} endpoint for renaming
- Python functions: rename_room(), rename_category()
- Add sort dropdown: Extension, Name, Category, Status (Online first), Transport
- Consistent width for action selects (95px) and buttons (65px)
- Extract renderDevices() function for re-sorting without API calls
- Store statusCache globally for sort comparisons
Stop command fix:
- Use double-stop pattern (stop, check port, stop again if needed)
- Print commands being run for debugging
Web Admin UI improvements:
- Sort devices by extension number
- Add Category dropdown to change device category from device list
- Rooms page: card layout with vertical member list, delete room button
- Categories page: card layout with vertical device list, move-to dropdown
- Add Room modal and functionality
- Add Category modal and functionality
- Consistent formatting across all pages (card layout, tables)
- Remove from room/category via buttons on those pages
New API endpoints:
- PUT /api/devices/{ext}/category - change device category
- POST /api/rooms - create new room
- DELETE /api/rooms/{ext} - delete room
- POST /api/categories - create new category
- DELETE /api/categories/{id} - delete category
Python functions added:
- change_device_category()
- create_room(), delete_room()
- create_category(), delete_category()
Goal: Web admin can now fully replace the script for client management
- Fix stop command: mask service before stopping to prevent
Restart=always from respawning the process
- Add "Add to Room" dropdown on device list to add clients to rooms
- Show clickable member badges in rooms (click to remove from room)
- Categories page now displays devices grouped under each category
- Add API endpoints for room membership management (POST/DELETE)
Stop command improvements:
- Added verbose progress output
- Uses pgrep to check if process is running
- Multiple kill attempts with escalating force
- Falls back to lsof if fuser unavailable
- Verifies process is stopped
Transport detection:
- If media_encryption=sdes/dtls found without explicit transport,
assume TLS (for older device configs)
Rename functionality:
- Added rename_device() Python function
- Added do_PUT API handler
- Added rename modal in HTML
- Added showRenameModal/renameDevice JavaScript functions
- Rename button added next to Delete in device list
Status detection:
- Fixed parsing of 'pjsip show endpoints' output
- Looks for Endpoint: header then Contact: line with Avail/NonQual
- Matches bash script logic
Stop command improvements:
- Kill remaining processes with pkill after systemctl stop
- Force kill (-9) if processes still running after 1 second
- Release port with fuser -k as final cleanup
Status detection fix:
- Parse 'pjsip show endpoints' output correctly
- Look for Endpoint: header to get extension number
- Check Contact: lines for 'Avail' or 'NonQual' status
- Matches the bash script's detection logic
Previously the script was only created if it didn't exist,
so updates to the parser weren't being applied. Now the
Python script is regenerated every time to ensure the
latest version is used.
The Python parser was waiting for password= which comes much later
in the auth section. Rewrote to match the simpler bash approach:
- Find device comment line, parse name/category/AA tag
- Find next [extension] line, add device to list
- Update transport/encryption if found in following lines
This matches how show_registered_devices() works in bash.
Port persistence:
- Add WEB_ADMIN_PORT to save_config() and load_config()
- Save config when port is changed in web_admin_menu
- Remove hardcoded initialization that overrode saved config
Device listing fix:
- Rewrite get_devices() parser to properly track state across sections
- Fix regex to handle variable spacing in device comment lines
- Continue tracking device through auth section to capture password
- Devices now properly listed in web admin interface
Fixes:
- Extension deletion now properly removes all sections (endpoint, auth, aor)
using awk for reliable multi-section removal
- Extension renaming now preserves AA tags correctly
- LAN/VPN devices now explicitly use transport=transport-udp to prevent
TLS fallback issues that caused clients to go offline immediately
- LAN devices now have media_encryption=no to prevent SRTP negotiation issues
New Features:
- 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
- Accessible via Server Settings > Web Admin menu
Verified:
- PTT is correctly disabled by default (only enabled when user configures it)
Changes:
- Update main menu header to use ${SCRIPT_VERSION} variable
- Update pjsip.conf header comment to use ${SCRIPT_VERSION}
- Update Baresip instructions header to use ${SCRIPT_VERSION}
- Changed Baresip heredoc from single-quoted to allow variable expansion
This ensures version display always matches the SCRIPT_VERSION variable
at line 44, eliminating hardcoded version mismatches.
Changes:
- Show both Linphone and Baresip setup in add_device_menu() output
- Remove separate menu requirement - both apps shown together
- Change language from "/e/OS-specific" to generic "if Linphone has audio issues"
- Update domain examples from sip.mydomain.com to asterisk.mydomain.com
- Add note that credentials work for other SIP apps (Zoiper, sipnetic)
- Verify no user FQDN hardcoded in script
This provides immediate access to both SIP app configurations when adding
a device, with clear headers distinguishing between the two approaches.
Based on actual user experience configuring Baresip Android app.
Previous instructions were incorrect/incomplete.
KEY CHANGES:
1. Removed /e/OS-specific language:
- Now generic: "Use if Linphone has audio issues"
- Applicable to all privacy ROMs, not just /e/OS
2. Accurate two-step setup process:
- Step 1: Add account with just SIP URI (ext@domain)
- Step 2: Edit account to add auth username, password, proxy
- This matches actual app behavior
3. Critical field corrections:
- Auth Username: JUST extension number (not ext@domain)
- Outbound Proxy: JUST domain (not sip:server:port format)
- Media Encryption: srtp (select from dropdown)
4. No provisioning URL:
- Clarified Baresip doesn't support remote provisioning
- Must configure manually (unlike Linphone)
5. Added top bar icon reference:
- ☰ = Menu, ✓ = Save, ⋮ = Options
- 📞 = Call, 🔊 = Speaker, 🔇 = Mute, ✕ = Hangup
6. Default action clarification:
- May not exist in all Baresip versions
- Provided alternatives: long-press or phone icon
7. Comprehensive troubleshooting:
- Registration failure solutions
- Dialing issues
- Audio problems
- Screen-off audio (rare with Baresip)
8. Extension dialing guidance:
- Method 1: Just the number (101, 202)
- Method 2: Full format (ext@domain) if needed
9. File renamed:
- .conf → .txt for better readability on phones
Output format:
- Detailed step-by-step instructions
- Quick reference summary at end
- Troubleshooting section
- Icon reference guide
This addresses all user-reported configuration issues and provides
accurate, tested setup instructions based on real app usage.