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

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:

configs/lyno.yaml
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: 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.

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.