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.
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
| Field | Default | Purpose |
|---|---|---|
name | — | Identifier used by routes and default_trunk. Required and unique. |
host | — | Carrier hostname or IP. Required. |
port | 5060 | Signalling port. |
transport | udp | udp or tcp. |
tenant | — | Pins every inbound call on this trunk to one tenant. |
auth | — | Username and password for outbound authentication and registration. |
registration | off | Register to the provider — see below. |
inbound | — | Which source addresses count as this trunk. |
caller_id | — | Fallback 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: truerequiresauth; validation refuses the combination without it.expirydefaults to5mwhen registration is enabled. Lyno re-registers at roughly 75% of the expiry.contact_usersets 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.
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.