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.matchIpsyou list, which are added to what DNS reveals.
See Trunks for the details and the failure modes.
The LiveKit bridge is a trunk that branches early
With mobile apps configured, the LiveKit SIP bridge
is registered as a trunk of its own, so its INVITEs arrive through this same
door. They are then handled apart from everything below: an app-originated call
is authorised by a one-time grant in an X-Lyno-Token header, not routed by
DID.
The source address only selected the code path. It cannot be the authorisation here, because the bridge is shared infrastructure whose egress may not even be pinnable on LiveKit Cloud — what proves the call is that a live, authenticated app session asked for exactly this destination less than a minute ago.
Which tenant owns the call
Matched in this order, first match wins:
- an
InboundOverridedocument — 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 InboundRoute documents decide where the call
goes — unless an override already pinned a destination, in which case that wins:
# Hand acme's number to globex's extension 900 instead.
configVersion: lyno/v1
kind: InboundOverride
metadata:
name: acme-main-to-globex
spec:
priority: 10
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.
A destination may name a queue, a menu or a mailbox and pass validation, but only
extension and ring_group are actually delivered — see
Inbound routing.
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
defaultTrunk.
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. Aqueuethat timed out and anivrnobody chose from behave the same way; - a
dialstep that is answered bridges the two legs and the plan stops there; hangupends the call, and so does running off the end of the plan;goto, and anivroption pointing at an extension, hand the call to another plan and do not come back. A call may enter at most 20 plans in total.
Before any of that: the guard
When security.enabled is on, a packet from a rate-limited or banned source is
dropped before it is parsed — no message is built, no handler runs, and there
is no SIP response at all. Trunk addresses and loopback are trusted
automatically. See The security guard.
Do-not-disturb and forwarding
Two pieces of runtime state can change the outcome of ringing an extension, whichever door the call came through:
forward alwaysreplaces the extension's targets with the forwarding destination, resolved in the target's own tenant, up to five hops deep.- Do-not-disturb makes the extension contribute no targets at all — indistinguishable from a phone that is not registered, so the caller's plan falls through exactly as it would otherwise.
forward noanswerapplies afterwards, and only to adialof a single endpoint. Ring groups and follow-me stages are exempt by design.
State lives in features.json in the data
directory and is set by
feature codes.
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.
Transfer
A phone in a bridged call can send REFER, from either leg. The PBX points the
other party at the new destination and rebuilds the bridge from the two
survivors. A transfer can only reach somewhere the tenant could have dialled
itself. Attended transfer is not supported — a Replaces header is stripped and
the transfer degrades to blind. See
Feature codes and transfer.
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.
RFC 2833 telephone-event is always offered alongside the configured codecs. Without it every keypad feature — menus, PINs, feature codes — would be silently dead, so it is not something you configure.
Every call is plain RTP. srtp: on a listener and media.savpProfile are
accepted by validation and never applied.
What gets written down
A call that is answered leaves a call log record, and may leave a recording or a voicemail message. All three go to the data directory, never back into the configuration.