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:
| Field | Must be |
|---|---|
| Domain / SIP server | the tenant domain, e.g. acme.pbx.example.com |
| Outbound proxy | the 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
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:
trunks:
- name: carrier
host: sip.provider.example
inbound:
match_ips: ["198.51.100.7", "198.51.100.8"]
match_ips 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: a global inbound_overrides entry,
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 default_trunk 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 caller_id, 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.external_ip 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".
A change to the YAML has no effect
Configuration is read once at startup. Restart the PBX.
Trunks
A trunk stays down
registration.enabled: truerequiresauth— 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.
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.auto_ips: false with explicit match_ips.
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.