settings: the SIP port, with a check against other listeners
This commit is contained in:
1 parent
6893c55e1b
commit
f2868cf5d3
5 files changed
+87
-7
No files matched your search
+3
-2
@@ -7,8 +7,9 @@ Docker.
|
|||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
- Settings → Security has **Accept SIP over TCP** (on by default, as before). It was a setting
|
- Settings → Security has the **SIP port** and **Accept SIP over TCP** (5060 and on by default,
|
||||||
with no switch.
|
as before). Both were settings with no field. Another port helps when a router or ISP keeps UDP
|
||||||
|
5060 for itself. A port that another part of SelfPhoneNG listens on is refused.
|
||||||
|
|
||||||
## v0.0.3 — 2026-10-21
|
## v0.0.3 — 2026-10-21
|
||||||
|
|
||||||
|
|||||||
@@ -115,6 +115,20 @@ export async function settingsRoutes(app: FastifyInstance) {
|
|||||||
}
|
}
|
||||||
await checkRefs(db, values);
|
await checkRefs(db, values);
|
||||||
const before = await getAllSettings(db);
|
const before = await getAllSettings(db);
|
||||||
|
// SIP ports may not land on another listener (Asterisk would fail to start the transport).
|
||||||
|
const c = app.config;
|
||||||
|
const [sip, tls] = [
|
||||||
|
(values['sip.udpPort'] ?? before['sip.udpPort']) as number,
|
||||||
|
(values['sip.tlsPort'] ?? before['sip.tlsPort']) as number,
|
||||||
|
];
|
||||||
|
for (const port of [sip, tls])
|
||||||
|
if ([80, 443, 8088, c.AMI_PORT, c.API_PORT].includes(port))
|
||||||
|
throw badRequest(`Port ${port} is already used by SelfPhoneNG`);
|
||||||
|
else if (port >= c.RTP_PORT_START && port <= c.RTP_PORT_END)
|
||||||
|
throw badRequest(
|
||||||
|
`Port ${port} is in the call audio range (${c.RTP_PORT_START}–${c.RTP_PORT_END})`,
|
||||||
|
);
|
||||||
|
if (sip === tls) throw badRequest('The SIP port and the TLS port must differ');
|
||||||
if (values['sip.tlsEnabled'] === true && !before['sip.tlsEnabled']) {
|
if (values['sip.tlsEnabled'] === true && !before['sip.tlsEnabled']) {
|
||||||
const cert = await readTlsCert(app.config.TLS_DIR);
|
const cert = await readTlsCert(app.config.TLS_DIR);
|
||||||
if (!cert.ok) throw badRequest(`Can't turn on TLS. ${cert.error}`);
|
if (!cert.ok) throw badRequest(`Can't turn on TLS. ${cert.error}`);
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
// The SIP port (Settings → Security), for routers or ISPs that keep UDP 5060 for themselves.
|
||||||
|
import type { LightMyRequestResponse } from 'fastify';
|
||||||
|
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
||||||
|
import { hasTestDb } from './db.ts';
|
||||||
|
import { ADMIN, H, createTestApp } from './app.ts';
|
||||||
|
|
||||||
|
describe.skipIf(!hasTestDb)('SIP port setting', () => {
|
||||||
|
let t: Awaited<ReturnType<typeof createTestApp>>;
|
||||||
|
let req: (method: string, url: string, payload?: unknown) => Promise<LightMyRequestResponse>;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
t = await createTestApp();
|
||||||
|
await t.clearMustChange(ADMIN.email);
|
||||||
|
const cookie = await t.login(ADMIN.email, ADMIN.password);
|
||||||
|
req = (method, url, payload) =>
|
||||||
|
t.app.inject({
|
||||||
|
method: method as 'GET',
|
||||||
|
url,
|
||||||
|
payload: payload as object,
|
||||||
|
headers: { ...H, cookie },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
afterAll(() => t?.close());
|
||||||
|
|
||||||
|
it('refuses ports something else listens on', async () => {
|
||||||
|
for (const [port, msg] of [
|
||||||
|
[8080, /already used/],
|
||||||
|
[5038, /already used/],
|
||||||
|
[15000, /call audio range/],
|
||||||
|
[5061, /must differ/],
|
||||||
|
] as const) {
|
||||||
|
const r = await req('PATCH', '/api/settings', { 'sip.udpPort': port });
|
||||||
|
expect(r.statusCode, String(port)).toBe(400);
|
||||||
|
expect(r.json().error.message).toMatch(msg);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('5080 is saved, and phones are told', async () => {
|
||||||
|
const r = await req('PATCH', '/api/settings', { 'sip.udpPort': 5080 });
|
||||||
|
expect(r.statusCode).toBe(200);
|
||||||
|
expect(r.json()['sip.udpPort']).toBe(5080);
|
||||||
|
expect((await req('GET', '/api/system/status')).json().ports.sip).toBe(5080);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -549,10 +549,11 @@ function NetworkTab({ s }: { s: SettingsDto }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Phone ACL (08 §2.2), outside calls at once (08 §1), provisioning access (07 §2), SIP over TCP, SIP over TLS (M11-T2). */
|
/** Phone ACL (08 §2.2), outside calls at once (08 §1), provisioning access (07 §2), SIP port and TCP, SIP over TLS (M11-T2). */
|
||||||
function SecurityTab({ s }: { s: SettingsDto }) {
|
function SecurityTab({ s }: { s: SettingsDto }) {
|
||||||
const save = useSave();
|
const save = useSave();
|
||||||
const cert = useQuery({ queryKey: ['tls'], queryFn: systemApi.tls });
|
const cert = useQuery({ queryKey: ['tls'], queryFn: systemApi.tls });
|
||||||
|
const [sipPort, setSipPort] = useState(String(s['sip.udpPort']));
|
||||||
const [tcpOn, setTcpOn] = useState(s['sip.tcpEnabled']);
|
const [tcpOn, setTcpOn] = useState(s['sip.tcpEnabled']);
|
||||||
const [tlsOn, setTlsOn] = useState(s['sip.tlsEnabled']);
|
const [tlsOn, setTlsOn] = useState(s['sip.tlsEnabled']);
|
||||||
const [tlsPort, setTlsPort] = useState(String(s['sip.tlsPort']));
|
const [tlsPort, setTlsPort] = useState(String(s['sip.tlsPort']));
|
||||||
@@ -580,6 +581,7 @@ function SecurityTab({ s }: { s: SettingsDto }) {
|
|||||||
'provisioning.enabled': provOn,
|
'provisioning.enabled': provOn,
|
||||||
'calls.maxPerExtension': Number(perExt),
|
'calls.maxPerExtension': Number(perExt),
|
||||||
'calls.maxTotal': Number(total),
|
'calls.maxTotal': Number(total),
|
||||||
|
'sip.udpPort': Number(sipPort),
|
||||||
'sip.tcpEnabled': tcpOn,
|
'sip.tcpEnabled': tcpOn,
|
||||||
'sip.tlsEnabled': tlsOn,
|
'sip.tlsEnabled': tlsOn,
|
||||||
'sip.tlsPort': Number(tlsPort),
|
'sip.tlsPort': Number(tlsPort),
|
||||||
@@ -641,11 +643,24 @@ function SecurityTab({ s }: { s: SettingsDto }) {
|
|||||||
<h2 className="font-medium">Phone setup files</h2>
|
<h2 className="font-medium">Phone setup files</h2>
|
||||||
{box(provOn, setProvOn, 'Serve setup files to phones (Phones page)')}
|
{box(provOn, setProvOn, 'Serve setup files to phones (Phones page)')}
|
||||||
{box(lanOnly, setLanOnly, 'Only to phones on the local networks')}
|
{box(lanOnly, setLanOnly, 'Only to phones on the local networks')}
|
||||||
<h2 className="font-medium">SIP over TCP</h2>
|
<h2 className="font-medium">SIP port</h2>
|
||||||
{box(tcpOn, setTcpOn, `Accept SIP over TCP (port ${s['sip.udpPort']})`)}
|
<Field
|
||||||
|
label="SIP port (UDP, and TCP below)"
|
||||||
|
hint="5060 unless your router or ISP keeps UDP 5060 for itself: then for example 5080, forwarded on the router. Phones need setting up again (Phones page or the Linphone QR); trunks don't mind. Needs a telephony restart."
|
||||||
|
>
|
||||||
|
{(p) => (
|
||||||
|
<input
|
||||||
|
{...p}
|
||||||
|
className={`${inputClass} max-w-32`}
|
||||||
|
inputMode="numeric"
|
||||||
|
value={sipPort}
|
||||||
|
onChange={(e) => setSipPort(e.target.value)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Field>
|
||||||
|
{box(tcpOn, setTcpOn, 'Accept SIP over TCP')}
|
||||||
<p className="-mt-2 text-xs text-zinc-500">
|
<p className="-mt-2 text-xs text-zinc-500">
|
||||||
For phones and trunks set to TCP. Phones set up by this server use UDP. Changing it needs a
|
For phones and trunks set to TCP. Phones set up by this server use UDP.
|
||||||
telephony restart.
|
|
||||||
</p>
|
</p>
|
||||||
<h2 className="font-medium">Encrypted SIP (TLS)</h2>
|
<h2 className="font-medium">Encrypted SIP (TLS)</h2>
|
||||||
<p className="text-sm" aria-label="TLS certificate">
|
<p className="text-sm" aria-label="TLS certificate">
|
||||||
|
|||||||
@@ -20,6 +20,11 @@ the wrong place.
|
|||||||
- TCP 5060 only for phones or trunks set to TCP (Settings → Security → Accept SIP over TCP;
|
- TCP 5060 only for phones or trunks set to TCP (Settings → Security → Accept SIP over TCP;
|
||||||
phones this server sets up use UDP);
|
phones this server sets up use UDP);
|
||||||
- TCP 5061 only if you use SIP over TLS.
|
- TCP 5061 only if you use SIP over TLS.
|
||||||
|
- If nothing reaches the server on UDP 5060 while other UDP ports do (System → Diagnostics, a
|
||||||
|
**Complete network test**), the router or the ISP keeps that port for itself: often an ISP
|
||||||
|
box with phone jacks, or a SIP ALG you can't turn off. Set another **SIP port** in Settings →
|
||||||
|
Security, for example 5080, forward that instead, restart telephony, and set the phones up
|
||||||
|
again. Trunks keep working: they're reached from the port the server registers from.
|
||||||
3. **Double NAT** (the ISP's box in front of your own router): forward the ports on both, or put
|
3. **Double NAT** (the ISP's box in front of your own router): forward the ports on both, or put
|
||||||
your router in the ISP box's DMZ / bridge mode.
|
your router in the ISP box's DMZ / bridge mode.
|
||||||
4. **Public IP and local networks.**
|
4. **Public IP and local networks.**
|
||||||
|
|||||||
Reference in new issue
Block a user