Compare commits

...
11 Commits
Author SHA1 Message Date
Outis d36f99a679 Merge pull request #40 from outis1one/claude/fix-call-ringing-delay-5KXT9
Remove coturn healthcheck — coturn/coturn:latest v4.8.0 image does not
2026-02-25 15:56:05 -05:00
Claude 8c47694a1a Remove coturn healthcheck — coturn/coturn:latest v4.8.0 image does not
support CMD-SHELL healthchecks (turnutils_stunclient, PID file checks
both fail). Asterisk uses service_started since it doesn't connect to
coturn directly — SIP clients do.

https://claude.ai/code/session_01KWVtEt9MmZdywcu7WmgchX
2026-02-25 20:29:24 +00:00
Outis 3806bef558 Merge pull request #39 from outis1one/claude/fix-call-ringing-delay-5KXT9
Fix coturn healthcheck: use PID file instead of turnutils_stunclient
2026-02-25 14:53:59 -05:00
Claude ad48517b80 Fix coturn healthcheck: use PID file instead of turnutils_stunclient
turnutils_stunclient is not available in coturn/coturn:latest (v4.8.0),
causing the healthcheck to always fail and preventing the asterisk
container from starting. Switch to checking the PID file that coturn
creates on successful startup.

https://claude.ai/code/session_01KWVtEt9MmZdywcu7WmgchX
2026-02-25 17:45:35 +00:00
Outis 87a98ec356 Merge pull request #38 from outis1one/claude/fix-call-ringing-delay-5KXT9
Fix coturn "Unknown argument:" error and noisy interface binding
2026-02-25 12:06:01 -05:00
Claude e1dd727521 Fix coturn "Unknown argument:" error and noisy interface binding
Replace the coturn image's fragile eval-based entrypoint with a robust
wrapper that handles external IP detection without word-splitting issues.
The image's `exec $(eval "echo $@")` produces empty tokens when
DETECT_EXTERNAL_IP's DNS lookup fails → "ERROR: CONFIG: Unknown argument:"

Also add --listening-ip=0.0.0.0 so coturn binds a single wildcard address
instead of enumerating every host interface (reduces log noise).

https://claude.ai/code/session_01KWVtEt9MmZdywcu7WmgchX
2026-02-25 14:04:02 +00:00
Outis 1b69ddaa98 Merge pull request #37 from outis1one/claude/fix-call-ringing-delay-5KXT9
Auto-provision STUN/TURN credentials to SIP clients
2026-02-25 08:29:46 -05:00
Claude 168054f99a Auto-provision STUN/TURN credentials to SIP clients
Three provisioning touchpoints now include TURN/STUN configuration:

1. Linphone XML provisioning: ICE firewall policy, STUN server, and
   TURN credentials are auto-configured — no manual network setup needed

2. "Device Added" CLI output: Shows TURN server, username, and password
   for manual SIP apps (Sipnetic, Zoiper, etc.)

3. Web admin /api/server: Returns TURN details so the credentials modal
   shows STUN/TURN info alongside extension/password when creating devices

https://claude.ai/code/session_01KWVtEt9MmZdywcu7WmgchX
2026-02-24 23:58:10 +00:00
Outis 29d47ec819 Merge pull request #36 from outis1one/claude/fix-call-ringing-delay-5KXT9
Remove stunaddr/turnaddr from rtp.conf — fixes STUN timeout causing 2…
2026-02-24 18:52:16 -05:00
Claude e6ef37b6bc Remove stunaddr/turnaddr from rtp.conf — fixes STUN timeout causing 27s delay
Asterisk doesn't need STUN/TURN in rtp.conf for this setup because:
- It already knows its public IP via external_media_address in pjsip.conf
- Its RTP ports are port-forwarded, so host ICE candidates are sufficient
- stunaddr/turnaddr caused STUN gather timeouts (~27s) on every call,
  whether pointing at the FQDN (DNS TTL=0 bug) or localhost (UDP timeout)

coturn is for SIP *clients* behind strict NAT — they configure TURN in
their own app settings, independently of Asterisk's rtp.conf.

