Browse Source
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_01UJKweojs6NK5svZwRgQjQHmain
23 changed files with 484 additions and 0 deletions
@ -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 |
|||
@ -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. |
|||
@ -0,0 +1,10 @@ |
|||
[Unit] |
|||
Description=Rebuild the Asterisk container image |
|||
PartOf=asterisk.target |
|||
|
|||
[Timer] |
|||
OnCalendar=daily |
|||
Persistent=true |
|||
|
|||
[Install] |
|||
WantedBy=asterisk.target |
|||
@ -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 |
|||
@ -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 |
|||
@ -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 |
|||
@ -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 |
|||
@ -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" ] |
|||
@ -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. |
|||
@ -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 |
|||
@ -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] |
|||
@ -0,0 +1,3 @@ |
|||
; No call detail records: nothing reads them. |
|||
[general] |
|||
enable = no |
|||
@ -0,0 +1,3 @@ |
|||
; No channel event logging: nothing reads it. |
|||
[general] |
|||
enable = no |
|||
@ -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] |
|||
@ -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 |
|||
@ -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 |
|||
@ -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. |
|||
|
|||
@ -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 |
|||
@ -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] |
|||
@ -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) |
|||
@ -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) |
|||
@ -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 |
|||
@ -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…
Reference in new issue