Skip to main content

Troubleshooting

Start here: run with -debug, reproduce, and read the Logs tab (or stderr under -headless). Most of what follows is recognisable from a single log line.

Registration

A phone will not register

The log names the domain the phone offered. If it is the PBX host rather than the tenant domain, the phone is misconfigured:

FieldMust be
Domain / SIP serverthe tenant domain, e.g. acme.pbx.example.com
Outbound proxythe PBX address, e.g. 192.0.2.10:5060

A registration whose domain matches no tenant is refused before any password is checked — so "wrong password" is rarely the real cause.

It registers, then drops

The expiry is clamped to between 60s and 3600s, defaulting to 300s when the phone asks for nothing. A phone behind NAT with a mapping shorter than its registration interval will appear to flap; lower the interval on the phone.

Two tenants, one phone works and the other does not

Check that the domains are distinct and that each phone uses its own. The digest realm is the tenant domain, so credentials only work against the domain they belong to.

Inbound calls

Nothing arrives at all

Before chasing routing, establish whether the packet reaches the process. Run with -debug and watch the SIP trace, which logs one line per message received:

lyno -config configs/lyno.yaml -headless -debug 2>&1 | grep sip-trace

The trace sits below parsing, routing and the guard, so it sees messages that are malformed, addressed to an unknown DID, or banned a moment later. No line for an inbound INVITE means the packet never arrived — look at the firewall, the NAT, and whether the carrier is sending to the address and port you think. A line that is there moves the problem to one of the sections below.

Inbound rejected while outbound works

Almost always the trunk source address. Open the dashboard's Trunks tab and read INBOUND FROM: those are the addresses a trunk call is accepted from. If the carrier's gateway is not listed:

configVersion: lyno/v1
kind: Trunk
metadata:
name: carrier
spec:
host: sip.provider.example
inbound:
matchIps: ["198.51.100.7", "198.51.100.8"]

matchIps is added to what DNS discovery finds, so it keeps working when resolution fails.

The call arrives but is rejected as unknown

No tenant claimed the DID. Check, in order: an InboundOverride document, the trunk's tenant, the tenant's dids.numbers, its dids.patterns, and a global did: "*" catch-all. Remember that patterns are unanchored unless you write ^…$.

Also check the format: a carrier sending 0201234567 will not match a DID claimed as +31201234567.

It reaches the wrong tenant

A global override is winning. Overrides beat every tenant claim, by design — grep lyno.yaml for the number.

Outbound calls

The call leaves on the wrong trunk

An earlier outbound route matched more broadly than intended. Routes are tried top to bottom: the tenant's first, then the global ones. Anchor patterns with ^ and $, and put specific routes above general ones.

The call fails with no trunk

Nothing matched and defaultTrunk is unset. Either add a catch-all route or set a default trunk.

The wrong number is presented

Caller ID resolves as the tenant's callerId, then the trunk's. A tenant without one inherits the trunk's, which is usually the carrier's main number.

Audio

No audio in either direction

Either the RTP range is not reachable, or media.externalIp is unset while the PBX is behind NAT. See Running behind NAT.

One-way audio

RTP forwarded in one direction only — or, in Docker, bridge networking. Use host networking.

A call connects and then drops immediately

Look for a bridge error in the logs. Lyno does not transcode: both legs must agree on a codec. A mismatch surfaces as a bridge error, not as silence. The same codec list is offered on both sides, so a mismatch means one party refused G.711 alaw and ulaw entirely.

A sound file will not play

WAV files must be 8 kHz mono 16-bit PCM, and they are validated at startup — so this fails before the PBX runs, not during a call. Convert with:

ffmpeg -i input.mp3 -ar 8000 -ac 1 -acodec pcm_s16le output.wav

Replacing a file on disk has no effect until restart: files are cached in memory at startup.

Dialplans

A step does not run

Either its time condition did not match — -debug logs every skipped step and why — or an earlier dial was answered and the plan ended there.

The plan continues after a dial that should have been final

That is the design: a dial nobody answers falls through. To end the call instead, follow it with a hangup step.

An extension rings nobody

Targets that resolve to nothing are skipped, and an extension with no registered device resolves to nothing. Check the Endpoints tab: an account that never registered still appears there, which distinguishes "phone is off" from "account does not exist".

If the phone is registered, suspect do-not-disturb. DND makes an extension contribute no targets, which is deliberately indistinguishable from an unregistered phone at the ringing stage. Look for the extension in features.json in the data directory, or dial the tenant's DND-off plan. Forwarding is the other candidate — the call is going somewhere, just not there.

