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:
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]
|
[trunk-a1b2c3d4-0000-4000-8000-000000000004]
|
||||||
type = aor
|
type = aor
|
||||||
contact = sip:chicago1.voip.ms:5061;transport=tls
|
contact = sip:chicago1.voip.ms:5061;transport=tls
|
||||||
qualify_frequency = 0
|
qualify_frequency = 60
|
||||||
|
|
||||||
[trunk-a1b2c3d4-0000-4000-8000-000000000004]
|
[trunk-a1b2c3d4-0000-4000-8000-000000000004]
|
||||||
type = endpoint
|
type = endpoint
|
||||||
|
|||||||
@@ -232,11 +232,9 @@ function trunkSections(t: SnapTrunk, snap: Snapshot): string {
|
|||||||
section(`trunk-${id}`, [
|
section(`trunk-${id}`, [
|
||||||
'type = aor',
|
'type = aor',
|
||||||
`contact = sip:${target}`,
|
`contact = sip:${target}`,
|
||||||
// ponytail: Asterisk 24 checks a qualify's TLS certificate against the IP, not the name, so
|
// Over TLS too since 24.0.0-rc3, which checks a qualify's certificate against the host name
|
||||||
// OPTIONS to voip.ms fail verify_server and the contact goes Unavailable (calls and
|
// (rc2 used the IP and failed verify_server; upgrade-test.ts tlsQualifyByHostname).
|
||||||
// REGISTER verify fine). No qualify over TLS: registration shows the trunk's health.
|
'qualify_frequency = 60',
|
||||||
// Turn it back on when a qualify to chicago1.voip.ms:5061 passes.
|
|
||||||
`qualify_frequency = ${t.transport === 'tls' ? 0 : 60}`,
|
|
||||||
]),
|
]),
|
||||||
section(`trunk-${id}`, [
|
section(`trunk-${id}`, [
|
||||||
'type = endpoint',
|
'type = endpoint',
|
||||||
|
|||||||
@@ -213,12 +213,10 @@ export class LiveState extends EventEmitter<{ message: [LiveMessage] }> {
|
|||||||
/** POST /api/trunks/:id/test: qualify now, then read registration and reachability. */
|
/** POST /api/trunks/:id/test: qualify now, then read registration and reachability. */
|
||||||
async testTrunk(
|
async testTrunk(
|
||||||
id: string,
|
id: string,
|
||||||
{ waitMs = 1500, qualify = true } = {},
|
{ waitMs = 1500 } = {},
|
||||||
): Promise<{ live: TrunkLive; rttMs: number | null }> {
|
): Promise<{ live: TrunkLive; rttMs: number | null }> {
|
||||||
if (qualify) {
|
await this.ami.action({ Action: 'PJSIPQualify', Endpoint: `trunk-${id}` }).catch(() => {});
|
||||||
await this.ami.action({ Action: 'PJSIPQualify', Endpoint: `trunk-${id}` }).catch(() => {});
|
await new Promise((r) => setTimeout(r, waitMs)); // the OPTIONS answer arrives asynchronously
|
||||||
await new Promise((r) => setTimeout(r, waitMs)); // the OPTIONS answer arrives asynchronously
|
|
||||||
}
|
|
||||||
await this.refreshRegistrations();
|
await this.refreshRegistrations();
|
||||||
const rttMs = await this.refreshReachable(id);
|
const rttMs = await this.refreshReachable(id);
|
||||||
const live = this.trunks.get(id);
|
const live = this.trunks.get(id);
|
||||||
|
|||||||
@@ -208,21 +208,18 @@ export async function trunkRoutes(app: FastifyInstance) {
|
|||||||
if (!t.enabled) throw badRequest('Turn the trunk on first');
|
if (!t.enabled) throw badRequest('Turn the trunk on first');
|
||||||
if (!app.live.amiConnected)
|
if (!app.live.amiConnected)
|
||||||
throw new HttpError(503, 'unavailable', 'Asterisk is not connected');
|
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);
|
||||||
const { live, rttMs } = await app.live.testTrunk(id, { qualify: t.transport !== 'tls' });
|
|
||||||
const reg = t.authMode === 'registration';
|
const reg = t.authMode === 'registration';
|
||||||
const detail =
|
const detail =
|
||||||
reg && live.registration === 'registered'
|
reg && live.registration === 'registered'
|
||||||
? `Registered with ${t.serverHost}`
|
? `Registered with ${t.serverHost}`
|
||||||
: reg && live.registration === 'rejected'
|
: reg && live.registration === 'rejected'
|
||||||
? `${t.serverHost} rejected the username or password`
|
? `${t.serverHost} rejected the username or password`
|
||||||
: !reg && t.transport === 'tls'
|
: !live.reachable
|
||||||
? `Can't check a TLS trunk without registration from here: make a test call.`
|
? `${t.serverHost} doesn't answer. Check the server name and that this server can reach it on port ${t.serverPort}.`
|
||||||
: !live.reachable
|
: reg
|
||||||
? `${t.serverHost} doesn't answer. Check the server name and that this server can reach it on port ${t.serverPort}.`
|
? `${t.serverHost} answers but the registration hasn't completed yet. Try again in a minute.`
|
||||||
: reg
|
: `${t.serverHost} answers (IP authentication: no registration)`;
|
||||||
? `${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 };
|
return { registration: reg ? live.registration : 'unknown', rttMs, detail };
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -15,12 +15,14 @@ describe('the pinned Asterisk', () => {
|
|||||||
).toBe(VERIFIED_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(
|
const spec = readFileSync(
|
||||||
new URL('../../../docs/spec/01-infrastructure.md', import.meta.url),
|
new URL('../../../docs/spec/01-infrastructure.md', import.meta.url),
|
||||||
'utf8',
|
'utf8',
|
||||||
);
|
);
|
||||||
expect(spec).toContain(`**Pinned: \`${pinnedAsterisk()}\``);
|
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,7 +4,7 @@
|
|||||||
// `pnpm test:asterisk-upgrade` on the new image, act on each change it lists, then update this.
|
// `pnpm test:asterisk-upgrade` on the new image, act on each change it lists, then update this.
|
||||||
import { readFileSync } from 'node:fs';
|
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. */
|
/** The version asterisk/Dockerfile builds. */
|
||||||
export function pinnedAsterisk(): string {
|
export function pinnedAsterisk(): string {
|
||||||
@@ -57,11 +57,11 @@ export const FACTS = {
|
|||||||
where: 'services/cdr.ts utc() adds the Z itself',
|
where: 'services/cdr.ts utc() adds the Z itself',
|
||||||
},
|
},
|
||||||
tlsQualifyByHostname: {
|
tlsQualifyByHostname: {
|
||||||
expect: false,
|
expect: true,
|
||||||
what: 'a qualify OPTIONS over TLS checks the certificate against the host name, not the IP',
|
what: 'a qualify OPTIONS over TLS checks the certificate against the host name, not the IP',
|
||||||
where:
|
where:
|
||||||
'generator/pjsip.ts sets qualify_frequency = 0 on TLS trunks because of this (08 §2.1); ' +
|
'generator/pjsip.ts qualifies TLS trunks and Test sends PJSIPQualify (08 §2.1). False on ' +
|
||||||
'24.0.0-rc3 says it fixes it ("Preserve target hostname for qualified OPTIONS destination")',
|
'24.0.0-rc2: a TLS trunk then showed as unreachable, so it had qualify_frequency = 0',
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ describe.skipIf(!hasTestDb || !hasOpenssl)('SIP TLS setting', () => {
|
|||||||
expect(await warningCodes()).not.toContain('tls_cert');
|
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', {
|
const trunk = await req('POST', '/api/trunks', {
|
||||||
name: 'voip.ms TLS',
|
name: 'voip.ms TLS',
|
||||||
serverHost: '127.0.0.1',
|
serverHost: '127.0.0.1',
|
||||||
@@ -119,13 +119,15 @@ describe.skipIf(!hasTestDb || !hasOpenssl)('SIP TLS setting', () => {
|
|||||||
await t.app.generator.apply({ trigger: 'test' });
|
await t.app.generator.apply({ trigger: 'test' });
|
||||||
const { readFile } = await import('node:fs/promises');
|
const { readFile } = await import('node:fs/promises');
|
||||||
const conf = await readFile(join(t.app.generator.dir, 'pjsip_selfphoneng.conf'), 'utf8');
|
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(/media_encryption = sdes/);
|
||||||
expect(conf).toMatch(/verify_server = yes/);
|
expect(conf).toMatch(/verify_server = yes/);
|
||||||
t.live.amiConnected = true;
|
t.live.amiConnected = true;
|
||||||
const test = await req('POST', `/api/trunks/${trunk.json().id}/test`);
|
const test = await req('POST', `/api/trunks/${trunk.json().id}/test`);
|
||||||
expect(test.statusCode).toBe(200);
|
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 });
|
const off = await req('PATCH', '/api/settings', { 'sip.tlsEnabled': false });
|
||||||
expect(off.statusCode).toBe(400);
|
expect(off.statusCode).toBe(400);
|
||||||
expect(off.json().error.message).toMatch(/uses TLS/);
|
expect(off.json().error.message).toMatch(/uses TLS/);
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
FROM debian:trixie-slim AS build
|
FROM debian:trixie-slim AS build
|
||||||
ARG ASTERISK_VERSION=24.0.0-rc2
|
ARG ASTERISK_VERSION=24.0.0-rc3
|
||||||
ENV DEBIAN_FRONTEND=noninteractive
|
ENV DEBIAN_FRONTEND=noninteractive
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
build-essential ca-certificates curl wget pkg-config autoconf automake libtool \
|
build-essential ca-certificates curl wget pkg-config autoconf automake libtool \
|
||||||
|
|||||||
@@ -142,10 +142,10 @@ missing or invalid, it logs a clear message and exits with code 1.
|
|||||||
## 2. Asterisk image (`asterisk/Dockerfile`)
|
## 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
|
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
|
> **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
|
> 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
|
> 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
|
> 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
|
```dockerfile
|
||||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
FROM debian:trixie-slim AS build
|
FROM debian:trixie-slim AS build
|
||||||
ARG ASTERISK_VERSION=24.0.0-rc2
|
ARG ASTERISK_VERSION=24.0.0-rc3
|
||||||
ENV DEBIAN_FRONTEND=noninteractive
|
ENV DEBIAN_FRONTEND=noninteractive
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
build-essential ca-certificates curl wget pkg-config autoconf automake libtool \
|
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 --from=build /out/ /
|
||||||
COPY conf/ /etc/asterisk/
|
COPY conf/ /etc/asterisk/
|
||||||
COPY entrypoint.sh /entrypoint.sh
|
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
|
&& chown -R asterisk:asterisk /etc/asterisk /var/lib/asterisk /var/spool/asterisk /var/log/asterisk /var/run/asterisk
|
||||||
ENTRYPOINT ["/entrypoint.sh"]
|
ENTRYPOINT ["/entrypoint.sh"]
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -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).
|
(and `from_user` when there is no username).
|
||||||
Skip disabled trunks entirely.
|
Skip disabled trunks entirely.
|
||||||
For TCP/TLS trunks, `server_uri`, `client_uri` and the AOR `contact` get `;transport=tcp|tls`.
|
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
|
TLS trunks are qualified like the others (`qualify_frequency = 60`) since 24.0.0-rc3. On rc2 they
|
||||||
against the IP, see 08 §2.1); their health comes from the registration.
|
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
|
`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
|
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.
|
that isn't turned on in Settings → SIP.
|
||||||
|
|||||||
@@ -34,10 +34,12 @@
|
|||||||
cert is missing or expires within 14 days.
|
cert is missing or expires within 14 days.
|
||||||
- `transport-tls` has `verify_server = yes` + `ca_list_file = /etc/ssl/certs/ca-certificates.crt`:
|
- `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,
|
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
|
checked 2026-10-19). Asterisk 24.0.0-rc2 checked a **qualify** OPTIONS against the IP instead
|
||||||
the host name, which fails on voip.ms ("server identity does not match") and marks the contact
|
of the host name, which failed on voip.ms ("server identity does not match") and marked the
|
||||||
Unavailable, so TLS trunks get `qualify_frequency = 0` and Test skips the qualify; REGISTER and
|
contact Unavailable, so TLS trunks had `qualify_frequency = 0` and Test skipped the qualify.
|
||||||
INVITE verify the name correctly. Re-check on Asterisk upgrades.
|
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.
|
- Phones over TLS are not provisioned (templates stay UDP); that's for remote phones later.
|
||||||
|
|
||||||
### 2.2 Phone ACL (setting `sip.restrictPhonesToLocalNets`)
|
### 2.2 Phone ACL (setting `sip.restrictPhonesToLocalNets`)
|
||||||
|
|||||||
Reference in new issue
Block a user