ICE support remains enabled so Asterisk participates in ICE negotiation.

https://claude.ai/code/session_01KWVtEt9MmZdywcu7WmgchX
2026-02-24 22:18:55 +00:00
Outis 114391fbfd Merge pull request #35 from outis1one/claude/fix-call-ringing-delay-5KXT9
Fix 27s call ringing delay: TURN credential mismatch and STUN DNS TTL=0
2026-02-24 17:10:46 -05:00
5 changed files with 122 additions and 41 deletions
+6 -5
View File
@@ -17,7 +17,7 @@
# - 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
# - Set TURN_PASSWORD below (generate one: openssl rand -base64 18)
# ================================================================
# ── Domain Name (REQUIRED) ────────────────────────────────────
@@ -60,11 +60,12 @@ VLAN_SUBNETS=
# ── TURN/STUN Settings ──────────────────────────────────────
# Used by coturn for TURN relay authentication.
# Both coturn and Asterisk must use the SAME password.
# If empty, both default to "changeme" — set a real password here.
# If empty, defaults to "changeme" — set a real password for security.
# Generate one with: openssl rand -base64 18
#
# These credentials are shared between coturn and Asterisk.
# SIP clients do NOT need these - only the server uses them.
# These credentials are for coturn only. SIP clients that need TURN
# relay (behind strict NAT) must configure the same credentials in
# their SIP app settings.
TURN_USERNAME=easyasterisk
TURN_PASSWORD=
+10 -9
View File
@@ -24,7 +24,7 @@ services:
network_mode: host
depends_on:
coturn:
condition: service_healthy
condition: service_started
volumes:
- asterisk-config:/etc/asterisk
- easy-asterisk-config:/etc/easy-asterisk
@@ -85,12 +85,19 @@ services:
# The coturn image runs as nobody:nogroup by default, which cannot
# create /var/run/turnserver.pid. Run as root to avoid this.
user: root
# Custom entrypoint bypasses the coturn image's fragile eval-based
# entrypoint which causes "Unknown argument:" errors when IP detection
# returns empty. Our wrapper handles detection robustly.
entrypoint: ["/coturn-entrypoint.sh"]
volumes:
- ./docker/coturn-entrypoint.sh:/coturn-entrypoint.sh:ro
environment:
# Image-native external IP detection (adds --external-ip automatically)
- DETECT_EXTERNAL_IP=${DETECT_EXTERNAL_IP:-yes}
# Passed to our entrypoint for --external-ip. Auto-detected if empty.
- PUBLIC_IP=${PUBLIC_IP:-}
command:
- -n
- --listening-port=${TURN_PORT:-3478}
- --listening-ip=0.0.0.0
- --fingerprint
- --lt-cred-mech
- --user=${TURN_USERNAME:-easyasterisk}:${TURN_PASSWORD:-changeme}
@@ -103,12 +110,6 @@ services:
- --no-multicast-peers
- --log-file=stdout
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "turnutils_stunclient -p ${TURN_PORT:-3478} 127.0.0.1 >/dev/null 2>&1"]
interval: 10s
timeout: 5s
retries: 3
start_period: 10s
volumes:
asterisk-config:
+26
View File
@@ -0,0 +1,26 @@
#!/bin/sh
# ================================================================
# Robust coturn entrypoint
#
# The coturn/coturn Docker image's native entrypoint uses:
# exec $(eval "echo $@")
# which is fragile — if DETECT_EXTERNAL_IP's DNS lookup returns empty,
# the eval produces an empty token → "ERROR: CONFIG: Unknown argument:"
#
# This wrapper reuses the image's detect-external-ip script but avoids
# the eval word-splitting issue. If detection fails, we simply omit
# --external-ip rather than passing a blank argument.
# ================================================================
# Use explicit PUBLIC_IP if provided, otherwise auto-detect
if [ -z "$PUBLIC_IP" ]; then
PUBLIC_IP=$(detect-external-ip 2>/dev/null || true)
fi
# Only add --external-ip if we actually have an IP
EXTERNAL_IP_ARG=""
if [ -n "$PUBLIC_IP" ]; then
EXTERNAL_IP_ARG="--external-ip=$PUBLIC_IP"
fi
exec turnserver "$@" $EXTERNAL_IP_ARG
+10 -12
View File
@@ -279,24 +279,22 @@ EOF
chown asterisk:asterisk /etc/asterisk/pjsip.conf
fi
# ── rtp.conf (always regenerated - includes TURN credentials) ──
# Use 127.0.0.1 for stunaddr/turnaddr because coturn runs on the same host
# (network_mode: host). Using the FQDN would cause DNS resolution, and if the
# DNS TTL is 0 Asterisk cancels recurring resolution — breaking ICE entirely
# and adding a ~27-second timeout delay to every call.
turn_port="${turn_server##*:}"
local_turn="127.0.0.1:${turn_port:-3478}"
log_info "Configuring RTP with ICE + STUN + TURN (local: ${local_turn})..."
# ── rtp.conf (always regenerated) ──
# ICE is enabled so Asterisk participates in ICE negotiation with clients.
# stunaddr/turnaddr are NOT set here because:
# - Asterisk already knows its public IP via external_media_address in pjsip.conf
# - Its RTP ports are port-forwarded, so host candidates are sufficient
# - Setting stunaddr/turnaddr causes STUN/TURN gather timeouts (~27s per call)
# when the STUN/TURN server is unreachable or misconfigured
# coturn is for SIP CLIENTS behind strict NAT — they configure TURN in their
# own app settings, independently of Asterisk's rtp.conf.
log_info "Configuring RTP with ICE support..."
cat > /etc/asterisk/rtp.conf << EOF
[general]
rtpstart=${RTP_START:-10000}
rtpend=${RTP_END:-20000}
strictrtp=yes
icesupport=yes
stunaddr=${local_turn}
turnaddr=${local_turn}
turnusername=${TURN_USERNAME}
turnpassword=${TURN_PASSWORD}
EOF
chown asterisk:asterisk /etc/asterisk/rtp.conf
+70 -15
View File
@@ -944,6 +944,28 @@ EOF
echo " Server Settings → Provisioning Manager → Create Baresip Config"
echo ""
echo "═══════════════════════════════════════════════════════════════"
# Show TURN/STUN settings if enabled (for manual SIP app configuration)
if [[ "$TURN_ENABLED" == "y" && -n "$TURN_SERVER" ]]; then
echo ""
echo -e " ${BOLD}STUN/TURN SETTINGS (for NAT traversal)${NC}"
echo "═══════════════════════════════════════════════════════════════"
echo ""
echo " Configure these in your SIP app's Network/ICE settings:"
echo " ICE: Enabled"
echo " STUN server: ${TURN_SERVER}"
echo " TURN server: ${TURN_SERVER}"
echo " TURN username: ${TURN_USERNAME}"
echo " TURN password: ${TURN_PASSWORD}"
echo " TURN transport: UDP"
echo ""
echo " Linphone: Auto-provisioned via XML (no manual setup needed)"
echo " Sipnetic: Settings → Network → ICE/STUN/TURN"
echo " Olinuxino: Settings → Network → ICE/STUN/TURN"
echo ""
echo "═══════════════════════════════════════════════════════════════"
fi
echo ""
echo " NOTE: These instructions work for most SIP apps (Zoiper,"
echo " sipnetic, etc.) - just use the same credentials."
@@ -2015,11 +2037,20 @@ create_linphone_xml() {
<section name="net">
<entry name="mtu">1300</entry>
<!-- ICE + STUN/TURN for NAT traversal -->
<entry name="firewall_policy">3</entry>
<entry name="stun_server">${TURN_SERVER:-${domain}:3478}</entry>
</section>
</config>
EOF
# Add TURN credentials section if TURN is enabled
if [[ "$TURN_ENABLED" == "y" && -n "$TURN_SERVER" && -n "$TURN_USERNAME" && -n "$TURN_PASSWORD" ]]; then
# Insert TURN credentials into the net section before </config>
sed -i "s|<entry name=\"stun_server\">.*</entry>|<entry name=\"stun_server\">${TURN_SERVER}</entry>\n <entry name=\"turn_enable\">1</entry>\n <entry name=\"turn_username\">${TURN_USERNAME}</entry>\n <entry name=\"turn_password\">${TURN_PASSWORD}</entry>|" "$xml_file"
fi
chown asterisk:asterisk "$xml_file"
chmod 644 "$xml_file"
@@ -3032,21 +3063,18 @@ transport=config,pjsip.conf,criteria=type=transport
EOF
fi
# ICE / STUN / TURN configuration
# Enabled for: FQDN/internet mode OR VPN with ICE enabled
# ICE configuration
# ICE is enabled so Asterisk participates in ICE negotiation with clients.
# stunaddr/turnaddr are NOT set because:
# - Asterisk knows its public IP via external_media_address in pjsip.conf
# - Its RTP ports are port-forwarded, so host candidates are sufficient
# - Setting stunaddr/turnaddr causes STUN/TURN gather timeouts (~27s delay)
# coturn (if running) is for SIP clients behind strict NAT — they configure
# TURN in their own app settings, independently of Asterisk's rtp.conf.
load_config
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
ice_config="icesupport=yes"
else
ice_config="# icesupport disabled - LAN only mode"
fi
@@ -4750,20 +4778,33 @@ qualify_frequency=30
return True, {'extension': extension, 'password': password, 'name': name}
def get_server_info():
"""Get server configuration info"""
"""Get server configuration info including TURN/STUN details"""
info = {
'domain': '',
'tls_enabled': False,
'server_ip': ''
'server_ip': '',
'turn_enabled': False,
'turn_server': '',
'turn_username': '',
'turn_password': ''
}
if os.path.exists(CONFIG_FILE):
with open(CONFIG_FILE, 'r') as f:
for line in f:
line = line.strip()
if line.startswith('DOMAIN_NAME='):
info['domain'] = line.split('=', 1)[1].strip().strip('"')
elif line.startswith('ENABLE_TLS='):
info['tls_enabled'] = 'y' in line.lower()
elif line.startswith('TURN_ENABLED='):
info['turn_enabled'] = 'y' in line.split('=', 1)[1].lower()
elif line.startswith('TURN_SERVER='):
info['turn_server'] = line.split('=', 1)[1].strip().strip('"')
elif line.startswith('TURN_USERNAME='):
info['turn_username'] = line.split('=', 1)[1].strip().strip('"')
elif line.startswith('TURN_PASSWORD='):
info['turn_password'] = line.split('=', 1)[1].strip().strip('"')
try:
result = subprocess.run(['hostname', '-I'], capture_output=True, text=True)
@@ -5657,11 +5698,25 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
if (result.success) {
closeModal();
document.getElementById('credentials-display').innerHTML = `
let credHtml = `
<p><strong>Extension:</strong> ${result.data.extension}</p>
<p><strong>Password:</strong> ${result.data.password}</p>
<p><strong>Name:</strong> ${result.data.name}</p>
`;
// Fetch server info to show TURN details
try {
const srvRes = await fetch(API_BASE + '/server');
const srv = await srvRes.json();
if (srv.turn_enabled && srv.turn_server) {
credHtml += `<hr style="margin:12px 0;border-color:#e2e8f0">
<p style="font-size:13px;color:#64748b;margin-bottom:6px">STUN/TURN (configure in app Network settings)</p>
<p><strong>STUN/TURN server:</strong> ${srv.turn_server}</p>
<p><strong>TURN username:</strong> ${srv.turn_username}</p>
<p><strong>TURN password:</strong> ${srv.turn_password}</p>
`;
}
} catch(e) {}
document.getElementById('credentials-display').innerHTML = credHtml;
document.getElementById('credentials-modal').classList.add('active');
} else {
showAlert(result.error || 'Failed to add device', 'error');