asterisk: 24.0.0-rc3, TLS trunks qualified again

rc3 checks a qualify's certificate against the host name, so TLS trunks get
qualify_frequency = 60 and Test qualifies them. The upgrade check and
pnpm test:asterisk pass on rc3. Spec 01's copy of the Dockerfile is in sync
again, and the version test now compares all of it.
This commit is contained in:
iamdoubz committed 2026-10-28 17:11:18 -05:00
1 parent a0121d4975
commit 0a18d5354d
11 files changed
+40 -40

No files matched your search

@@ -140,7 +140,7 @@ endpoint = trunk-a1b2c3d4-0000-4000-8000-000000000004
[trunk-a1b2c3d4-0000-4000-8000-000000000004]
type = aor
contact = sip:chicago1.voip.ms:5061;transport=tls
qualify_frequency = 0
qualify_frequency = 60
[trunk-a1b2c3d4-0000-4000-8000-000000000004]
type = endpoint
+3 -5
View File
@@ -232,11 +232,9 @@ function trunkSections(t: SnapTrunk, snap: Snapshot): string {
section(`trunk-${id}`, [
'type = aor',
`contact = sip:${target}`,
// ponytail: Asterisk 24 checks a qualify's TLS certificate against the IP, not the name, so
// OPTIONS to voip.ms fail verify_server and the contact goes Unavailable (calls and
// REGISTER verify fine). No qualify over TLS: registration shows the trunk's health.
// Turn it back on when a qualify to chicago1.voip.ms:5061 passes.
`qualify_frequency = ${t.transport === 'tls' ? 0 : 60}`,
// Over TLS too since 24.0.0-rc3, which checks a qualify's certificate against the host name
// (rc2 used the IP and failed verify_server; upgrade-test.ts tlsQualifyByHostname).
'qualify_frequency = 60',
]),
section(`trunk-${id}`, [
'type = endpoint',
+3 -5
View File
@@ -213,12 +213,10 @@ export class LiveState extends EventEmitter<{ message: [LiveMessage] }> {
/** POST /api/trunks/:id/test: qualify now, then read registration and reachability. */
async testTrunk(
id: string,
{ waitMs = 1500, qualify = true } = {},
{ waitMs = 1500 } = {},
): Promise<{ live: TrunkLive; rttMs: number | null }> {
if (qualify) {
await this.ami.action({ Action: 'PJSIPQualify', Endpoint: `trunk-${id}` }).catch(() => {});
await new Promise((r) => setTimeout(r, waitMs)); // the OPTIONS answer arrives asynchronously
}
await this.ami.action({ Action: 'PJSIPQualify', Endpoint: `trunk-${id}` }).catch(() => {});
await new Promise((r) => setTimeout(r, waitMs)); // the OPTIONS answer arrives asynchronously
await this.refreshRegistrations();
const rttMs = await this.refreshReachable(id);
const live = this.trunks.get(id);
+6 -9
View File
@@ -208,21 +208,18 @@ export async function trunkRoutes(app: FastifyInstance) {
if (!t.enabled) throw badRequest('Turn the trunk on first');
if (!app.live.amiConnected)
throw new HttpError(503, 'unavailable', 'Asterisk is not connected');
// No OPTIONS over TLS (see the trunk's aor in generator/pjsip.ts).
const { live, rttMs } = await app.live.testTrunk(id, { qualify: t.transport !== 'tls' });
const { live, rttMs } = await app.live.testTrunk(id);
const reg = t.authMode === 'registration';
const detail =
reg && live.registration === 'registered'
? `Registered with ${t.serverHost}`
: reg && live.registration === 'rejected'
? `${t.serverHost} rejected the username or password`
: !reg && t.transport === 'tls'
? `Can't check a TLS trunk without registration from here: make a test call.`
: !live.reachable
? `${t.serverHost} doesn't answer. Check the server name and that this server can reach it on port ${t.serverPort}.`
: reg
? `${t.serverHost} answers but the registration hasn't completed yet. Try again in a minute.`
: `${t.serverHost} answers (IP authentication: no registration)`;
: !live.reachable
? `${t.serverHost} doesn't answer. Check the server name and that this server can reach it on port ${t.serverPort}.`
: reg
? `${t.serverHost} answers but the registration hasn't completed yet. Try again in a minute.`
: `${t.serverHost} answers (IP authentication: no registration)`;
return { registration: reg ? live.registration : 'unknown', rttMs, detail };
});
+4 -2
View File
@@ -15,12 +15,14 @@ describe('the pinned Asterisk', () => {
).toBe(VERIFIED_ASTERISK);
});
it('is the one spec 01 §2 names', () => {
it('is the one spec 01 §2 names, and its copy of the Dockerfile is the file', () => {
const spec = readFileSync(
new URL('../../../docs/spec/01-infrastructure.md', import.meta.url),
'utf8',
);
expect(spec).toContain(`**Pinned: \`${pinnedAsterisk()}\``);
expect(spec).toContain(`ARG ASTERISK_VERSION=${pinnedAsterisk()}`);
const copy = /^## 2\. [\s\S]*?^```dockerfile\n([\s\S]*?)^```$/m.exec(spec)?.[1];
const file = readFileSync(new URL('../../../asterisk/Dockerfile', import.meta.url), 'utf8');
expect(copy, 'spec 01 §2 has a copy of asterisk/Dockerfile: keep it the same').toBe(file);
});
});
+4 -4
View File
@@ -4,7 +4,7 @@
// `pnpm test:asterisk-upgrade` on the new image, act on each change it lists, then update this.
import { readFileSync } from 'node:fs';
export const VERIFIED_ASTERISK = '24.0.0-rc2';
export const VERIFIED_ASTERISK = '24.0.0-rc3';
/** The version asterisk/Dockerfile builds. */
export function pinnedAsterisk(): string {
@@ -57,11 +57,11 @@ export const FACTS = {
where: 'services/cdr.ts utc() adds the Z itself',
},
tlsQualifyByHostname: {
expect: false,
expect: true,
what: 'a qualify OPTIONS over TLS checks the certificate against the host name, not the IP',
where:
'generator/pjsip.ts sets qualify_frequency = 0 on TLS trunks because of this (08 §2.1); ' +
'24.0.0-rc3 says it fixes it ("Preserve target hostname for qualified OPTIONS destination")',
'generator/pjsip.ts qualifies TLS trunks and Test sends PJSIPQualify (08 §2.1). False on ' +
'24.0.0-rc2: a TLS trunk then showed as unreachable, so it had qualify_frequency = 0',
},
} as const;
+5 -3
View File
@@ -105,7 +105,7 @@ describe.skipIf(!hasTestDb || !hasOpenssl)('SIP TLS setting', () => {
expect(await warningCodes()).not.toContain('tls_cert');
});
it('a TLS trunk: no qualify, Test does not send OPTIONS, TLS stays on while it exists', async () => {
it('a TLS trunk: qualified like the others (24.0.0-rc3), TLS stays on while it exists', async () => {
const trunk = await req('POST', '/api/trunks', {
name: 'voip.ms TLS',
serverHost: '127.0.0.1',
@@ -119,13 +119,15 @@ describe.skipIf(!hasTestDb || !hasOpenssl)('SIP TLS setting', () => {
await t.app.generator.apply({ trigger: 'test' });
const { readFile } = await import('node:fs/promises');
const conf = await readFile(join(t.app.generator.dir, 'pjsip_selfphoneng.conf'), 'utf8');
expect(conf).toMatch(/contact = sip:127\.0\.0\.1:5061;transport=tls\nqualify_frequency = 0\n/);
expect(conf).toMatch(/contact = sip:127\.0\.0\.1:5061;transport=tls\nqualify_frequency = 60\n/);
expect(conf).toMatch(/media_encryption = sdes/);
expect(conf).toMatch(/verify_server = yes/);
t.live.amiConnected = true;
const test = await req('POST', `/api/trunks/${trunk.json().id}/test`);
expect(test.statusCode).toBe(200);
expect(t.ami.actions.filter((a) => a.Action === 'PJSIPQualify')).toEqual([]);
expect(t.ami.actions.filter((a) => a.Action === 'PJSIPQualify')).toEqual([
expect.objectContaining({ Endpoint: `trunk-${trunk.json().id}` }),
]);
const off = await req('PATCH', '/api/settings', { 'sip.tlsEnabled': false });
expect(off.statusCode).toBe(400);
expect(off.json().error.message).toMatch(/uses TLS/);
+1 -1
View File
@@ -1,6 +1,6 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
FROM debian:trixie-slim AS build
ARG ASTERISK_VERSION=24.0.0-rc2
ARG ASTERISK_VERSION=24.0.0-rc3
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential ca-certificates curl wget pkg-config autoconf automake libtool \
+5 -4
View File
@@ -142,10 +142,10 @@ missing or invalid, it logs a clear message and exits with code 1.
## 2. Asterisk image (`asterisk/Dockerfile`)
Build Asterisk **24 LTS from source**. Pin an exact version: the newest 24.x release on the day you write it, recorded here in
the same commit. **Pinned: `24.0.0-rc2` (2026-10-17).** The file in the repo is the source of truth; this copy is kept in sync.
the same commit. **Pinned: `24.0.0-rc3` (2026-10-28).** The file in the repo is the source of truth; this copy is kept in sync.
> **Asterisk 24 status (checked 2026-10-17):** Asterisk 24 is the LTS line, but
> only release candidates are published so far (24.0.0-rc2). Until 24.0.0 is final,
> only release candidates are published so far (24.0.0-rc3 on 2026-10-28). Until 24.0.0 is final,
> develop against the newest 24.0.0-rcN. Release candidates may sit under a
> different download folder than `releases/`, so check the directory listing. Switch to 24.0.0 (or newer
> 24.x) as soon as it is final, and don't ship v1.0 on a release candidate. If a module, option
@@ -164,7 +164,7 @@ that moved with what depends on it) and `pnpm test:asterisk`, change what moved,
```dockerfile
# SPDX-License-Identifier: AGPL-3.0-or-later
FROM debian:trixie-slim AS build
ARG ASTERISK_VERSION=24.0.0-rc2
ARG ASTERISK_VERSION=24.0.0-rc3
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential ca-certificates curl wget pkg-config autoconf automake libtool \
@@ -209,7 +209,8 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
COPY --from=build /out/ /
COPY conf/ /etc/asterisk/
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh && mkdir -p /etc/asterisk/selfphoneng /var/lib/asterisk/db /var/lib/asterisk/moh /var/run/asterisk \
COPY sp-vm-pincheck /usr/local/bin/sp-vm-pincheck
RUN chmod +x /entrypoint.sh /usr/local/bin/sp-vm-pincheck && mkdir -p /etc/asterisk/selfphoneng /var/lib/asterisk/db /var/lib/asterisk/moh /var/run/asterisk \
&& chown -R asterisk:asterisk /etc/asterisk /var/lib/asterisk /var/spool/asterisk /var/log/asterisk /var/run/asterisk
ENTRYPOINT ["/entrypoint.sh"]
```
+2 -2
View File
@@ -322,8 +322,8 @@ For `auth_mode = 'ip'`, leave out the `-auth` and `-reg` sections and the `outbo
(and `from_user` when there is no username).
Skip disabled trunks entirely.
For TCP/TLS trunks, `server_uri`, `client_uri` and the AOR `contact` get `;transport=tcp|tls`.
As built (M11-T2): TLS trunks get `qualify_frequency = 0` (Asterisk 24 checks a qualify's certificate
against the IP, see 08 §2.1); their health comes from the registration.
TLS trunks are qualified like the others (`qualify_frequency = 60`) since 24.0.0-rc3. On rc2 they
had `qualify_frequency = 0`, because rc2 checked a qualify's certificate against the IP (08 §2.1).
`identify` resolves `match` when the config loads, and an unresolvable name is an ERROR that drops the
trunk's identify (verified), so the API refuses a server name that doesn't resolve, and a transport
that isn't turned on in Settings → SIP.
+6 -4
View File
@@ -34,10 +34,12 @@
cert is missing or expires within 14 days.
- `transport-tls` has `verify_server = yes` + `ca_list_file = /etc/ssl/certs/ca-certificates.crt`:
the provider's certificate is checked (voip.ms: Let's Encrypt, every POP in its SAN list,
checked 2026-10-19). Asterisk 24.0.0-rc2 checks a **qualify** OPTIONS against the IP instead of
the host name, which fails on voip.ms ("server identity does not match") and marks the contact
Unavailable, so TLS trunks get `qualify_frequency = 0` and Test skips the qualify; REGISTER and
INVITE verify the name correctly. Re-check on Asterisk upgrades.
checked 2026-10-19). Asterisk 24.0.0-rc2 checked a **qualify** OPTIONS against the IP instead
of the host name, which failed on voip.ms ("server identity does not match") and marked the
contact Unavailable, so TLS trunks had `qualify_frequency = 0` and Test skipped the qualify.
24.0.0-rc3 fixed it ("Preserve target hostname for qualified OPTIONS destination"): TLS trunks
are qualified again, and the upgrade check (`tlsQualifyByHostname`, 01 §2) catches it if it
comes back.
- Phones over TLS are not provisioned (templates stay UDP); that's for remote phones later.
### 2.2 Phone ACL (setting `sip.restrictPhonesToLocalNets`)