Skip to main content

Outbound routing

When a dialled number is not a local extension, outbound routes decide which trunk carries it and what number is actually sent.

The order

  1. the calling tenant's own OutboundRoute documents, in priority order;
  2. the OutboundRoute documents with no tenant — the global fallback — in priority order;
  3. defaultTrunk, with the number unchanged.

The first pattern that matches wins — within a list and across the fallback. If nothing matches and there is no defaultTrunk, the call fails.

A route

Each route is a document. Without metadata.tenant it belongs to the global fallback set:

configVersion: lyno/v1
kind: OutboundRoute
metadata:
name: emergency
spec:
priority: 10
pattern: '^112$'
trunk: carrier
---
configVersion: lyno/v1
kind: OutboundRoute
metadata:
name: national
spec:
priority: 20
pattern: '^0[1-9]\d{8}$'
strip: 1
prepend: "+31"
trunk: carrier
---
configVersion: lyno/v1
kind: OutboundRoute
metadata:
name: international
spec:
priority: 30
pattern: '^\+\d+$'
trunk: carrier
---
configVersion: lyno/v1
kind: System
spec:
defaultTrunk: carrier
FieldRequiredMeaning
metadata.nameyesLabel, used in logs, error messages and to break ties.
prioritynoAscending. This is what decides which route is tried first.
patternyesGo regular expression matched against the dialled number.
stripnoDigits removed from the front of the number.
prependnoText put in front of what remains.
trunkyesThe trunk to use. Must exist.

Manipulation is applied in that order: match, then strip, then prepend. In the national example, 0201234567 matches, loses its leading 0, and is sent as +31201234567.

Order beats cleverness, and order is priority

Routes are first-match-wins in ascending priority, so put the specific ones on the lower numbers. ^112$ before ^\d+$ is the difference between reaching emergency services and reaching your carrier's error announcement.

Document order is not consulted — it cannot be, when a route may live in any file. Leave gaps (10, 20, 30) so a route can be inserted later; saving renumbers to exactly that.

Per-tenant routes

A tenant's routes are tried before the global ones, which is how one tenant gets its own carrier for a subset of numbers without affecting anybody else:

configVersion: lyno/v1
kind: OutboundRoute
metadata:
name: acme-mobile
tenant: acme # this is what makes it acme's own
spec:
priority: 10
pattern: '^06\d{8}$'
strip: 1
prepend: "+31"
trunk: acme-mobile-carrier

Everything acme dials that is not a Dutch mobile falls through to the global routes as usual.

Caller ID

The number presented is the calling tenant's callerId, falling back to the trunk's callerId. That is what allows several tenants to share one carrier account and still each present their own number.

Calling an external number from a dialplan

Inside a dialplan, an external destination is written external:<number>, and that number goes through exactly the routing described above:

- id: follow-me
type: dial
target:
kind: followme
stages:
- targets: ["101", "external:0612345678"]
timeout: 25s

or as a direct target:

- id: forward
type: dial
target:
kind: external
number: "+31612345678"

Debugging a route

  • Run with -debug: the matched route name and the resulting number are logged when a call is placed.
  • A call that leaves on the wrong trunk is almost always an earlier route matching more broadly than intended — check anchoring (^ and $).
  • A call that fails with no trunk means nothing matched and defaultTrunk is unset.