A DID rings nothing and the caller gets 404

The destination type. Only extension and ring_group are delivered; a DID pointed at a queue, ivr, voicemail, external or hangup destination passes validation and then fails the call with invalid destination type in the log. Point it at an extension whose plan starts with that step — see Inbound routing.

A menu key does nothing and the plan moves on

Same cause, one level down. An IVR option of type queue, ivr or voicemail logs destination type … is not wired up yet and the menu falls through to the next step. Point the key at an extension instead.

A change to the YAML has no effect

Configuration is read once at startup. Restart the PBX.

SIGHUP reloads API tokens and only API tokens; it is not a general reload.

The setting is in the file, valid, and ignored

Some keys are accepted and never read. The fraud limits, security.acl, requireSrtp, SIP TLS and srtp:, recording.retention, recording.beep, storage.s3.maxRetryAge, voicemail.mwi, featureCodes: and a queue's dynamic: are all in that category. They are listed together under the security guard, and each is called out on its own page.

Trunks

A trunk stays down

  • registration.enabled: true requires auth — validation catches this, so a running PBX has already passed it.
  • A hostname that does not resolve is reported with a readable cause rather than a crash.
  • Failures are retried with a backoff; the Trunks tab shows the current state.

Trunk registration hammers the provider

expiry is the re-registration period, not a retry delay. Setting it to a few seconds makes Lyno re-register constantly. Leave it at the default 5m unless the carrier asks for something else.

Startup

It exits immediately with error: …

Run -validate and read the whole list — validation collects every problem rather than stopping at the first.

Permission denied reading the configuration

Configuration files are mode 0600. In Docker the process runs as uid 65532; either chown the tree to that uid or run the container as the owner. See Docker.

It refuses to start because of dataDir

dataDir is required: call logs, recordings or voicemail are enabled and there is nowhere to write them

Something switched on cdr, recording or voicemail, or a tenant declared mailboxes. Set dataDir. Validation never checks the directory itself, so an unwritable one is a startup failure instead, naming the uid that could not write. -validate reports the state without failing on it.

Voicemail works but no lamp lights on the phone

There is no message-waiting indication. voicemail.mwi is accepted and does nothing, and a phone's SUBSCRIBE is answered 405. Email notification and the voicemail_check step are the two ways anybody learns about a message.

Recordings are filling the disk

Nothing prunes them — recording.retention is accepted and not applied. Prune by day directory yourself; see Call recording.

The management API

Every recording download returns 500

api.recordings.presign is on. No storage backend can presign, and there is deliberately no fallback to streaming. Turn it off.

A request returns 400 naming query parameters

An unknown query parameter is an error rather than a silent full listing. Check the spelling against the accepted set.

A token sees an empty list, or a 404 for a record that exists

Tenant scoping. A ?tenant= the token may not see is 403; a record belonging to another tenant is 404, never 403, so ids cannot be probed across tenants. /v1/security/* refuses a tenant-scoped token outright.

The API bound a port nobody chose

bindPort has no default. Zero binds an ephemeral port. Set one.

Security

A real customer is locked out

Open the dashboard's Security tab and press x on their address, or DELETE /v1/security/bans/{ip}. Then work out why: repeated authentication failures usually mean a phone left configured with an old password, retrying forever.

Adding their prefix to security.allow exempts them from banning and rate limiting entirely.

The carrier got banned

It should not be possible — every address discovered for a trunk is trusted automatically, including matchIps on a trunk with autoIps: false. If it happened, check the Trunks tab: an address the carrier is actually calling from that is not listed under INBOUND FROM is not trusted either.

A monthly minute cap lets more through than it should

outboundMinutesPerHour and outboundMinutesPerMonth are counted in memory and are not seeded from the call log at startup, so a restart returns them to zero. The concurrency caps and blockedPrefixes are unaffected. For a hard ceiling on spend, use your carrier's own limits as well.

Every local call is treated as a trunk call

A trunk's discovered addresses include the loopback address. This only happens on a development machine, but it is confusing: local phones never get the chance to authenticate. Point the trunk at a real host, or set inbound.autoIps: false with explicit matchIps.

Getting more detail

lyno -config configs/lyno.yaml -headless -debug 2>&1 | tee lyno.log

-debug adds the matched outbound route and resulting number, discovered trunk addresses and every change to them, each dialplan step as it runs, and the SIP responses that are otherwise dropped as noise.