Skip to main content
Each node reports itself, and carries three limits that run out independently. All three are enforced: a call past one is refused before it is answered, so the calls already up keep their audio. A relayed call costs 0 units. Opus against G.711 costs 2, G.729 or iLBC against Opus costs 4, G.726 against G.711 costs 1, and a virtual agent adds 3 — so the transcode figure is what a node actually runs out of, and a node can be comfortable on channels and out of budget. The Overview shows each as in use / limit.
The audio-streaming module this project builds is free for ten concurrent streams, and a call past the module’s own limit is not refused by it — it is answered and silent. That is why the node refuses first, and why the default is 8: live assist draws on the same ten.
Limits are re-read every 30 seconds, so lowering one during an incident takes effect without a restart.

When the machine itself is loaded: the load brake

A counted limit can only see calls. On a box shared with anything else — a database, a mail server, other sites — a neighbour taking a core is not a channel, and every gauge still reads fine while audio breaks on every call at once. So the node also watches the machine: This is the load brake. Calls this platform chose to make — campaigns, API-placed AI calls — are shed first; a person who clicked to call is refused only when everything is. The overview shows a badge while it is happening, with what the machine was doing. An API call refused this way answers 503 node_busy with Retry-After: 30: “try again shortly”, as distinct from 422, “this request is wrong”.

Which node an organisation lives on

Every node card says who lives here: how many organisations are placed on it, the first few by name, and a link to the list of them. Placement is set on the organisation — Organisations → the tenant → Node — and the fleet is where you see the result. One node carries a base badge. It is the node the control plane shares a machine with, and it is the only node where virtual agents, voicemail, IVR prompts, hold music and call recording run: a virtual agent’s audio is a named pipe both the daemon and the switch have to open, and a recording is written where the call is. So an organisation that uses any of those can be placed on base and nowhere else, and the server refuses a placement they cannot follow rather than letting it fail on the next call. The refusal names what stopped it. The same refusal meets an attempt to switch one of those on for an organisation already placed on another node. An organisation placed on a node does not fall back when that node is down. That is the point of placing: one organisation’s node failing stays that organisation’s problem, instead of spilling its calls onto other tenants’ switches. An organisation on any node goes to whichever is healthy.

Calls for an organisation on another node

Carriers connect to base and nowhere else — a plain-call node holds no gateway and no carrier password. So a call for an organisation placed on another node is carried by two switches: the carrier’s leg on base, and the phone’s leg on the organisation’s own node, joined by one hop over the mesh.
  • Inbound: base takes the carrier’s call, works out the organisation, sees it lives elsewhere and passes the call to that node, which rings the phone.
  • Outbound: a phone on the node dials; the node passes the call to base, and base sends it out through the trunk as if the phone were its own.
The call is billed once, on the leg that touches the carrier. The call’s journey shows the step as Crossed nodes. The hop is recognised by where it comes from — another node of yours — and a call from a node’s address that does not carry the hop is refused like any stranger’s. Two costs to know. Audio relays through base, so base’s channel count includes every hopped call; when placing many organisations elsewhere, size base for that. And a placed organisation whose node is down is refused, not carried by base — the same rule as above.

Adding a node

Base registers itself when firetoned starts beside a switch. Every other node is added in Fleet → New node: a name, its mesh address (WireGuard or Tailscale — never a public one), and two credentials the panel generates — the node’s own ESL password and directory secret. Saving hands back one command to run on the box:
The token is good once, for twelve hours, and only from the node’s own mesh address. Redeemed, it carries everything base alone can make — the two credentials, an agent key bound to this node, base’s addresses and ports — so nothing is typed and the two ends cannot disagree. join.sh refuses a public control-plane address, writes the node’s env, builds FreeSWITCH if it is not there, installs the configuration pointed at base, installs the agent with the bundled key, installs the firewall when told the interface, and ends by proving each path from the node’s side. Join token on a node’s card issues a fresh one when the first was spent or the box is being rebuilt. Base must listen on the mesh for this: FIRETONE_ESL_LISTEN, FIRETONE_PROVISION_LISTEN and FIRETONE_API_LISTEN on its mesh address (deploy/control-plane/make-env.sh asks for the address). While they are on loopback no token can be issued, the dialog says which variable, and the two secrets are shown for carrying by hand. The daemon re-reads the fleet every half minute, so a node added here is connected within that — no restart. A node deleted here is disconnected the same way.

