This (non official) repository provides dockerized Asterisk PBX.
- Asterisk powering IP PBX systems and VoIP gateways
- PrivateDial, customizable Asterisk configuration
- WebSMS, send and receive messages, SMS, over HTTP
- AutoBan, a built in intrusion detection and prevention system
- Additionally provide the G.729 and G.723.1 audio codecs
- Small image size based on Alpine Linux
- Demo based on
docker-compose.yml
andMakefile
files - Automatic integration of Let’s Encrypt TLS certificates using the reverse proxy Traefik
- Persistent storage facilitated by configuration and run data being consolidated under
/srv
- Container audio using the pulse socket of the host
- Use runit, providing an init scheme and service supervision
- Health check
- Log directed to docker daemon with configurable level
- Multi-staged build providing the images
mini
,base
,full
andxtra
The MAJOR.MINOR.PATCH SemVer
is used. In addition to the three number version number you can use two or
one number versions numbers, which refers to the latest version of the
sub series. The tag latest
references the build based on the latest commit to the repository.
The mlan/asterisk
repository contains a multi staged built. You select which build using the appropriate tag from mini
, base
, full
and xtra
. The image with the tag mini
only contains Asterisk itself.
The base
tag also include support for TLS, logging, WebSMS and AutoBan. full
adds support for console audio. The xtra
tag includes all Asterisk packages.
To exemplify the usage of the tags, lets assume that the latest version is 1.0.0
. In this case latest
, 1.0.0
, 1.0
, 1
, full
, full-1.0.0
, full-1.0
and full-1
all identify the same image.
There are many things to consider when configuring Asterisk and its components. We discuss some fundamentals here and in the separate documentation for the add-ons.
If you want to test the image right away, probably the best way is to clone the github repository and run the demo therein.
git clone https://github.com/mlan/docker-asterisk.git
An example of how to configure an VoIP SIP server using docker compose is given below.
name: demo
services:
tele:
image: mlan/asterisk
network_mode: bridge # Only here to help testing
cap_add:
- sys_ptrace # Only here to help testing
- net_admin # Allow NFT, used by AutoBan
- net_raw # Allow NFT, used by AutoBan
ports:
- "${SMS_PORT-8080}:${WEBSMSD_PORT:-80}" # WEBSMSD port mapping
- "5060:5060/udp" # SIP UDP port
- "5060:5060" # SIP TCP port
- "5061:5061" # SIP TLS port
- "10000-10099:10000-10099/udp" # RTP ports
environment:
- SYSLOG_LEVEL=${SYSLOG_LEVEL-4} # Logging
- HOSTNAME=${TELE_SRV-tele}.${DOMAIN-docker.localhost}
- PULSE_SERVER=unix:/run/pulse/socket # Use host audio
- PULSE_COOKIE=/run/pulse/cookie # Use host audio
- WEBSMSD_PORT=${WEBSMSD_PORT-80} # WEBSMSD internal port
volumes:
- tele-conf:/srv # Persistent storage
- ./pulse:/run/pulse:rshared # Use host audio
- /etc/localtime:/etc/localtime:ro # Use host timezone
volumes:
tele-conf: # Persistent storage
This repository contains a demo
directory which hold the docker-compose.yml
file as well as a Makefile
which might come handy. From within the demo
directory you can start the container simply by typing:
make up
The you can connect to the asterisk command line interface (CLI) running inside the container by typing
make cli
From the Asterisk CLI you can type
pjsip show endpoints
to see the endpoints (soft phones) that are configured in the /etc/asterisk/pjsip_endpoint.conf
configuration file that comes with the image by default.
When you are done testing you can destroy the test container by typing
make destroy
Despite the fact that Asterisk is configured using configuration files, there is a handful of environmental variables that controls the behavior of services within the mlan/asterisk
container. These services are logging, the management of TLS certificates, and the WebSMS add-on.
Variable | Default | Description |
---|---|---|
SYSLOG_LEVEL | 4 | Logging level, from 0 to 8. 0 is the least, whereas, 8 is the most log outputs. |
SYSLOG_OPTIONS | -SDt | S: smaller output, D: drop duplicates, t: Strip client-generated timestamps. |
ACME_FILE | /acme/acme.json | File that contains TLS certificates, provided by Let's encrypt using Traefik. |
HOSTNAME | $(hostname) | Used to identify the relevant TLS certificates in ACME_FILE. |
TLS_CERTDAYS | 30 | Self-signed TLS certificate validity duration in days. |
TLS_KEYBITS | 2048 | Self-signed TLS key length in bits. |
WEBSMSD_PORT | 80 | PHP web server port, used by WebSMS. Undefined or non-numeric, will disable the PHP web server. |
Asterisk and its modules are configured using several configuration files which are typically found in /etc/asterisk
. The /mlan/asterisk
image includes a collection of sample configuration files which can serve as starting point for your system.
Some of the configuration files provided does not contain any user specific data and might initially be left unmodified. These files are:
File name | Description |
---|---|
alsa.conf | Open Sound System (ALSA) console driver configuration |
asterisk.conf | Asterisk global configuration including; debug, run-as-user and directory structure |
ccss.conf | Call Completion Supplementary Services configuration |
cli_aliases.conf | Asterisk Command Line Interface aliases |
features.conf | Call Features (transfer, monitor, etc) configuration |
indications.conf | Location specific tone indications |
logger.conf | Logging configuration |
modules.conf | Module Loader configuration |
musiconhold.conf | Music on Hold configuration |
pjproject.conf | Common pjproject options |
rtp.conf | RTP configuration including port range |
The configuration files mentioned above are perhaps not the ones that require the most attention. The configuration files defining key aspects of the Asterisk server — like for instance, the call flow and SIP trunk and phone details — is the concern of the add-on PrivateDial. Please refer to its separate documentation for details.
By default, docker will store the configuration and run data within the container. This has the drawback that the configuration and state of the applications are lost together with the container, should it be deleted. It can therefore be a good idea to use docker volumes and mount the configuration and spool directories directories on such volumes so that the data will survive a container deletion.
To facilitate such approach, to achieve persistent storage, the configuration and spool directories of the services has been consolidated under /srv
. The applications running inside the container still finds files in their usual locations since symbolic links are placed in these locations pointing back to /srv
. With this approach simply mounting a docker volume at /srv
let you keep application configuration and state persistent.
The volume tele-conf
in the demo, which uses docker compose
, described above, achieves this. Mounting a volume using the docker CLI, can look like this:
docker run -d -v tele-conf:/srv ... mlan/asterisk
The mlan/asterisk
image contains sample configuration files placed in a seeding directory. The actual configuration directory is empty. When the container starts, the configuration directory, etc/asterisk
, is scanned. If it is found to be empty, sample configuration files from the seeding directory are copied to the configuration directory.
The seeding procedure will leave any existing configuration untouched. If configuration files are found, nothing is copied or modified during start up. Only when etc/asterisk
is found to be empty, will seeding files be copied. This behavior should keep your conflagration safe also when upgrading to a new version of the mlan/asterisk
image. Should a new version of the mlan/asterisk
image come with interesting updates to any sample configuration files, it needs to manually be copied or merged with the present configuration files.
The level of output for logging is in the range of 0 to 8. 1 means emergency logging only, 2 for alert messages, 3 for critical messages only, 4 for error or worse, 5 for warning or worse, 6 for notice or worse, 7 for info or worse, 8 debug. Default: SYSLOG_LEVEL=4
The mlan/asterisk
repository contains add-ons that utilizes and extends the already impressive capabilities of Asterisk.
PrivateDial is a suite of Asterisk configuration files. This configuration is tailored to residential use cases, supporting the capabilities of mobile smart phones, that is, voice, video, instant messaging or SMS, and voice mail delivered by email.
It uses the PJSIP channel driver and therefore natively support simultaneous connection of several soft-phones to each user account/endpoint.
The underlying design idea is to separate the dial plan functionality from the user data. To achieve this all user specific data has been pushed out from the main extensions.conf
file.
AutoBan is an intrusion detection and prevention system which is built-in the mlan/asterisk
image. The intrusion detection is achieved by Asterisk itself. Asterisk generates security events which AutoBan listens to on the AMI interface.
When security events occurs AutoBan start to monitor the source IP address. Should repeated security events occur intrusion prevention is activated. Intrusion prevention is achieved by AutoBan asking the Linux kernel firewall nftables to drop packages from offending source IP addresses.
Asterisk supports SMS to be sent and received using the extended SIP method; MESSAGE, natively. Still many Internet Telephony Service Providers (ITSP) does not support this method, but instead used a web API based on HTTP requests. This leaves your Asterisk server without a mechanisms to exchange SMS externally.
The WebSMS service bridges this limitation, with the help of two components. The first, websmsd
, which waits for incoming SMS to be sent from your ITSP and once received, forwards it to Asterisk. The second, websms
, is used by Asterisk to send outgoing SMS to your ITSP.
WebSMS uses PHP's integrated web server. The environment variable WEBSMSD_PORT=80
determinate which port the web server listens to. If WEBSMSD_PORT
is undefined or non-numeric the PHP web server is disabled, and consequently, WebSMS too. Disabling the web server might be desired in scenarios when the container runs in host mode and there are concerns with port number clashes with other services running on the host.
SIP networking is quite complex and there is many things that can go wrong. We try to offer some guidance by discussing some fundamentals here.
The Session Initiation Protocol (SIP) is a signaling protocol used for initiating, maintaining, and terminating real-time sessions that include voice, video and messaging applications.
SIP is designed to be independent of the underlying transport layer protocol, and can be used with the User Datagram Protocol (UDP), the Transmission Control Protocol (TCP), and the Stream Control Transmission Protocol (SCTP). For secure transmissions of SIP messages over insecure network links, the protocol may be encrypted with Transport Layer Security (TLS). For the transmission of media streams (voice, video) the Session Description Protocol (SDP) payload carried in SIP messages typically employs the Real-time Transport Protocol (RTP) or the Secure Real-time Transport Protocol (SRTP).
When sending an audio stream it is far better to lose a packet than to have a packet retransmitted, causing excessive jitter in the packet timing. Audio is real-time and requires a protocol like UDP to work correctly. Packet loss does not break audio, it only reduces the quality. Therefore it is no surprise that RTP is built on top of UDP; an connection-less protocol.
TCP is a connection-oriented protocol as it establishes an end to end connection between computers before transferring the data. TCP is therefore, by contrast, ideal for SIP signaling. The somewhat unexpected fact that most SIP communication uses UDP should not discourage you from choosing TCP when possible. TCP often provide more reliable contacts with endpoints/phones than UDP.
TLS, operating as an application protocol layered directly over TCP, protects against attackers who try to listen on the signaling link but it does not provide end-to-end security to prevent espionage and law enforcement interception, as the encryption is only hop-by-hop and every single intermediate proxy has to be trusted.
The media streams which are separate connections from the signaling stream, may be encrypted with the Secure Real-time Transport Protocol (SRTP). The key exchange for SRTP is performed with SDES, or with DTSL.
SIP traffic typically use the port numbers 5060 or 5061. Port 5060 is commonly used for non-encrypted signaling traffic, i.e., TCP or UDP, whereas port 5061 is used for encrypted, TLS, traffic.
RTP uses a dynamic port range generally between 10000-20000. This range can usually be customized on the client to suit differing firewall configurations or other concerns.
When publishing ports on a docker container, using the default bridge
networking, two things happen; routing rules are updated in the Linux kernel firewall and a proxy processes are stated. The current networking implementation in docker (19.03.8), built on the aged iptables and docker-proxy, cannot publish port ranges, but instead publish them one by one.
Normally RTP uses the port range 10000-20000, encompassing 10001 ports. This is problematic since, updating the firewall rules 10001 times and starting 10001 proxy processes can take unacceptable long time (minutes) and can cause the container startup to stall. Longterm this limitation will be addressed, since it is inevitable that docker networking will have to be redesigned. But right now we have to work around this imitation and we describe two ways to address this here.
First, you can use the host
network mode (docker run --network host …
). The host network mode does not use the docker-proxy and does not need to set up routing rules in the firewall. The downside is that a container stated in this way cannot communicate on any of the docker networks.
Second, you can stay with the default or user-defined bridge
mode and instead limit the RTP port range to something manageable say 10000-10099, or up to 10000-10999. This actually seems to work in practice, at least with some trunk providers (ITSP). The RTP port range is configured in rtp.conf
[general]
rtpstart = 10000
rtpend = 10099
Network address translation (NAT) is a method of remapping one IP address space into another by modifying the network address information in the IP header of packets while they are in transit across a traffic routing device. Network environments often results in NAT being used. On the one hand, the SIP server we deploy using mlan/asterisk
often uses a Docker bridge network, connecting Dockers local network with the one the host is connected to. On the other, SIP clients running on mobile phones often end up connect to remote local networks.
To provide SIP clients with the external network address of a server behind NAT it can explicitly be defined on the transport used which is configured in pjsip_transport.conf
[t_wan](!)
type = transport
bind = 0.0.0.0:5060
domain = example.com
external_signaling_address = sip.example.com
external_media_address = sip.example.com
For endpoints connected to remote local networks you need the following parameters which are defined in pjsip_wizard.conf
[_nat](!)
endpoint/rewrite_contact = yes
endpoint/direct_media = no
endpoint/rtp_symmetric = yes
endpoint/bind_rtp_to_media_address = yes
Strict RTP learning is not compatible with NAT. When enabled, RTP media packets that have passed NAT will be dropped, resulting in an one way audio experience. Disable strict RTP learning in rtp.conf
[general]
strictrtp = no
Sometimes there is a need for other more elaborate NAT traversal methods; ICE, STUN or TURN. A treatment of these is a little bit out of scope for this text.
Transport Layer Security (TLS) provides encryption for call signaling. A excellent guide for setting up TLS between Asterisk and a SIP client, involving creating key files, modifying Asterisk's SIP configuration to enable TLS, creating a SIP endpoint/user that's capable of TLS, and modifying the SIP client to connect to Asterisk over TLS, can be found here Secure Calling Tutorial.
The PrivateDial configuration is already set up to provide both UDP and TCP transport. TLS, SDES and DTSL SRTP are also prepared, but a TLS/SSL server certificate and key are needed for their activation. If the certificate and key do not exist when the container starts a self-signed certificate and private key will automatically be generated.
The private key length and self-signed certificate validity duration can be configured using the environment variables: TLS_KEYBITS=2048
and TLS_CERTDAYS=30
.
Let’s Encrypt provide free, automated, authorized certificates when you can demonstrate control over your domain. Automatic Certificate Management Environment (ACME) is the protocol used for such demonstration.
There are many agents and applications that supports ACME, e.g., certbot. The reverse proxy Traefik also supports ACME. mlan/asterisk
can use the TLS certificates Traefik has acquired.
The mlan/asterisk
image looks for the file ACME_FILE=/acme/acme.json
at container startup. If it is found certificates within this file are extracted. If the host or domain name of one of those certificates matches HOSTNAME=$(hostname)
or DOMAIN=${HOSTNAME#*.}
it will be used by the TLS transport. Moreover, the ACME_FILE
will be monitored and should it change the certificates will be exported anew. So when Traefik renews its certificates Asterisk will automatically also have access to the new certificate.
Once the certificates and keys have been updated, we run the command in the environment variable ACME_POSTHOOK="sv restart asterisk"
. Asterisk needs to be restarted to reload the transport, i.e., TLS parameters to be updated. If automatic restarting of Asterisk is not desired, set ACME_POSTHOOK=
to empty.
Using Traefik's certificates will work "out of the box" simply by making sure that the /acme
directory in the Traefik container is also is mounted in the mlan/asterisk
container.
docker run -d -v proxy-acme:/acme:ro mlan/asterisk
Note, if the target certificate Common Name (CN) or Subject Alternate Name (SAN) is changed the container needs to be restarted.
Attempts by attackers to crack SIP passwords and hijack SIP accounts are very common. Most likely the server will have to fend off thousands of attempts every day. here we mention three means to improve intrusion prevention; obscurity by using non-standard ports, SIP passwords strength, and AutoBan; an Intrusion Detection and Prevention System.
When using non-standard ports the amount of attacks drop significantly, so it might be considered whenever practical. When changing port numbers they need to be updated both for docker and asterisk. To exemplify, assume we want to use 5560 for UDP and TCP and 5561 for TLS, in which case we update the configuration in two places:
- docker or docker compose, e.g.,
docker run -p "5560-5561:5560-5561" -p "5560:5560/udp" …
- asterisk transport in
pjsip_transport.conf
(pjsip.conf
), e.g.bind = 0.0.0.0:5560
andbind = 0.0.0.0:5561
It’s recommended that the minimum strength of a password used in a SIP digests are at least 8 characters long, preferably 10 characters, and have characters that include lower and upper case alphabetic, a number and a non-alphabetic, non-numeric ASCII character, see SIP Password Security - How much is yours worth?
Asterisk natively provides several audio and video codec modules. Additionally the G.729 and G.723.1 audio codecs has been copied to the image. These are maintained by arkadijs/asterisk-g72x.
The mlan/asterisk
container supports two-way audio using PulseAudio. This allows you to use the Asterisk console channel to do some management or debugging. The audio stream is passed between the container and host by sharing the user's pulse UNIX socket.
The method described here was chosen since it allows audio to be enabled on an already running container. The method involves a directory ./pulse:/run/pulse:rshared
on the host being mounted in the container, see the compose example, and environment variables being set within the container, allowing pulse to locate the socket; PULSE_SERVER=unix:/run/pulse/socket
and cookie; PULSE_COOKIE=/run/pulse/cookie
. To arrange the pulse directory on the host, from a shell, run:
mkdir -p pulse
touch pulse/socket
Without additional steps, the bind mount and environment variables described above achieve nothing. This is fine since most of the time we are not interested in sharing audio with the container. But should there come a time when we want to enable the audio, we can do so in 3 simple steps: 1) Mount the user's pulse socket in the host directory ./pulse/socket
and, 2) copy the user's pulse cookie there too, ./pulse/cookie
. 3) Have asterisk, running inside the container, load the chan_alsa.so
module. From a shell running on the host, these steps are:
sudo mount --bind $(pactl info | sed '1!d;s/.*:\s*//g') pulse/socket
cp -f ${PULSE_COOKIE-$HOME/.config/pulse/cookie} pulse/cookie
docker compose exec $(SRV_NAME) asterisk -rx 'module load chan_alsa.so'
A limitation of this approach is that you need sudo/root access do be able to bind mount on the host. And, naturally, there needs to be a pulse server running on the host for any of this to work.
You can use the demo above, and once the container is running, enable audio by typing, from within the demo
directory:
make sound_enable
Now you can tryout any of the trivial sound checks, for example:
make sound_5
To disable audio, type:
make sound_disable
Here some implementation details are presented.
The mlan/asterisk
container use runit, providing an init scheme and service supervision, allowing multiple services to be started.
When the container is started, execution is handed over to the script docker-entrypoint.sh
. It has 4 stages; 0) register the SIGTERM signal (IPC) handler, which is programmed to run all exit scripts in DOCKER_EXIT_DIR=/etc/docker/exit.d
and terminate all services, 1) run all entry scripts in DOCKER_ENTRY_DIR=/etc/docker/entry.d
, 2) start services registered in /etc/service/
, 3) wait forever, allowing the signal handler to catch the SIGTERM and run the exit scripts and terminate all services.
The entry scripts are responsible for tasks like, seeding configurations, register services and reading state files. These scripts are run before the services are started.
There is also exit script that take care of tasks like, writing state files. These scripts are run when docker sends the SIGTERM signal to the main process in the container. Both docker stop
and docker kill --signal=TERM
sends SIGTERM.
The entry and exit scripts, discussed above, as well as other utility scrips are copied to the image during the build phase. The source file tree was designed to facilitate simple scanning, using wild-card matching, of source-module directories for files that should be copied to image. Directory names indicate its file types so they can be copied to the correct locations. The code snippet in the Dockerfile
which achieves this is show below.
COPY src/*/bin $DOCKER_BIN_DIR/
COPY src/*/entry.d $DOCKER_ENTRY_DIR/
COPY src/*/exit.d $DOCKER_EXIT_DIR/
COPY src/*/php $DOCKER_PHP_DIR/
COPY sub/*/php $DOCKER_PHP_DIR/
COPY src/*/config $DOCKER_SEED_CONF_DIR/
COPY src/*/nft $DOCKER_SEED_NFT_DIR/
COPY sub/*/module $DOCKER_DL_DIR/
There is also a mechanism for excluding files from being copied to the image from some source-module directories. Source-module directories to be excluded are listed in the file .dockerignore
. Since we don't want files from the module notused
we list it in the .dockerignore
file:
src/notused