Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d36f99a679 | ||
|
|
8c47694a1a | ||
|
|
3806bef558 | ||
|
|
ad48517b80 | ||
|
|
87a98ec356 | ||
|
|
e1dd727521 | ||
|
|
1b69ddaa98 | ||
|
|
168054f99a | ||
|
|
29d47ec819 | ||
|
|
e6ef37b6bc | ||
|
|
114391fbfd |
+6
-5
@@ -17,7 +17,7 @@
|
|||||||
# - All SIP clients connect to DOMAIN_NAME:5061 (TLS)
|
# - All SIP clients connect to DOMAIN_NAME:5061 (TLS)
|
||||||
# - coturn handles NAT traversal (STUN) and media relay (TURN)
|
# - coturn handles NAT traversal (STUN) and media relay (TURN)
|
||||||
# - Works from any network: LAN, cellular, Proton VPN, hotel WiFi
|
# - 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) ────────────────────────────────────
|
# ── Domain Name (REQUIRED) ────────────────────────────────────
|
||||||
@@ -60,11 +60,12 @@ VLAN_SUBNETS=
|
|||||||
|
|
||||||
# ── TURN/STUN Settings ──────────────────────────────────────
|
# ── TURN/STUN Settings ──────────────────────────────────────
|
||||||
# Used by coturn for TURN relay authentication.
|
# Used by coturn for TURN relay authentication.
|
||||||
# Both coturn and Asterisk must use the SAME password.
|
# If empty, defaults to "changeme" — set a real password for security.
|
||||||
# If empty, both default to "changeme" — set a real password here.
|
# Generate one with: openssl rand -base64 18
|
||||||
#
|
#
|
||||||
# These credentials are shared between coturn and Asterisk.
|
# These credentials are for coturn only. SIP clients that need TURN
|
||||||
# SIP clients do NOT need these - only the server uses them.
|
# relay (behind strict NAT) must configure the same credentials in
|
||||||
|
# their SIP app settings.
|
||||||
TURN_USERNAME=easyasterisk
|
TURN_USERNAME=easyasterisk
|
||||||
TURN_PASSWORD=
|
TURN_PASSWORD=
|
||||||
|
|
||||||
|
|||||||
+10
-9
@@ -24,7 +24,7 @@ services:
|
|||||||
network_mode: host
|
network_mode: host
|
||||||
depends_on:
|
depends_on:
|
||||||
coturn:
|
coturn:
|
||||||
condition: service_healthy
|
condition: service_started
|
||||||
volumes:
|
volumes:
|
||||||
- asterisk-config:/etc/asterisk
|
- asterisk-config:/etc/asterisk
|
||||||
- easy-asterisk-config:/etc/easy-asterisk
|
- easy-asterisk-config:/etc/easy-asterisk
|
||||||
@@ -85,12 +85,19 @@ services:
|
|||||||
# The coturn image runs as nobody:nogroup by default, which cannot
|
# The coturn image runs as nobody:nogroup by default, which cannot
|
||||||
# create /var/run/turnserver.pid. Run as root to avoid this.
|
# create /var/run/turnserver.pid. Run as root to avoid this.
|
||||||
user: root
|
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:
|
environment:
|
||||||
# Image-native external IP detection (adds --external-ip automatically)
|
# Passed to our entrypoint for --external-ip. Auto-detected if empty.
|
||||||
- DETECT_EXTERNAL_IP=${DETECT_EXTERNAL_IP:-yes}
|
- PUBLIC_IP=${PUBLIC_IP:-}
|
||||||
command:
|
command:
|
||||||
- -n
|
- -n
|
||||||
- --listening-port=${TURN_PORT:-3478}
|
- --listening-port=${TURN_PORT:-3478}
|
||||||
|
- --listening-ip=0.0.0.0
|
||||||
- --fingerprint
|
- --fingerprint
|
||||||
- --lt-cred-mech
|
- --lt-cred-mech
|
||||||
- --user=${TURN_USERNAME:-easyasterisk}:${TURN_PASSWORD:-changeme}
|
- --user=${TURN_USERNAME:-easyasterisk}:${TURN_PASSWORD:-changeme}
|
||||||
@@ -103,12 +110,6 @@ services:
|
|||||||
- --no-multicast-peers
|
- --no-multicast-peers
|
||||||
- --log-file=stdout
|
- --log-file=stdout
|
||||||
restart: unless-stopped
|
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:
|
volumes:
|
||||||
asterisk-config:
|
asterisk-config:
|
||||||
|
|||||||
Executable
+26
@@ -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
@@ -279,24 +279,22 @@ EOF
|
|||||||
chown asterisk:asterisk /etc/asterisk/pjsip.conf
|
chown asterisk:asterisk /etc/asterisk/pjsip.conf
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ── rtp.conf (always regenerated - includes TURN credentials) ──
|
# ── rtp.conf (always regenerated) ──
|
||||||
# Use 127.0.0.1 for stunaddr/turnaddr because coturn runs on the same host
|
# ICE is enabled so Asterisk participates in ICE negotiation with clients.
|
||||||
# (network_mode: host). Using the FQDN would cause DNS resolution, and if the
|
# stunaddr/turnaddr are NOT set here because:
|
||||||
# DNS TTL is 0 Asterisk cancels recurring resolution — breaking ICE entirely
|
# - Asterisk already knows its public IP via external_media_address in pjsip.conf
|
||||||
# and adding a ~27-second timeout delay to every call.
|
# - Its RTP ports are port-forwarded, so host candidates are sufficient
|
||||||
turn_port="${turn_server##*:}"
|
# - Setting stunaddr/turnaddr causes STUN/TURN gather timeouts (~27s per call)
|
||||||
local_turn="127.0.0.1:${turn_port:-3478}"
|
# when the STUN/TURN server is unreachable or misconfigured
|
||||||
log_info "Configuring RTP with ICE + STUN + TURN (local: ${local_turn})..."
|
# 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
|
cat > /etc/asterisk/rtp.conf << EOF
|
||||||
[general]
|
[general]
|
||||||
rtpstart=${RTP_START:-10000}
|
rtpstart=${RTP_START:-10000}
|
||||||
rtpend=${RTP_END:-20000}
|
rtpend=${RTP_END:-20000}
|
||||||
strictrtp=yes
|
strictrtp=yes
|
||||||
icesupport=yes
|
icesupport=yes
|
||||||
stunaddr=${local_turn}
|
|
||||||
turnaddr=${local_turn}
|
|
||||||
turnusername=${TURN_USERNAME}
|
|
||||||
turnpassword=${TURN_PASSWORD}
|
|
||||||
EOF
|
EOF
|
||||||
chown asterisk:asterisk /etc/asterisk/rtp.conf
|
chown asterisk:asterisk /etc/asterisk/rtp.conf
|
||||||
|
|
||||||
|
|||||||
+70
-15
@@ -944,6 +944,28 @@ EOF
|
|||||||
echo " Server Settings → Provisioning Manager → Create Baresip Config"
|
echo " Server Settings → Provisioning Manager → Create Baresip Config"
|
||||||
echo ""
|
echo ""
|
||||||
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 ""
|
||||||
echo " NOTE: These instructions work for most SIP apps (Zoiper,"
|
echo " NOTE: These instructions work for most SIP apps (Zoiper,"
|
||||||
echo " sipnetic, etc.) - just use the same credentials."
|
echo " sipnetic, etc.) - just use the same credentials."
|
||||||
@@ -2015,11 +2037,20 @@ create_linphone_xml() {
|
|||||||
|
|
||||||
<section name="net">
|
<section name="net">
|
||||||
<entry name="mtu">1300</entry>
|
<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>
|
</section>
|
||||||
|
|
||||||
</config>
|
</config>
|
||||||
EOF
|
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"
|
chown asterisk:asterisk "$xml_file"
|
||||||
chmod 644 "$xml_file"
|
chmod 644 "$xml_file"
|
||||||
|
|
||||||
@@ -3032,21 +3063,18 @@ transport=config,pjsip.conf,criteria=type=transport
|
|||||||
EOF
|
EOF
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ICE / STUN / TURN configuration
|
# ICE configuration
|
||||||
# Enabled for: FQDN/internet mode OR VPN with ICE enabled
|
# 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
|
load_config
|
||||||
local ice_config=""
|
local ice_config=""
|
||||||
if [[ -n "$DOMAIN_NAME" ]] || [[ "$VPN_ICE_ENABLED" == "y" ]] || [[ "$TURN_ENABLED" == "y" ]]; then
|
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"
|
||||||
ice_config="icesupport=yes
|
|
||||||
stunaddr=${stun_addr}"
|
|
||||||
# Add TURN relay if configured (required for calls through strict NAT/VPN)
|
|
||||||
if [[ "$TURN_ENABLED" == "y" && -n "$TURN_SERVER" && -n "$TURN_USERNAME" && -n "$TURN_PASSWORD" ]]; then
|
|
||||||
ice_config="${ice_config}
|
|
||||||
turnaddr=${TURN_SERVER}
|
|
||||||
turnusername=${TURN_USERNAME}
|
|
||||||
turnpassword=${TURN_PASSWORD}"
|
|
||||||
fi
|
|
||||||
else
|
else
|
||||||
ice_config="# icesupport disabled - LAN only mode"
|
ice_config="# icesupport disabled - LAN only mode"
|
||||||
fi
|
fi
|
||||||
@@ -4750,20 +4778,33 @@ qualify_frequency=30
|
|||||||
return True, {'extension': extension, 'password': password, 'name': name}
|
return True, {'extension': extension, 'password': password, 'name': name}
|
||||||
|
|
||||||
def get_server_info():
|
def get_server_info():
|
||||||
"""Get server configuration info"""
|
"""Get server configuration info including TURN/STUN details"""
|
||||||
info = {
|
info = {
|
||||||
'domain': '',
|
'domain': '',
|
||||||
'tls_enabled': False,
|
'tls_enabled': False,
|
||||||
'server_ip': ''
|
'server_ip': '',
|
||||||
|
'turn_enabled': False,
|
||||||
|
'turn_server': '',
|
||||||
|
'turn_username': '',
|
||||||
|
'turn_password': ''
|
||||||
}
|
}
|
||||||
|
|
||||||
if os.path.exists(CONFIG_FILE):
|
if os.path.exists(CONFIG_FILE):
|
||||||
with open(CONFIG_FILE, 'r') as f:
|
with open(CONFIG_FILE, 'r') as f:
|
||||||
for line in f:
|
for line in f:
|
||||||
|
line = line.strip()
|
||||||
if line.startswith('DOMAIN_NAME='):
|
if line.startswith('DOMAIN_NAME='):
|
||||||
info['domain'] = line.split('=', 1)[1].strip().strip('"')
|
info['domain'] = line.split('=', 1)[1].strip().strip('"')
|
||||||
elif line.startswith('ENABLE_TLS='):
|
elif line.startswith('ENABLE_TLS='):
|
||||||
info['tls_enabled'] = 'y' in line.lower()
|
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:
|
try:
|
||||||
result = subprocess.run(['hostname', '-I'], capture_output=True, text=True)
|
result = subprocess.run(['hostname', '-I'], capture_output=True, text=True)
|
||||||
@@ -5657,11 +5698,25 @@ HTML_TEMPLATE = '''<!DOCTYPE html>
|
|||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
closeModal();
|
closeModal();
|
||||||
document.getElementById('credentials-display').innerHTML = `
|
let credHtml = `
|
||||||
<p><strong>Extension:</strong> ${result.data.extension}</p>
|
<p><strong>Extension:</strong> ${result.data.extension}</p>
|
||||||
<p><strong>Password:</strong> ${result.data.password}</p>
|
<p><strong>Password:</strong> ${result.data.password}</p>
|
||||||
<p><strong>Name:</strong> ${result.data.name}</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');
|
document.getElementById('credentials-modal').classList.add('active');
|
||||||
} else {
|
} else {
|
||||||
showAlert(result.error || 'Failed to add device', 'error');
|
showAlert(result.error || 'Failed to add device', 'error');
|
||||||
|
|||||||
Reference in New Issue
Block a user