Browse Source

asterisk: SIP PBX for local phones, with their TFTP server

Asterisk 22 (LTS) and dnsmasq from the Alpine 3.24 packages, in one image
built locally. Asterisk runs as UID 10036 with PJSIP only and an explicit
module list; dnsmasq serves the provisioning files of the phones over TFTP
from the virtiofs mount, where the host can write firmware images that do
not fit in an Ignition file.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UJKweojs6NK5svZwRgQjQH
main
Nicolas Massé 2 weeks ago
parent
commit
897b484196
  1. 10
      cookbooks/asterisk/Makefile
  2. 116
      cookbooks/asterisk/README.md
  3. 10
      cookbooks/asterisk/asterisk-build.timer
  4. 50
      cookbooks/asterisk/asterisk-tftp.container
  5. 28
      cookbooks/asterisk/asterisk.build
  6. 58
      cookbooks/asterisk/asterisk.container
  7. 12
      cookbooks/asterisk/asterisk.target
  8. 15
      cookbooks/asterisk/config/container/Containerfile
  9. 3
      cookbooks/asterisk/config/etc/acl.conf
  10. 20
      cookbooks/asterisk/config/etc/asterisk.conf
  11. 4
      cookbooks/asterisk/config/etc/ccss.conf
  12. 3
      cookbooks/asterisk/config/etc/cdr.conf
  13. 3
      cookbooks/asterisk/config/etc/cel.conf
  14. 3
      cookbooks/asterisk/config/etc/features.conf
  15. 6
      cookbooks/asterisk/config/etc/logger.conf
  16. 51
      cookbooks/asterisk/config/etc/modules.conf
  17. 4
      cookbooks/asterisk/config/etc/pjproject.conf
  18. 5
      cookbooks/asterisk/config/etc/rtp.conf
  19. 4
      cookbooks/asterisk/config/etc/udptl.conf
  20. 11
      cookbooks/asterisk/config/examples/etc/extensions.conf
  21. 48
      cookbooks/asterisk/config/examples/etc/pjsip.conf
  22. 9
      cookbooks/asterisk/overlay.bu
  23. 11
      cookbooks/asterisk/tmpfiles.d/asterisk.conf

10
cookbooks/asterisk/Makefile

@ -0,0 +1,10 @@
##
## Makefile for Asterisk quadlet
##
# Asterisk and its TFTP server run as UID 10036 (asterisk) / GID 10000 (itix-svc) on the host
PROJECT_UID = 10036
PROJECT_GID = 10000
# Include common Makefile
include ../../scripts/common.mk

116
cookbooks/asterisk/README.md

