Skip to main content

Trunks

A trunk is a connection to a carrier. Trunks are defined globally in lyno.yaml; by default they are shared by every tenant, and the DID decides who an inbound call belongs to.

configs/lyno.yaml
trunks:
- name: carrier
host: sip.provider.example
port: 5060
transport: udp
auth:
username: "12345678"
password: "change-me"
registration:
enabled: true
expiry: 5m
contact_user: "12345678"
inbound:
match_ips: ["198.51.100.7"]
auto_ips: true
caller_id:
number: "+31201234567"
name: Acme
FieldDefaultPurpose
nameIdentifier used by routes and default_trunk. Required and unique.
hostCarrier hostname or IP. Required.
port5060Signalling port.
transportudpudp or tcp.
tenantPins every inbound call on this trunk to one tenant.
authUsername and password for outbound authentication and registration.
registrationoffRegister to the provider — see below.
inboundWhich source addresses count as this trunk.
caller_idFallback caller ID when the tenant has none.

Registration

Some carriers identify you by a registration rather than by IP.

registration:
enabled: true
expiry: 5m
contact_user: "12345678"
  • enabled: true requires auth; validation refuses the combination without it.
  • expiry defaults to 5m when registration is enabled. Lyno re-registers at roughly 75% of the expiry.
  • contact_user sets the user part of the Contact URI, which some carriers require to match the account.

A trunk that fails to register is retried with a backoff and reported as down on the dashboard's Trunks tab. One unreachable trunk never affects the others or the process.

If a trunk needs no registration — the carrier authorises you by source IP — omit the registration block entirely and keep auth only if outbound calls are challenged.

How inbound calls are recognised

An incoming INVITE is a trunk call when its source address belongs to a trunk. Those addresses come from two places, and they add up:

DNS discovery (auto_ips, on by default). The trunk host is resolved to its A/AAAA records, plus the targets of its _sip._udp and _sip._tcp SRV records, and refreshed every five minutes. A provider that changes gateways therefore does not break inbound calls, and there is usually no allowlist to maintain.

Explicit addresses (match_ips). Gateways DNS does not reveal, added to whatever discovery found. Because they are additive, they keep working when resolution fails — which is the reason to list your carrier's known signalling IPs even when discovery works.

inbound:
match_ips: ["198.51.100.7", "198.51.100.8"]
auto_ips: false # rely on match_ips alone

A trunk configured with a literal IP as its host needs no DNS at all.

The discovered addresses are logged on startup and whenever they change, and the dashboard's Trunks tab shows them under INBOUND FROM. Check there first when inbound calls are rejected while outbound works.

A trunk matching 127.0.0.1 swallows local calls

If a trunk's addresses include the loopback address, every INVITE from loopback is classified as a trunk call and never gets the chance to authenticate. This only bites on a development machine, but it is confusing when it does.

Caller ID

The number presented on an outgoing call resolves as: the tenant's caller_id, then the trunk's caller_id, then nothing. One carrier account can therefore serve many tenants while each presents its own number.

Pinning a trunk to one tenant

trunks:
- name: acme-only
tenant: acme
host: sip.provider-b.example

Every inbound call on this trunk belongs to acme, whatever number it arrives on — useful when a tenant brings their own carrier account. The tenant must exist; validation refuses an unknown name.

Outbound

Trunks are chosen by outbound routes and by default_trunk. A route names a trunk; nothing else about a trunk affects outbound routing.