XTurn

An Elixir TURN / STUN server for WebRTC.

If you have used RTCPeerConnection with iceServers, you have already talked to something like this. When two browsers cannot send media directly (home routers, corporate firewalls, mobile CGNAT), ICE asks a TURN server to relay packets. XTurn is that relay -- plus STUN Binding for "what is my public address?"

Home: https://github.com/Lazarus404/xturn

Hex: https://hex.pm/packages/xturn

Installation

Add to your mix.exs dependencies:

def deps do
[
{:xturn, "~> 2.0"}
]
end

Then configure listeners and auth in your app config (see Setup below). For a standalone server checkout, clone the repo and run mix deps.get then mix run --no-halt.

Monorepo / local xsockets checkout:

mix deps.get

Set XTURN_SERVER_IP (and optional certs under certs/) before binding on a real NIC. Defaults in config/config.exs advertise 127.0.0.1.

What problem this solves

WebRTC wants peer-to-peer. The internet often says no.

XTurn speaks those protocols on UDP, TCP, TLS, and DTLS so browsers and native clients can keep calls working when host / server-reflexive candidates fail.

You configure listeners and auth in Elixir config. Clients still use normal WebRTC APIs (iceServers: [{ urls: "turn:...", username, credential }]).

RFCs (the specs we implement)

You do not need to read these to run the server. They are here when you want the official wording:

What you get

How sockets and framing work underneath lives in xsockets. How the TURN pieces fit together is in ARCHITECTURE.md. Plugin hooks: PLUGIN.md.

Setup

Open config/config.exs. Almost everything lives there (and in config/runtime.exs for env overrides).

Logging

Debug logging is great while you learn. It is expensive at scale.

For production, set the logger to :info or :error. Leave :debug for local dev only.

config :logger,
level: :debug,
compile_time_purge_level: :debug

Ports

Default idea (when cert files exist):

Port 443 is never hardcoded. To serve TURNS on 80 or 443, set XTURN_TURNS_PORT at runtime.

config :xturn,
authentication: %{required: true},
permissions: %{required: false},
realm: "xirsys.com",
listen: [
{:udp, ~c"0.0.0.0", 3478},
{:tcp, ~c"0.0.0.0", 3478},
{:udp, ~c"::", 3478},
{:tcp, ~c"::", 3478},
{:udp, ~c"0.0.0.0", 5349, :secure},
{:tcp, ~c"0.0.0.0", 5349, :secure},
{:udp, ~c"::", 5349, :secure},
{:tcp, ~c"::", 5349, :secure}
],
server_ip: {127, 0, 0, 1},
server_local_ip: {0, 0, 0, 0},
certs: [
certfile: "certs/server.crt",
keyfile: "certs/server.key"
]

Env knobs:

See config/runtime.exs and Xirsys.XTurn.ListenConfig.

Config cheat sheet:

Authentication (think "how ICE logs in")

Browsers and coturn-style stacks use two common patterns. XTurn supports both.

Long-term credentials -- a username/password you store (via POST /auth or Auth.Client.add_user/4). On first try the server answers 401 with a nonce; the client retries with MESSAGE-INTEGRITY using MD5(username:realm:password) as the key. Nonces rotate and expire after nonce_max_age_ms (default one hour). An expired nonce becomes 438 Stale Nonce.

Shared-secret / TTL credentials (TURN REST) -- same idea as coturn use-auth-secret. Off by default. Turn it on and mint credentials with GET /auth/rest?username=<id>&ttl=<seconds> (returns 503 while disabled):

config :xturn,
shared_secret: [
enabled: true,
secret: "your-shared-secret",
default_ttl_seconds: 86_400
]

Usernames look like "<expiry-unix>:<user-id>". Passwords are Base64(HMAC-SHA1(secret, username)). If a username matches that timestamp shape and shared secret is enabled, we verify that way; otherwise we use the long-term store.

Allocation quota -- optional allocation_quota caps how many allocations one username can hold at once (RFC 8656 -> 486). Unset by default so a laptop lab is not capped.

RFC 7635 third-party auth -- optional ACCESS-TOKEN path. Long-term + SHA-256 STUN integrity stays the default.

Short-term credentials -- not implemented for TURN (and RFC 8656 says TURN should use long-term). Plain STUN Binding can skip auth in our pipeline. Coturn does not do short-term TURN either.

TURNS certificates (for real browsers)

Production turns: / DTLS needs a publicly trusted cert for the hostname clients dial (for example turn.example.com):

config :xturn,
certs: [
certfile: "/etc/xturn/certs/turn.example.com.crt",
keyfile: "/etc/xturn/certs/turn.example.com.key"
],
cert_watch_interval_ms: 60_000

Issue and renew certs outside XTurn with lego and DNS-01. XTurn polls the PEM paths and reloads secure listeners when files change.

Obtain and renew (lego + DNS-01)

Start with Let's Encrypt. Switching CA later is mostly a --server flag change; XTurn does not care which CA signed the files.

lego --email ops@example.com \
--domains turn.example.com \
--dns cloudflare \
--path /etc/xturn/lego \
renew --days 30 \
--renew-hook 'XTURN_CERT_DOMAIN=turn.example.com /path/to/xturn/scripts/xturn-deploy-certs.sh'

Copy scripts/xturn-deploy-certs.sh onto the host and set XTURN_CERT_DOMAIN in the hook. The script installs PEMs under /etc/xturn/certs/ (mode 0640). No special signal is required; the cert watcher notices within one poll interval.

For an immediate reload, set XTURN_SERVICE to your systemd unit name so the hook can send SIGHUP (XTurn maps that to Certs.reload/0).

Check the chain looks complete:

openssl crl2pkcs7 -nocrl -certfile /etc/xturn/certs/turn.example.com.crt \
| openssl pkcs7 -print_certs -noout

Expect two or more certificates listed.

Native clients

Browsers use the OS trust store. Some native WebRTC stacks (Flutter, React Native, Unreal) ship a stricter root list and have rejected some Let's Encrypt chains in the past. If you ship native apps, test turns: early; ZeroSSL or Google Trust Services are common fallbacks if you see "Unknown CA".

Local development

Self-signed files under certs/ are enough (see config/test.exs). No lego required.

HTTP API (Maru)

Maru is a small Elixir HTTP layer. XTurn uses it for operator endpoints: create users, mint TURN REST credentials, peek at allocation counts.

config :maru, Xirsys.API,
http: [port: 8880]

Change the port if something else already owns 8880.

Changelog

See CHANGELOG.md.

Contact

Questions or ideas: Jahred at experts@xirsys.com

License

BSD-3-Clause. See LICENSE.md.