Asterisk PBX on OpenWrt with a Telekom All-IP Trunk over TLS
Table of Contents
Deutsche Telekom’s All-IP consumer connections hand you SIP credentials for your landline numbers, and you are free to use them with your own equipment. I bought some VoIP desk phones recently and configured our landline with a self-hosted Asterisk PBX on a Raspberry Pi 4 running OpenWrt (see Router).
tl;dr Telekom’s consumer SIP works fine with your own PBX, also over TLS, but requires TLS 1.2 and SDES-encrypted media. Finally, the DNS SRV record order changes and messes with registration target and outgoing calls in Asterisk.
Pinning the order for local resolution via dnsmasq in OpenWRT ensures that registrations and outbound calls land on
the same SBC node and don’t cause 403 Forbidden errors without any “reason” given.
My Setup #
- Raspberry Pi 4 running OpenWrt 23.05 as my primary router, Asterisk 20.x installed via
opkg(PJSIP stack). - SNOM 300 desk phones on the LAN, registered against Asterisk as local extensions.
- A Telekom consumer (“All-IP”) landline with three phone numbers, each registered independently at
tel.t-online.de.
The PBX is set up as follows: phones only talk to Asterisk, Asterisk talks to Telekom. Asterisk initiates and maintains the connection to Telekom’s SBC, and both sides reuse that connection in both directions for signaling (outgoing and incoming calls).
TLS for a Consumer All-IP Account #
Most setup guides you’ll find cover UDP with anonymous@t-online.de and no password, which indeed works. I wanted
signaling over TLS (port 5061) instead. That turns out to work equally well, but with a few non-obvious constraints.
The authoritative source is Telekom’s 1TR119 technical directive,
which contains a key line: “The SIPS URI format is not supported.”
The correct configuration uses plain sip: URIs throughout, and the transport carries the TLS:
[transport-tls]
type = transport
protocol = tls
bind = 0.0.0.0
; Server verification of Telekom's cert (T-TeleSec roots in system bundle)
ca_list_file = /etc/ssl/certs/ca-certificates.crt
verify_server = true
method = tlsv1_2 ; Telekom SBC rejects TLS 1.3 handshakes
external_signaling_address = 203.0.113.10 ; your public WAN IPv4
[telekom-reg]
type = registration
transport = transport-tls
outbound_auth = telekom-auth
server_uri = sip:tel.t-online.de;transport=tls
client_uri = sip:+49XXXXXXXXXX@tel.t-online.de
[mytrunk]
type = aor
contact = sip:tel.t-online.de ; sip:, NOT sips: — the TLS transport already implies port 5061
Configuration Notes #
server_uricontrols the Contact scheme. PJSIP copies theserver_urischeme into the REGISTER’s Contact header. With asips:Contact, outbound still looks fine, but Telekom silently rejects every inbound call on their side — the caller hears “not available” and no packet ever reaches you. Withsip:...;transport=tls, inbound INVITEs arrive on your existing TLS connection.The trunk AOR contact must be
sip:tel.t-online.deas well, for the same reason. Withsips:there you get a bare480 Temporarily Unavailableinstead of the expected407digest challenge.Media must be SDES-encrypted over TLS. The SBC answers plain-RTP offers on the TLS transport with
488 Not Acceptable Here — security violation.media_encryption=sdeson the endpoint (plusres_srtp.soloaded) fixes both directions. Telekom’s additionalmediasecnegotiation (1TR119 §10.4) is not required — I tested inbound and outbound without it.external_signaling_addressmatters. PJSIP’s transport has no interface-selection option, onlybind. Bound to0.0.0.0, it picked my Tailscale address (100.64.0.1) into Via/Contact. Setting the public WAN IPv4 literal fixes it. Since PPPoE redials change the address, a small hotplug hook onifupof the WAN interface rewrites an include file with the current address and restarts Asterisk (transport-tlshasallow_reload=false).Use the arrow to unfold the hotplug hook
/etc/hotplug.d/iface/12-asterisk-wan.Keeps external_signaling_addresson the current WAN IPv4 across PPPoE redials.#!/bin/sh # asterisk-wan-ip.sh — keep Asterisk's TLS Via/Contact address on the WAN IPv4 # # OpenWrt hotplug (iface) script, e.g. linked as: # ln -nfs /root/asterisk-wan-ip.sh /etc/hotplug.d/iface/12-asterisk-wan # # Why: transport-tls binds 0.0.0.0, so pjsip advertises a guessed address in # Via/Contact. external_signaling_address fixes that but only accepts an IPv4 # literal, and the PPPoE WAN address changes on every redial. [ "$ACTION" = ifup ] && [ "$INTERFACE" = WAN ] || exit 0 CONF=/etc/asterisk/pjsip-wan.conf ; #included from [transport-tls] . /lib/functions/network.sh network_get_ipaddr ip WAN [ -n "$ip" ] || exit 0 line="external_signaling_address=$ip" if [ -r "$CONF" ] && [ "$(grep -v '^;' "$CONF")" = "$line" ]; then exit 0 fi printf '%s\n%s\n' "; Generated by asterisk-wan-ip.sh on WAN ifup — do not edit" "$line" > "$CONF.tmp" \ && mv "$CONF.tmp" "$CONF" || exit 0 # restart runs in the background: hotplug runs iface scripts sequentially # and must not be held up (transport-tls has allow_reload=false) /usr/sbin/asterisk -rx "core restart now" >/dev/null 2>&1 &Inbound caller ID needs
trust_id_inbound=yeson the trunk endpoints. Telekom sends the real caller inP-Asserted-Identity, but untrusted endpoints cause Asterisk to stamp the endpoint’s owncalleridonto inbound legs, so every call appeared to come “from” my own number.
OpenWRT Asterisk Packages #
OpenWRT splits Asterisk into a myriad of different packages that provide individual functionality. In order to have a full PBX working with VoIP phones (physical or softphone), a bunch of codecs, converters, transports and whatnot need to be installed and enabled. Module loading of available modules does not happen automatically on OpenWRT’s Asterisk and needs to be handled manually.
I have the following opkg packages installed:
opkg install asterisk asterisk-pjsip asterisk-res-pjproject asterisk-res-rtp-asterisk asterisk-res-sorcery \
asterisk-bridge-simple asterisk-bridge-native-rtp asterisk-bridge-builtin-features \
asterisk-codec-alaw asterisk-codec-ulaw asterisk-codec-g722 asterisk-codec-resample asterisk-codec-gsm \
asterisk-format-gsm asterisk-format-wav asterisk-format-pcm asterisk-format-sln \
asterisk-res-srtp \
asterisk-app-stack asterisk-app-voicemail asterisk-app-system \
asterisk-func-channel asterisk-func-hangupcause asterisk-func-vmcount \
asterisk-pbx-spool asterisk-res-agi asterisk-res-musiconhold asterisk-sounds
Highlights:
asterisk-res-srtp— required for the SDES media Telekom mandates on TLS.asterisk-codec-alawis Telekom’s baseline;codec-g722+codec-resamplecover the phones and transcoding when Telekom answers alaw-only.- The
asterisk-format-*packages are separate from the codecs and easy to miss — without them Asterisk has no file formats registered and can’t play a single sound prompt.
and the following modules.conf (autoload=no, so everything needed is loaded explicitly):
; modules.conf — load only explicitly listed modules
[modules]
autoload=no
; Core / Timing
load=res_timing_timerfd.so
load=res_crypto.so
; PJSIP stack
load=res_pjproject.so
load=res_pjsip.so
load=res_pjsip_authenticator_digest.so
load=res_pjsip_caller_id.so
load=res_pjsip_dtmf_info.so
load=res_pjsip_endpoint_identifier_anonymous.so
load=res_pjsip_endpoint_identifier_ip.so
load=res_pjsip_endpoint_identifier_user.so
load=res_pjsip_header_funcs.so
load=res_pjsip_logger.so
load=res_pjsip_nat.so
load=res_pjsip_notify.so
load=res_pjsip_outbound_authenticator_digest.so
load=res_pjsip_outbound_registration.so
load=res_pjsip_outbound_publish.so
load=res_pjsip_exten_state.so
load=res_pjsip_dialog_info_body_generator.so
load=res_pjsip_mwi.so
load=res_pjsip_mwi_body_generator.so
load=res_pjsip_registrar.so
load=res_pjsip_refer.so
load=res_pjsip_rfc3326.so
load=res_pjsip_sdp_rtp.so
load=res_pjsip_session.so
load=res_rtp_asterisk.so
; Channel driver
load=chan_pjsip.so
; SRTP (encrypted audio)
load=res_srtp.so
; Sorcery (config backend)
load=res_sorcery_astdb.so
load=res_sorcery_config.so
load=res_sorcery_memory.so
; Dialplan
load=pbx_config.so
; Applications
load=app_dial.so
load=app_echo.so
load=app_playback.so
load=app_stack.so
load=app_system.so
load=app_voicemail.so
; Codecs
load=codec_alaw.so
load=codec_ulaw.so
load=codec_g722.so
load=codec_resample.so
load=codec_gsm.so
; Formats (file playback)
load=format_gsm.so
load=format_wav.so
load=format_pcm.so
load=format_sln.so
; Functions
load=func_callerid.so
load=func_channel.so
load=func_hangupcause.so
load=func_logic.so
load=func_pjsip_endpoint.so
load=func_strings.so
load=func_timeout.so
load=func_vmcount.so
; Music on Hold
load=res_musiconhold.so
; Bridges (required for connecting calls)
load=bridge_simple.so
load=bridge_native_rtp.so
load=bridge_builtin_features.so
SRV Rotation vs. Your Registration #
tel.t-online.de doesn’t resolve to one SBC. The _sips._tcp SRV lookup returns a pool of nodes and Telekom rotates
the order between answers, likely to balance load. PJSIP resolves that name for every request:
the initial REGISTER, every re-REGISTER, every OPTIONS qualify, and every outbound INVITE.
The persistent connection is opened with REGISTER and lives on that first node. Later, the SRV answer order rotates,
and the next INVITE goes to a different node. That node has no registration state for your line and answers a bare
403 Forbidden (i.e. without any further indication of reason) right after the 100 Trying.
Meanwhile all registrations show Registered, OPTIONS keepalives succeed (whichever node answers is happy to do that),
but outbound calls fail when SRV doesn’t resolve the same node as the REGISTER went to. Restarting Asterisk
“fixes” it only because the new registration momentarily aligns with wherever DNS points at the time. Forum threads
attribute this to an SBC bug that drops session state, but it’s reproducible deterministically as a node mismatch.
Inbound calls have the symmetric problem: Telekom delivers calls to whichever node your registration currently lives on, and they must be able to correlate that with your Contact — another reason the Contact header had to be exactly right (above).
Pinning the SRV order in dnsmasq #
A workaround that seems to work reliably on OpenWrt is to serve a locally pinned SRV record for
_sips._tcp.tel.t-online.de via dnsmasq, with an explicit priority order over the nodes Telekom currently advertises:
❯ dig +short SRV _sips._tcp.tel.t-online.de
10 0 5061 ffm021-l01-mav-pc-ra-001.edns.t-ipnet.de.
20 0 5061 hno002-f01-mav-pc-rb-001.edns.t-ipnet.de.
30 0 5061 nes008-f01-mav-pc-rb-001.edns.t-ipnet.de.
Leads to:
uci add dhcp srvhost
uci set dhcp.@srvhost[-1].srv='_sips._tcp.tel.t-online.de'
uci set dhcp.@srvhost[-1].target='ffm021-l01-mav-pc-ra-001.edns.t-ipnet.de'
uci set dhcp.@srvhost[-1].port='5061'
uci set dhcp.@srvhost[-1].class='10' # priority
# ... two more entries with class 20 and 30
This pins an order, not a single server: if the priority-10 node goes down, clients fall through to priority 20/30 —
the pin only removes the Telekom-side shuffling, not redundancy. Asterisk keeps doing SRV lookups of the exact name
tel.t-online.de, so TLS certificate verification (verify_server, which only lists that name in the SAN) keeps
working. A static IP or outbound_proxy override would break that.
The result of the dig command will change over time.
A nightly script re-asks a public resolver (1.1.1.1, not the local dnsmasq that serves the override) for the current
SRV answer, validates port and target domain, rewrites the pin in the dnsmasq config, restarts dnsmasq, then restarts
Asterisk when convenient so the registration follows the new order.
Use the arrow to unfold /root/telekom-srv-rotate.sh.
telekom-srv-rotate.sh: Nightly re-pin of the dnsmasq SRV record to Telekom’s currently advertised SBC nodes.#!/bin/sh
# telekom-srv-rotate.sh — rotate the SRV pin for the Telekom SIP trunk
#
# Cron (router): 10 4 * * * /root/telekom-srv-rotate.sh
# -n dry run: show current pin and Telekom's answer, change nothing
#
# Rules:
# * Only DNS queries to the upstream resolver; nothing is sent to the SBC.
# * Restarts Asterisk with "core restart when convenient" (waits for the
# last call to end, accepts new ones meanwhile).
# * Only accepts port 5061 and targets under edns.t-ipnet.de that resolve.
SRV=_sips._tcp.tel.t-online.de
UPSTREAM=1.1.1.1
LOCK=/tmp/telekom-srv-rotate.lock
DRY=0
[ "$1" = "-n" ] && DRY=1
# one run at a time
if [ -e "$LOCK" ] && kill -0 "$(cat "$LOCK" 2>/dev/null)" 2>/dev/null; then
exit 0
fi
echo $$ > "$LOCK"
trap 'rm -f "$LOCK"' EXIT
# Telekom's own answer, asked from the real upstream (not our local override)
answer=$(nslookup -type=SRV "$SRV" "$UPSTREAM" 2>/dev/null \
| awk '/service =/ {sub(/\.$/, "", $7); print $4, $6, $7}' | sort -n | head -3)
new=""
n=0
while read -r prio port target; do
[ -n "$prio" ] || continue
case "$port" in 5061) ;; *) echo "ignoring answer: unexpected port $port for $target"; exit 1 ;; esac
case "$target" in *.edns.t-ipnet.de) ;; *) echo "ignoring answer: unexpected target $target"; exit 1 ;; esac
ip=$(nslookup "$target" "$UPSTREAM" 2>/dev/null | awk '/^Name:/ {f=1} f && /^Address/ {print $NF; exit}')
[ -n "$ip" ] || { echo "ignoring answer: $target does not resolve"; exit 1; }
new="${new}${prio} ${target}
"
n=$((n + 1))
done <<EOF
$answer
EOF
[ "$n" -ge 1 ] || { echo "no usable SRV answer from $UPSTREAM — pin unchanged"; exit 1; }
current=""
for i in 1 2 3; do
t=$(uci -q get dhcp.telekom_srv$i.target) || continue
current="${current}$(uci -q get dhcp.telekom_srv$i.class) ${t}
"
done
if [ "$new" = "$current" ]; then
exit 0
fi
if [ "$DRY" = 1 ]; then
echo "current pin:"; echo "$current"
echo "Telekom answer (would become the pin):"; echo "$new"
exit 0
fi
for i in 1 2 3; do uci -q delete dhcp.telekom_srv$i; done
i=0
echo "$new" | while read -r prio target; do
[ -n "$prio" ] || continue
i=$((i + 1))
uci set dhcp.telekom_srv$i=srvhost
uci set dhcp.telekom_srv$i.srv="$SRV"
uci set dhcp.telekom_srv$i.target="$target"
uci set dhcp.telekom_srv$i.port=5061
uci set dhcp.telekom_srv$i.class="$prio"
uci set dhcp.telekom_srv$i.weight=0
done
uci commit dhcp
/etc/init.d/dnsmasq restart >/dev/null 2>&1
# waits for the last call to end; restarts at once when idle
/usr/sbin/asterisk -rx "core restart when convenient" >/dev/null 2>&1 &
OpenWrt dnsmasq gotcha #
dnsmasq’s filterwin2k option silently drops all SRV queries. It ships enabled by default in some configurations,
and pjproject depends on SRV for SIP registrar discovery, so registrations failed with cryptic DNS errors.
uci set dhcp.@dnsmasq[0].filterwin2k='0'
fixes it.
Conclusion #
TLS on a Telekom consumer All-IP line works fine with Asterisk — the hurdles are all about URI scheme purity (sip:
everywhere, TLS only as transport), SDES media, and keeping your requests routed to the same SBC node your registration
lives on. The dnsmasq SRV pin is conceptually somewhat involved, but avoids the 403 Forbidden errors without further
information, and so far works rock solid.