Compare commits

...

2 Commits

Author SHA1 Message Date
Nicolas Massé 7addbfdc27 asterisk: create /var/lib/quadlets/asterisk from the package, not tmpfiles 2 weeks ago
Nicolas Massé 897b484196 asterisk: SIP PBX for local phones, with their TFTP server 2 weeks ago
  1. 18
      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. 15
      cookbooks/asterisk/tmpfiles.d/asterisk.conf

18
cookbooks/asterisk/Makefile

@ -0,0 +1,18 @@
##
## 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
# Spool and logs of Asterisk, created by the package and not by systemd-tmpfiles:
# see the comment in tmpfiles.d/asterisk.conf.
TARGET_FILES += $(TARGET_CHROOT)/var/lib/quadlets/asterisk/spool
TARGET_FILES += $(TARGET_CHROOT)/var/lib/quadlets/asterisk/log
# Include common Makefile
include ../../scripts/common.mk
$(TARGET_CHROOT)/var/lib/quadlets/asterisk/spool $(TARGET_CHROOT)/var/lib/quadlets/asterisk/log: $(TARGET_CHROOT)/var/lib/quadlets/asterisk
install -d -m 0750 -o $(PROJECT_UID) -g $(PROJECT_GID) $@

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

15
cookbooks/asterisk/tmpfiles.d/asterisk.conf

@ -0,0 +1,15 @@
# 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`).
#
# /var/lib/quadlets/asterisk is NOT declared here, and must not be. The package
# creates it, once. A "d" line adjusts the directory again at every run of
# systemd-tmpfiles, and resets its SELinux label to var_lib_t on the way. Such a
# run happens AFTER the containers start on the first boot of a Fedora CoreOS
# machine: rpm-ostree applies the qemu-guest-agent package live, then runs
# "systemd-tmpfiles --create --prefix=/var". Asterisk then loses access to the
# directory Podman had just relabeled, and every REGISTER fails with
# "ast_db_put: SQL logic error". Measured on 2026-10-25. The virtiofs mount is
# not affected: its label is fixed by the mount.
d$ /var/lib/virtiofs/data/asterisk 0750 10036 10000 -
d$ /var/lib/virtiofs/data/asterisk/tftp 0750 10036 10000 -
Loading…
Cancel
Save