iax2 0.2.2

Native Rust implementation of the Inter-Asterisk eXchange v2 (IAX2, RFC 5456) protocol: sans-io core with MD5 auth and call-token, G.711 codec and an adaptive jitter buffer, plus an optional async UDP driver and cross-platform audio.
Documentation

iax2

A native Rust implementation of the Inter-Asterisk eXchange v2 protocol (RFC 5456) — the protocol Asterisk and FreePBX use to trunk and register endpoints over a single UDP port.

The crate is built around a sans-io core: all protocol logic (framing, information elements, the registration/call state machine, reliability, jitter buffering) is pure and synchronous, driven by poll/handle calls. You bring your own I/O. Optional features add an async UDP driver and cross-platform audio so you can go from "parse a frame" to "make a phone call" without leaving the crate.

Status: experimental (0.x). The protocol core has been validated against a live FreePBX/Asterisk instance — registration, inbound/outbound calls, qualify, per-call reliability, multi-PBX operation and the jitter buffer all work against real hardware. The public API is not yet stable and the codec support is intentionally narrow (see Scope).

New in 0.2.0: Added DTMF support Fix in 0.2.1: Fixed PING/PONG sequence numbers Fix in 0.2.2: Fixed jitter misalignments( from "streak" to "leaky bucket" strategy )

What works

  • Registration with call-token pre-authentication and MD5 challenge/response
  • Outbound and inbound calls (full call setup: NEW / AUTH / ACCEPT / ANSWER / HANGUP)
  • Qualify (POKE/PONG) that keeps the peer reachable, with a rotating call number so Asterisk stops logging spurious notices
  • Per-call reliability: real sliding-window retransmission with cumulative ACK — each call leg has its own independent window
  • Multi-call / multi-PBX: N parallel registrations (one account per PBX), call waiting across PBXes, hold/unhold, call switching
  • Adaptive jitter buffer: reorders by timestamp, adapts depth to measured network jitter (RFC 3550 estimate), and reports gaps for packet-loss concealment
  • G.711 µ-law codec, with optional polyphase resampling to/from the device rate
  • LAGRQ/LAGRP lag measurement and comfort-noise handling
  • DTMF send and receive (RFC 5456 DTMF frames). Sending the feature codes your PBX expects lets you reach server-side features such as call transfer.

Architecture

The core is sans-io and pulls in no heavy dependencies — if you only need the protocol, you don't drag in tokio, cpal or rubato:

module feature contents
frame, ie, consts always frame / information-element encode & decode
g711 always G.711 µ-law codec
client always sans-io PbxClient state machine (one per account)
jitter always adaptive jitter buffer
resample dsp polyphase sinc anti-alias resampling (rubato)
audio audio microphone / speaker via cpal (cross-platform)

Feature flags: net (async UDP driver, tokio), dsp (resampling), audio (= dsp + net + cpal). The default feature set is empty.

Library usage

The PbxClient is a sans-io state machine. You own the socket and the clock; the client tells you what to send, what happened, and when to wake it next:

use iax2::{PbxClient, Config, Command, Event};
use std::time::Instant;

let mut client = PbxClient::new(Config::new("Office", "1001", "secret"));

loop {
    // 1) drain outgoing datagrams to your UDP socket
    while let Some(datagram) = client.poll_transmit() {
        // socket.send(&datagram)?;
    }

    // 2) react to protocol events
    while let Some(event) = client.poll_event() {
        match event {
            Event::Registered => println!("registered"),
            Event::Incoming { call, from, .. } => {
                println!("incoming call from {from}");
                client.handle_command(Command::Answer { call }, Instant::now());
            }
            Event::Voice { call, ts, ulaw } => {
                // decode µ-law and feed it to iax2::JitterBuffer keyed by `ts`
            }
            _ => {}
        }
    }

    // 3) feed inbound datagrams and tick the clock
    // client.handle_input(&buf[..n], Instant::now());
    // client.handle_timeout(Instant::now());
    // then sleep until client.poll_timeout()
}

The jitter buffer is equally I/O-free — push received frames keyed by their timestamp, pull one 20 ms frame per playout tick:

use iax2::{JitterBuffer, Pull};
use std::time::Instant;

let mut jb = JitterBuffer::new();
jb.push(timestamp_ms, ulaw_payload, Instant::now());

match jb.pull() {
    Pull::Play(ulaw) => { /* decode and play */ }
    Pull::Conceal    => { /* packet lost: do PLC */ }
    Pull::Silence    => { /* buffer priming / underrun */ }
}

Example binaries

Built with --features audio (or net for the diagnostic-only one):

binary feature what it does
regtest net registration handshake only (diagnostics)
calltest audio outbound call to the *43 echo test, with audio
regdaemon audio persistent registration + answers a single inbound call
softphone audio multi-line / multi-PBX softphone (keyboard-driven)

The softphone registers against one or more PBXes (one account each), rings on inbound calls, supports call waiting across PBXes, hold/unhold, call switching and DTMF, and runs the adaptive jitter buffer on the active call.

softphone <host> <user> <secret> [port] [refresh] [name]   # single account
softphone accounts.conf                                     # multiple PBXes

See accounts.conf.example for the multi-PBX configuration format.

Building

cargo build --release --features audio    # all binaries
cargo build --release --features net      # regtest only
cargo build                               # protocol core, no I/O
  • Linux needs ALSA development headers for cpal: libasound2-dev and pkg-config.
  • Windows uses WASAPI — no system dependencies.
  • rubato is pinned to =0.15.0 (the resampler API is not yet stable across minor versions).

Minimum supported Rust version: 1.75.

FreePBX / Asterisk setup

Create an IAX2 extension (type=friend, a secret, host=dynamic), allow=ulaw, context=from-internal. Call-token authentication (requirecalltoken=yes) is supported and expected.

To receive calls, the peer must be registered (qualify OK). The client answers POKE keepalives, so once registered Asterisk routes calls to it.

Scope and limitations

This crate is a focused, honest implementation — not a full IAX2 stack:

  • Codec: G.711 µ-law only. No A-law, GSM, G.729 or other formats yet.
  • No video, text messages, or trunked (meta) frames.
  • Call transfer is done through PBX feature codes (sent as DTMF), not via the IAX2 native transfer (TXREQ/TXMEDIA) media-path optimization, which is not implemented.
  • No acoustic echo cancellation — use a headset, or wire your own AEC into the audio layer (the playout reference and capture frames are both exposed).
  • Validated with requirecalltoken=yes, ulaw, from-internal. Other configurations may work but are untested.

Contributions and field reports against other PBX setups are welcome.

Protocol notes

A few hard-won details, in case they save you the packet captures they cost us:

  • Call-token pre-auth means resending the original request unchanged with the token added — not learning sequence numbers from the token frame.
  • The qualify PONG must echo the POKE's timestamp, not your own uptime, or Asterisk computes an absurd round-trip time and marks the peer unreachable.
  • The same echo rule applies to LAGRP replying to a LAGRQ.
  • Frame body parsing depends on the frame type: IAX/CONTROL bodies are information elements; VOICE/VIDEO bodies are raw media.
  • A POKE keepalive holds the NAT pinhole open; for a stable deployment a point-to-point overlay (e.g. WireGuard) is the real fix.

License

Licensed under either of

at your option.