TURN Server
Use this page when you need a real relay, not a STUN-only best-case path.
TURN is what gets you through the ugly NAT cases that direct peer-to-peer never will. icey's TURN server is built on the same runtime and socket layer as the rest of the library, so you can run relay, signalling, and application code in one process if you want to.
If TURN is new territory, read STUN first. TURN messages are STUN messages with allocation and relay semantics layered on top.
What You Actually Need
A TURN server that works in practice needs four things right:
- bind address
realm- credentials
- the right public relay address
That last one is where people usually waste time.
If your process binds to a private address but clients are out on the public internet, set externalIP correctly or the server will advertise the wrong relay address.
Minimal Server
#include "icy/turn/server/server.h"
using namespace icy;
class MyServer : public turn::ServerObserver {
public:
turn::Server server;
MyServer(const turn::ServerOptions& opts)
: server(*this, opts) {}
turn::AuthenticationState authenticateRequest(turn::Server*, turn::Request& req) override;
void onServerAllocationCreated(turn::Server*, turn::IAllocation*) override {}
void onServerAllocationRemoved(turn::Server*, turn::IAllocation*) override {}
};
turn::ServerOptions opts;
opts.realm = "example.com";
opts.listenAddr = net::Address("0.0.0.0", 3478);
opts.externalIP = "203.0.113.1";
opts.enableUDP = true;
opts.enableTCP = true;
MyServer srv(opts);
srv.server.start();That gives you the actual TURN server. The rest of the work is auth policy and deployment detail.
Authentication
icey's TURN server uses the standard long-term credential path from RFC 5389.
The usual flow is:
- client sends unauthenticated
Allocate - server responds
401 - client retries with
USERNAME,REALM,NONCE, andMESSAGE-INTEGRITY - server verifies the HMAC and proceeds
Your observer decides what to do:
AuthorizedNotAuthorizedQuotaReachedAuthenticating
That last state matters if you want to do real async auth without blocking the loop.
The externalIP Rule
This is the thing to get right before anything else.
Use externalIP when:
- the server binds to
10.x,172.16-31.x, or192.168.x - the box is behind cloud or host networking indirection
- clients need a public relay address
Do not leave it empty and hope ICE will work it out later. TURN needs to tell the client the truth up front.
Local Permissions
icey can auto-grant permissions for local and RFC 1918 peers:
opts.enableLocalIPPermissions = true;That is useful for development and mixed LAN/public deployments.
It is not a substitute for understanding TURN permissions. It is a convenience for the cases where local probes and local peers are normal.
Testing It
The included sample is still the fastest way to prove the server path works:
You can hit it with coturn's turnutils_uclient:
turnutils_uclient -u username -w password 127.0.0.1That gives you a much faster signal than trying to debug a full browser stack before the relay basics are sound.
Production Notes
- Replace the hard-coded sample credentials with a real auth backend.
- Set
externalIPto the public relay IP the client should use. - Keep UDP and TCP enabled unless you have a strong reason not to.
- Watch allocation counts and lifetimes if you care about quotas and abuse.
- Treat
CreatePermissionand relay traffic as part of the real data plane, not as optional control-plane noise.
Good Next Stops
- TURN guide for the full server and client surface
- STUN guide for the shared message model
turnserverfor the sample binary- Run icey-server if TURN is part of a full self-hosted browser media deployment