@ -0,0 +1,116 @@
# Podman Quadlet: Asterisk
## Overview
[Asterisk](https://www.asterisk.org/) is an open source PBX. This cookbook runs it as a SIP server for
phones on the local network, together with the TFTP server the phones fetch their configuration and
firmware from.
This cookbook:
- Builds a container image locally, from Alpine Linux 3.24, with the Asterisk 22 (LTS) and dnsmasq
packages of the distribution.
- Runs Asterisk directly as a dedicated, unprivileged UID/GID (no user namespace mapping), with
PJSIP as its only channel driver.
- Runs dnsmasq as a TFTP server only (no DNS, no DHCP), from the same image, as the same user.
- Loads an explicit list of Asterisk modules: what calls between SIP phones need, and nothing else.
- Includes a timer to rebuild the container image daily.
It does not provide voicemail, call recording, conferencing, a trunk to an operator, nor a web
interface.
## Prerequisites
- The `base` cookbook (implicit dependency), for the virtiofs mount.
- A virtiofs mount on `/var/lib/virtiofs/data`, which holds the TFTP root.
- 1 vCPU and 1 GB of RAM are enough for a household (not measured beyond that).
- Network access at the first boot, to pull `docker.io/library/alpine:3.24` and the Alpine packages.
## Files provided by the operator
| Path | Content |
|------|---------|
| `/etc/quadlets/asterisk/etc/pjsip.conf` | Transport, endpoints, authentication and AOR of each phone. Required. |
| `/etc/quadlets/asterisk/etc/extensions.conf` | Dialplan. Required. |
| `/etc/quadlets/asterisk/etc/indications.conf` | Call progress tones of the country. Optional: without it, Asterisk logs a warning and uses no tones of its own; the phones play theirs. |
| `/var/lib/virtiofs/data/asterisk/tftp/` | Configuration files and firmware images of the phones, as each vendor names them. Optional: the TFTP server also runs on an empty directory, so that files written later are served without a restart. |
Asterisk does not start without `pjsip.conf` and `extensions.conf`. Examples of both are in
`config/examples/etc/`.
The TFTP root is on the virtiofs mount rather than under `/etc/quadlets`, so that the host can write
it: a firmware image weighs tens of megabytes, which an Ignition file cannot carry. The files must be
readable by UID 10036.
The other files of `/etc/quadlets/asterisk/etc/` are part of the cookbook. Some are empty on purpose:
Asterisk reads them at startup whether or not the feature is used, and logs an error when they are
missing.
## Ports
| Port | Protocol | Service |
|------|----------|---------|
| 5060 | UDP | SIP |
| 10000-10999 | UDP | RTP (set in `rtp.conf`) |
| 69 | UDP | TFTP |
Both containers use host networking.
## UID and GID
| UID | GID | Usage |
|-----|-----|-------|
| 10036 | 10000 | Asterisk and dnsmasq |
## Usage
In a separate terminal, follow the logs.
```sh
sudo make tail-logs
```
Install the Podman Quadlets and start Asterisk.
```sh
sudo make clean install
```
The **asterisk-build.service** builds the image, then **asterisk.service** starts. Check the
endpoints and the dialplan:
```sh
sudo podman exec asterisk asterisk -rx "pjsip show endpoints"
sudo podman exec asterisk asterisk -rx "dialplan show"
```
Make a phone ring without a second phone (`res_clioriginate`):
```sh
sudo podman exec asterisk asterisk -rx "channel originate PJSIP/101 application Wait 10"
```
Trace the SIP messages:
```sh
sudo podman exec asterisk asterisk -rx "pjsip set logger on"
```
Finally, remove the quadlets, their configuration and their data.
```sh
sudo make uninstall clean
```
## Known messages
At every start, `res_pjsip` logs twice: `There are no local system nameservers configured, resorting
to system resolution`. Measured on Alpine 3.24 with a populated `/etc/resolv.conf`. Name resolution
still works through the system resolver, and calls between phones addressed by IP do not use it.
## Container image updates
The daily rebuild pulls a newer `alpine:3.24` when there is one, and the containers restart on the
new image through `AutoUpdate=local`. A rebuild reuses the cached package layer as long as the base
image is unchanged: an Asterisk update published in the Alpine repository between two releases of
the base image is not picked up until the next one.

10
cookbooks/asterisk/asterisk-build.timer

@ -0,0 +1,10 @@
[Unit]
Description=Rebuild the Asterisk container image
PartOf=asterisk.target
[Timer]
OnCalendar=daily
Persistent=true
[Install]
WantedBy=asterisk.target

50
cookbooks/asterisk/asterisk-tftp.container

@ -0,0 +1,50 @@
[Unit]
Description=Asterisk - TFTP server for the provisioning of the phones
Documentation=https://thekelleys.org.uk/dnsmasq/docs/dnsmasq-man.html
Wants=asterisk-build.service
After=network.target asterisk-build.service var-lib-virtiofs-data.mount
Requires=var-lib-virtiofs-data.mount
# The provisioning files are operator-provided, on the precious-data mount so
# that the host can write them -- firmware images do not fit in an Ignition
# file. No condition on their presence: the host may write them after the
# first boot, and a unit skipped by its condition is not started again when
# the files arrive. An empty TFTP root only answers "file not found".
# Start/stop this unit when the target is started/stopped
PartOf=asterisk.target
[Container]
ContainerName=asterisk-tftp
Image=localhost/asterisk:latest
AutoUpdate=local
User=10036
Group=10000
# TFTP listens on 69/udp, a privileged port.
AddCapability=CAP_NET_BIND_SERVICE
# Host networking: TFTP answers from an ephemeral port, which port publishing
# does not follow.
Network=host
# dnsmasq as a TFTP server and nothing else:
# --port=0 disables DNS, and no dhcp-range means no DHCP;
# --conf-file=/dev/null ignores the configuration file of the image;
# --pid-file= (empty) writes no PID file, which this user could not write.
Entrypoint=/usr/sbin/dnsmasq
Exec=--keep-in-foreground --conf-file=/dev/null --pid-file= --port=0 --log-facility=- --enable-tftp --tftp-root=/var/lib/virtiofs/data/asterisk/tftp
# Read-only: the phones fetch, they never upload.
Volume=/var/lib/virtiofs/data/asterisk/tftp:/var/lib/virtiofs/data/asterisk/tftp:ro,z
[Service]
Restart=always
RestartSec=10
TimeoutStartSec=300
TimeoutStopSec=30
[Install]
WantedBy=asterisk.target

28
cookbooks/asterisk/asterisk.build

@ -0,0 +1,28 @@
[Unit]
Description=Build of the Asterisk container image
Documentation=https://www.asterisk.org/
Wants=network-online.target
After=network-online.target
# Keep trying. A build that fails because the network is not there yet would
# otherwise never run again, and the default start limit (5 attempts in 10 s)
# turns the unit "failed" for good. Same reasoning as the image units of the
# other cookbooks.
StartLimitIntervalSec=0
# Stopping the target stops a build that is still retrying.
PartOf=asterisk.target
[Build]
File=/etc/quadlets/asterisk/container/Containerfile
ImageTag=localhost/asterisk:latest
SetWorkingDirectory=/etc/quadlets/asterisk/container
# Pull the base image only when the registry has a newer one. Pull errors are
# ignored when a local image exists, so a rebuild without network keeps the
# current image instead of failing.
Pull=newer
[Service]
Restart=on-failure
RestartSec=60

58
cookbooks/asterisk/asterisk.container

@ -0,0 +1,58 @@
[Unit]
Description=Asterisk PBX
Documentation=https://docs.asterisk.org/
# Wants= and not Requires= on the build: a failed build -- no network at boot --
# must not keep a container whose image is already in local storage from
# starting. A genuinely missing image fails the container, and Restart= below
# retries until the build has produced it.
Wants=asterisk-build.service
After=network.target asterisk-build.service
# The SIP endpoints and the dialplan are operator-provided: without them,
# Asterisk would start and accept no phone.
ConditionPathExists=/etc/quadlets/asterisk/etc/pjsip.conf
ConditionPathExists=/etc/quadlets/asterisk/etc/extensions.conf
# Start/stop this unit when the target is started/stopped
PartOf=asterisk.target
[Container]
ContainerName=asterisk
Image=localhost/asterisk:latest
AutoUpdate=local
# No need for root: SIP listens on 5060 and RTP on 10000-10999, all above 1024.
User=10036
Group=10000
# Host networking: RTP takes a range of a thousand UDP ports, which port
# publishing handles badly, and SIP must see the real addresses of the phones.
Network=host
# Configuration (read-only). The etc/ sub-directory only, never the whole
# /etc/quadlets/asterisk: ":Z" relabels what it mounts, and the build context
# next to it must stay readable by the build.
Volume=/etc/quadlets/asterisk/etc:/etc/asterisk:ro,Z
# Internal database and spool. Nothing precious: no voicemail, no recordings.
Volume=/var/lib/quadlets/asterisk:/var/lib/quadlets/asterisk:Z
# Control socket, used by "asterisk -rx" and by the health check.
Tmpfs=/run/asterisk:rw,mode=1777
# Health check: the CLI answers only when the core is up.
HealthCmd=asterisk -rx "core show uptime"
HealthInterval=30s
HealthTimeout=10s
HealthStartPeriod=30s
HealthRetries=3
[Service]
Restart=always
RestartSec=10
TimeoutStartSec=300
TimeoutStopSec=30
[Install]
WantedBy=asterisk.target

12
cookbooks/asterisk/asterisk.target

@ -0,0 +1,12 @@
[Unit]
Description=Asterisk Service Target
Documentation=man:systemd.target(5)
Requires=asterisk.service asterisk-build.timer
Wants=asterisk-tftp.service
After=asterisk.service asterisk-tftp.service asterisk-build.timer
# Allow isolation - can stop/start this target independently
AllowIsolate=yes
[Install]
WantedBy=multi-user.target

15
cookbooks/asterisk/config/container/Containerfile

@ -0,0 +1,15 @@
FROM docker.io/library/alpine:3.24
# Asterisk 22 (the LTS branch), PJSIP included: Alpine bundles pjproject in the
# main package. dnsmasq serves the provisioning files of the phones over TFTP,
# from the same image, so that a single build keeps both up to date.
RUN apk add --no-cache asterisk dnsmasq
# /etc/asterisk is empty: the sample files are in asterisk-sample-config, which
# is not installed. The configuration is mounted read-only there at run time.
# SIP signalling. RTP uses 10000-10999/udp, as set in rtp.conf.
EXPOSE 5060/udp
ENTRYPOINT [ "/usr/sbin/asterisk" ]
CMD [ "-f", "-n", "-vv" ]

3
cookbooks/asterisk/config/etc/acl.conf

@ -0,0 +1,3 @@
; Core configuration files Asterisk reads at startup whether or not the
; feature is used. Present and empty, so that a missing file does not log an
; error at every start.

20
cookbooks/asterisk/config/etc/asterisk.conf

@ -0,0 +1,20 @@
; Asterisk runs as UID 10036, which owns nothing in the image: every directory
; it writes to is moved out of the image, onto a path the container mounts.
;
; - /var/lib/quadlets/asterisk: the internal database and the spool, mounted
; from the host. Nothing there is precious: no voicemail, no recordings.
; - /run/asterisk: the control socket, on a tmpfs.
;
; The read-only directories stay where the Alpine package puts them.
[directories]
astetcdir => /etc/asterisk
astmoddir => /usr/lib/asterisk/modules
astvarlibdir => /var/lib/asterisk
astdbdir => /var/lib/quadlets/asterisk
astkeydir => /var/lib/asterisk
astdatadir => /var/lib/asterisk
astagidir => /var/lib/asterisk/agi-bin
astspooldir => /var/lib/quadlets/asterisk/spool
astrundir => /run/asterisk
astlogdir => /var/lib/quadlets/asterisk/log
astsbindir => /usr/sbin

4
cookbooks/asterisk/config/etc/ccss.conf

@ -0,0 +1,4 @@
; Core configuration files Asterisk reads at startup whether or not the
; feature is used. Present and empty, so that a missing file does not log an
; error at every start.
[general]

3
cookbooks/asterisk/config/etc/cdr.conf

@ -0,0 +1,3 @@
; No call detail records: nothing reads them.
[general]
enable = no

3
cookbooks/asterisk/config/etc/cel.conf

@ -0,0 +1,3 @@
; No channel event logging: nothing reads it.
[general]
enable = no

3
cookbooks/asterisk/config/etc/features.conf

@ -0,0 +1,3 @@
; Default transfer and pickup settings. The phones transfer with SIP REFER,
; handled by res_pjsip_refer, not with DTMF feature codes.
[general]

6
cookbooks/asterisk/config/etc/logger.conf

@ -0,0 +1,6 @@
; Everything goes to the console, i.e. to the standard output of the container,
; i.e. to the journal. No log file: there is no log rotation in the container.
[general]
[logfiles]
console => notice,warning,error,verbose

51
cookbooks/asterisk/config/etc/modules.conf

@ -0,0 +1,51 @@
; An explicit list rather than autoload: autoload brings some three hundred
; modules, most of which have no configuration and decline to load with an
; error, which buries the messages that matter.
;
; What is listed is what calls between local SIP phones need: PJSIP, RTP, the
; static dialplan, Dial() and the codecs the phones offer. A feature that needs
; another module needs a line here.
[modules]
autoload = no
; Configuration storage for PJSIP
load => res_sorcery_config.so
load => res_sorcery_memory.so
load => res_sorcery_astdb.so
; PJSIP: transport, registration, authentication, calls
load => res_pjproject.so
load => res_pjsip.so
load => res_pjsip_session.so
load => res_pjsip_pubsub.so
load => res_pjsip_authenticator_digest.so
load => res_pjsip_endpoint_identifier_user.so
load => res_pjsip_registrar.so
load => res_pjsip_sdp_rtp.so
load => res_pjsip_caller_id.so
load => res_pjsip_dtmf_info.so
load => res_pjsip_rfc3326.so
load => res_pjsip_diversion.so
load => res_pjsip_header_funcs.so
; Transfer button of the phones (SIP REFER)
load => res_pjsip_refer.so
load => res_pjsip_logger.so
load => chan_pjsip.so
; Media
load => res_rtp_asterisk.so
load => res_timing_timerfd.so
load => bridge_simple.so
load => bridge_native_rtp.so
load => codec_g722.so
load => codec_alaw.so
load => codec_ulaw.so
load => codec_a_mu.so
; Dialplan
load => pbx_config.so
load => app_dial.so
load => func_callerid.so
; "channel originate" from the CLI, to test a phone without a second one
load => res_clioriginate.so

4
cookbooks/asterisk/config/etc/pjproject.conf

@ -0,0 +1,4 @@
; Core configuration files Asterisk reads at startup whether or not the
; feature is used. Present and empty, so that a missing file does not log an
; error at every start.

5
cookbooks/asterisk/config/etc/rtp.conf

@ -0,0 +1,5 @@
; One thousand ports, i.e. five hundred simultaneous calls: far more than a
; household needs, and a range small enough to be named in a firewall rule.
[general]
rtpstart = 10000
rtpend = 10999

4
cookbooks/asterisk/config/etc/udptl.conf

@ -0,0 +1,4 @@
; Core configuration files Asterisk reads at startup whether or not the
; feature is used. Present and empty, so that a missing file does not log an
; error at every start.
[general]

11
cookbooks/asterisk/config/examples/etc/extensions.conf

@ -0,0 +1,11 @@
; Example: the two phones of pjsip.conf call each other. Any other number is
; rejected: there is no route out.
[general]
static = yes
writeprotect = yes
[phones]
exten => _10[12],1,Dial(PJSIP/${EXTEN},30)
same => n,Hangup()
exten => _X.,1,Hangup(1)

48
cookbooks/asterisk/config/examples/etc/pjsip.conf

@ -0,0 +1,48 @@
; Example: two phones, 101 and 102, registering over UDP.
; The operator provides the real file, with real secrets.
[transport-udp]
type = transport
protocol = udp
bind = 0.0.0.0:5060
[phone](!)
type = endpoint
context = phones
disallow = all
allow = g722
allow = alaw
allow = ulaw
direct_media = no
dtmf_mode = rfc4733
[phone-auth](!)
type = auth
auth_type = userpass
[phone-aor](!)
type = aor
max_contacts = 1
remove_existing = yes
qualify_frequency = 60
[101](phone)
auth = 101
aors = 101
callerid = "Phone 101" <101>
[101](phone-auth)
username = 101
password = example-only-101
[101](phone-aor)
[102](phone)
auth = 102
aors = 102
callerid = "Phone 102" <102>
[102](phone-auth)
username = 102
password = example-only-102
[102](phone-aor)

9
cookbooks/asterisk/overlay.bu

@ -0,0 +1,9 @@
variant: fcos
version: 1.4.0
passwd:
users:
- name: asterisk
uid: 10036
gecos: Asterisk PBX
home_dir: /var/lib/quadlets/asterisk
primary_group: itix-svc

11
cookbooks/asterisk/tmpfiles.d/asterisk.conf

@ -0,0 +1,11 @@
# Internal database, spool and logs of Asterisk. Not precious: no voicemail,
# no recordings.
d /var/lib/quadlets/asterisk 0750 10036 10000 -
d /var/lib/quadlets/asterisk/spool 0750 10036 10000 -
d /var/lib/quadlets/asterisk/log 0750 10036 10000 -
# Provisioning files of the phones, served over TFTP. Operator-provided, on the
# precious-data mount so that the host can write them. The "$" marks it for
# removal on `systemd-tmpfiles --purge` (i.e. `make uninstall`).
d$ /var/lib/virtiofs/data/asterisk 0750 10036 10000 -
d$ /var/lib/virtiofs/data/asterisk/tftp 0750 10036 10000 -
Loading…
Cancel
Save