Skip to main content

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 host resolved through DNS (A/AAAA records plus the targets of its _sip._udp and _sip._tcp SRV records), refreshed every five minutes;
  • any inbound.matchIps you 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:

  1. an InboundOverride document — this is how you redirect a number;
  2. the owning tenant of the trunk, if the trunk names one;
  3. a tenant's exact DID claim (dids.numbers);
  4. a tenant's DID pattern (dids.patterns);
  5. 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:

  1. an extension of the caller's own tenant;
  2. tenant:<name>/<extension> if inter-tenant calling is enabled on both sides;
  3. 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 dial step that nobody answers continues to the next step — this is what makes "greet, then follow-me, then announce we are closed" work. A queue that timed out and an ivr nobody chose from behave the same way;
  • a dial step that is answered bridges the two legs and the plan stops there;
  • hangup ends the call, and so does running off the end of the plan;
  • goto, and an ivr option 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 always replaces 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 noanswer applies afterwards, and only to a dial of 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.