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.match_ips you 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:

  1. a global inbound_overrides entry — 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 inbound_routes decide where the call goes — unless the override already pinned a destination, in which case that wins:

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

  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 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 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 dial step that is answered bridges the two legs and the plan stops there;
  • hangup ends 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.