Verify node

Each node card shows three dots — ESL, Directory, Agent — one per path between the node and base, refreshed with the card. Verify asks each path at its real endpoint and says what to fix: The verdict is the worst status. A node whose card is green on ESL alone and red on Directory is a node whose phones cannot register, and this is where it says so.

What a node holds

A node is the most exposed machine in the system, so what is on it reaches only that node:
  • Its own ESL password, set on the node in Fleet. The control plane opens each node’s socket with that node’s password and no other.
  • Its own directory secret (provision_secret, set on the node). When the node’s switch asks the control plane for a phone’s credentials, it is held to that secret, judged by the mesh address the request came from. A node with none set is held to the shared FIRETONE_PROVISION_SECRET, which is what a single box uses. A secret lifted from one node opens no other’s directory.
  • An agent key bound to the node. firetonectl node-agent-key mints a key that can report metrics and sync the firewall for that one node, and is refused for every other node — whoever owns it.
  • No carrier credential. Trunks are on base. A node that is not base gets a gateway file with nothing in it and a carriers ACL that admits the fleet and no carrier. There is nothing on it worth stealing.

Keeping transcode low

Both legs agreeing on a codec lets the switch relay rather than transcode. That is the difference between near-zero CPU and the most expensive thing the system does — see extensions.

Which codecs a node speaks

Opus and G.711 always. G.729, iLBC and G.726 are separate modules, built with the switch (deploy/node/build-g729.sh, deploy/node/build-codecs.sh) and loaded only where they are installed. A node without one still takes the call: every codec profile and every sensible trunk list keeps G.711 behind its first choice, so the call connects in G.711 instead. verify.sh names any module that is installed and not loaded. On a trunk they are ticked, not typed — see a trunk’s codecs. Through the API the names are G729, iLBC and G726-32: the rate is part of the name, and a bare G726 is refused because the switch would drop it from the offer without saying so. When a trunk is pinned to a node, its editor says so if a ticked codec’s module is not loaded there.

Admission control

A node refuses new calls beyond its configured session limit rather than degrading everything already in progress. The switch’s own limit is per second as well as total, and it is shared with everything arriving inbound — which is why a wide outbound campaign competes with your tenants’ incoming calls. The same is true of a carrier: a trunk’s channel limit counts both directions, because that is what the carrier contracted. Where a dialling burst must not be able to fill it, set the trunk’s outbound channel limit as well — it caps the outgoing share and reserves the rest for calls arriving. See trunks.

The name desk phones use

A node’s Handset server name (e.g. sip.firet.one) is what every extension’s handset settings name as the server. It is display only — nothing is written to the switch — but a phone using TLS on 5061 needs it: the phone checks the certificate against the server it was given, and a certificate names a host, not an address. Leave it empty and extensions show the advertised address, which is fine for UDP and TCP on 5060.
The name must resolve to this node directly. A website domain behind Cloudflare (or any web proxy) resolves to the proxy, which carries HTTP only: a phone given it never reaches the switch, and nothing on the switch shows a thing. Use a name that is not proxied — getent ahosts <name> must print the node’s own address. The extension’s handset settings and verify.sh both check it and say so when it leads elsewhere.
The switch listens for SIP on IPv4 only. A name with an IPv6 (AAAA) record sends a phone that prefers IPv6 to an address nothing answers on. An organisation’s handsets are shown the node chosen on the organisation, or the only node there is. With several nodes and none chosen, the handset settings say so instead of guessing.