How a call is routed
Every INVITE Lyno receives is classified before anything else happens: it is either a call from a provider or a local phone placing a call. The decision is made on the source address. An address belonging to a configured trunk is a provider; anything else must authenticate as a local account.
That fork lives in internal/call/inbound.go; the rest of this page follows
each branch.
Door 1 — from a trunk
Providers do not send credentials with an inbound call, so the trunk's source addresses are the authorisation. Those addresses come from two places:
- the trunk
hostresolved through DNS (A/AAAA records plus the targets of its_sip._udpand_sip._tcpSRV records), refreshed every five minutes; - any
inbound.match_ipsyou list, which are added to what DNS reveals.
See Trunks for the details and the failure modes.
Which tenant owns the call
Matched in this order, first match wins:
- a global
inbound_overridesentry — this is how you redirect a number; - the owning tenant of the trunk, if the trunk names one;
- a tenant's exact DID claim (
dids.numbers); - a tenant's DID pattern (
dids.patterns); - a global catch-all override (
did: "*").
If nothing matches, the call is rejected.
Which destination inside that tenant
Once the tenant is known, its own inbound_routes decide where the call goes —
unless the override already pinned a destination, in which case that wins:
# hand acme's number to globex's extension 900 instead
inbound_overrides:
- did: "+31201234567"
tenant: globex
destination: {type: extension, target: "900"}
An override without a destination only reassigns the tenant; that tenant's own
inbound routes then run normally.
Door 2 — from an endpoint
Anything not coming from a trunk address is a phone. The SIP domain in the request selects the tenant, and digest authentication runs against that tenant's accounts using the tenant's domain as the realm.
A registration whose domain matches no tenant is refused with a log line naming the domain that was offered. That is nearly always a phone configured with the PBX host as its domain instead of the tenant domain — set the PBX address as the outbound proxy and the tenant domain as the domain.
Once authenticated, the dialled number is resolved in this order:
- an extension of the caller's own tenant;
tenant:<name>/<extension>if inter-tenant calling is enabled on both sides;- otherwise, the outbound routes — the tenant's
first, then the global ones, then
default_trunk.
Inside the dialplan
The destination is an extension, whose dialplan is an ordered list of steps. Execution runs top to bottom:
- a step whose time condition does not match is skipped;
- a
dialstep that nobody answers continues to the next step — this is what makes "greet, then follow-me, then announce we are closed" work; - a
dialstep that is answered bridges the two legs and the plan stops there; hangupends the call.
Ringing several devices
Multiple devices registered to the same extension ring in parallel. Ring groups and follow-me stages fan out further. In all cases the first answer wins and the other legs are cancelled.
There is one edge worth knowing when reading logs: the winning leg is acknowledged with the parent context, not the fork context that was just cancelled to stop the losers. A loser that answers during that cancel race is ACKed and immediately hung up, which shows up as a very short answered leg.
Media
Lyno bridges the two legs and proxies RTP. There is no transcoding, so the same codec list is offered on both sides; a mismatch surfaces as a bridge error rather than as silence. Endpoints behind NAT are dialled at the source address observed during REGISTER, not the address advertised in their Contact header.