Running STUNner as a public TURN server, Part 1
In my previous blog post, I explored two open-source TURN server implementations: coTurn and STUNner. We looked at their strengths and weaknesses, particularly in the context of WebRTC, where TURN servers play a crucial role in facilitating media transmission through firewalls and NATs.
While coTurn is a widely-used and robust solution for public TURN server needs, STUNner was designed with a different focus. Created as a Kubernetes-native media gateway, STUNner provides a streamlined solution to deploy WebRTC media servers within Kubernetes environments. Its Kubernetes-centric design means STUNner offers unique benefits for those running media services within containerized clusters.
However, STUNner’s flexible architecture means it’s not limited to Kubernetes based media server deployments. With a straightforward setup using a lightweight Kubernetes distribution (e.g. K3s or MicroK8s), STUNner can also be configured as a standalone, public TURN server — allowing developers to leverage its Kubernetes-native strengths in a more traditional TURN server role.
In this blog post, I’ll walk through the steps to set up STUNner as a public TURN server. Whether you’re new to Kubernetes or just looking for an alternative to coTurn, this guide will show you how to get STUNner running and configured with ease. Let’s get started!
1. Create a virtual machine
As a first step, you’ll need a server or a virtual machine on the public internet to host STUNner. For this, we need to have a VM directly with a public IP address, unfortunately private networking with a public NAT address won’t work. To that end, mainstream cloud providers (e.g. AWS, Azure, GCP) are not good for this setup. I suggest using providers like Linode or Digital Ocean, since they support VMs directly on the public internet. Moreover, many VPS providers also use public networking so if you want to have a budget option I can also suggest providers like Hetzner or Contabo.
2. Set-up Kubernetes
To run STUNner as a standalone TURN server, we’ll first set up a lightweight Kubernetes environment. Here, we’ll use K3s — a minimal Kubernetes distribution designed for quick and efficient setups in smaller environments. K3s is an excellent choice because it’s easy to install, has a low resource footprint, and is suitable for both local and cloud-based deployments. Once K3s is up and running, we’ll install STUNner within this environment and configure it as a public TURN server.
The installation of K3s is very simple, requiring only a single command. SSH into your server and execute the following command:
curl -sfL https://get.k3s.io | sh -
This script installs K3s and automatically starts a single-node Kubernetes cluster. You can verify the installation by running:
kubectl get nodes
I typically run the following commands as well to have bash completion for kubectl:
echo 'source <(kubectl completion bash)' >>~/.bashrc
echo 'alias k=kubectl' >>~/.bashrc
echo 'complete -o default -F __start_kubectl k' >>~/.bashrc
You’ll also need to install Helm, which requires some special steps in k3s so run the following commands.
mkdir -p $HOME/.kube
sudo cp -i /etc/rancher/k3s/k3s.yaml $HOME/.kube/config
sudo chown $(id -u):$(id -g) /etc/rancher/k3s/k3s.yaml
sudo chown $(id -u):$(id -g) $HOME/.kube/config
curl https://raw.githubusercontent.com/helm/helm/master/scripts/get-helm-3 | bash
echo 'source <(helm completion bash)' >>~/.bashrc
There you go, now you have a fully working Kubernetes cluster that can be used to deploy STUNner as a public TURN server.
3. Create TLS certificates using cert-manager
STUNner can be used to set-up secure TURN endpoints (with TLS and DTLS protocols), but for this we’ll need a valid SSL/TLS certificate. We’ll use cert-manager, a powerful Kubernetes-native tool, to automatically request and manage certificates from Let’s Encrypt, a trusted certificate authority (CA). Cert-manager will handle all aspects of obtaining and renewing the certificates, simplifying the process significantly.
As a first step you’ll need to add the public IP of the virtual machine to your DNS provider, since Let’s Encrypt will contact the server to verify your domain. In this blog, I’ll use “turn.stunner.cc” as the example domain name, please replace this to your own. Also, my favorite command to check your public IPv4 is this:
curl -4 ifconfig.me
Alternatively, you can use nip.io for your test with the “
Now, we’ll use Helm to install cert-manager in K3s, so add the cert-manager Helm repository:
helm repo add jetstack https://charts.jetstack.io
helm repo update
Now, install cert-manager using Helm. It’s recommended to specify the namespace (cert-manager) for better organization:
kubectl create namespace cert-manager
helm install cert-manager jetstack/cert-manager --namespace cert-manager --set installCRDs=true
Check that cert-manager is up and running (all pods should be in a Running status):
kubectl get pods -n cert-manager
To obtain certificates from Let’s Encrypt, you need to create an Issuer or ClusterIssuer resource that cert-manager can use to interact with Let’s Encrypt.
Here, we’ll set up a ClusterIssuer (a global issuer usable across all namespaces) for Let’s Encrypt. You’ll need to choose between the staging environment, which allows testing without hitting Let’s Encrypt rate limits, and the production environment, which issues real certificates. Create a YAML file (letsencrypt-issuer.yaml) with the following content:
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-production
spec:
acme:
email: your-email@example.com # Replace with your email
server: https://acme-v02.api.letsencrypt.org/directory
privateKeySecretRef:
name: letsencrypt-production
solvers:
- http01:
ingress:
class: traefik # Use "traefik" if you're using Traefik ingress (built-in to k3s), otherwise adjust as needed
Apply the ClusterIssuer configuration:
kubectl apply -f letsencrypt-issuer.yaml
Create a Certificate Resource for STUNner
Now that cert-manager and the ClusterIssuer are ready, let’s create a certificate resource for STUNner. This will automatically request a Let’s Encrypt certificate for STUNner’s domain and keep it renewed.
First, create a namespace where all the STUNner specific resources will be installed:
kubectl create namespace stunner
Now, create a YAML file (stunner-cert.yaml) with the following content:
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: stunner-cert
namespace: stunner
spec:
secretName: stunner-tls # The name of the secret that will store the TLS certificate
issuerRef:
name: letsencrypt-production
kind: ClusterIssuer
commonName: turn.stunner.cc # Replace with your domain name
dnsNames:
- turn.stunner.cc # Replace with your domain name
Apply the certificate configuration:
kubectl apply -f stunner-cert.yaml
To check that cert-manager successfully issued the certificate, you can inspect the created secret:
kubectl get certificates -n stunner
kubectl get secret -n stunner stunner-tls -o yaml
If the certificate has been issued, you should see tls.crt and tls.key fields in the secret, indicating that the certificate and private key are stored securely.
4. Install and set-up STUNner
The easiest way to install STUNner is via the STUNner Gateway Operator. This operator will help manage and configure the stunnerd instances on the cluster using CRDs.
Use the following Helm command to install the operator:
helm repo add stunner https://l7mp.io/stunner
helm repo update
helm install stunner-gateway-operator stunner/stunner-gateway-operator --create-namespace --namespace=stunner --set stunnerGatewayOperator.dataplane.spec.hostNetwork=true
It is important to set the variable stunnerGatewayOperator.dataplane.spec.hostNetwork to true, since that will deploy STUNner in so-called HostNetwork mode, which will result in that STUNner will use the VMs root network namespace, therefore will have the same primary IP address as the VM, which is important when you want a TURN server on the public internet.
Then, we need a GetewayConfig to set-up the authentication mode in STUNner. Here we’ll use a simple username/password combination. It is important to know that STUNner can be set-up with more advanced authentication, but for this blog we’ll keep it simple. Apply the following yaml:
apiVersion: stunner.l7mp.io/v1
kind: GatewayConfig
metadata:
name: stunner-gatewayconfig
namespace: stunner
spec:
realm: stunner.l7mp.io
authType: plaintext
userName: "stunneruser"
password: "stunnerpassword"
Next, we need to create a GatewayClass, where we refer to the previous configuration. Apply the following yaml.
apiVersion: gateway.networking.k8s.io/v1
kind: GatewayClass
metadata:
name: stunner-gatewayclass
spec:
controllerName: "stunner.l7mp.io/gateway-operator"
parametersRef:
group: "stunner.l7mp.io"
kind: GatewayConfig
name: stunner-gatewayconfig
namespace: stunner
description: "Public STUNner TURN server"
Now, let’s create a Gateway that will tell STUNner on which ports it should listen to TURN traffic. In this example we create all possible protocols: UDP, TCP, TLS and DTLS. Apply this yaml:
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
annotations:
stunner.l7mp.io/enable-mixed-protocol-lb: "true"
name: public-stunner-server
namespace: stunner
spec:
gatewayClassName: stunner-gatewayclass
listeners:
- name: udp-listener
port: 3478
protocol: UDP
- name: tcp-listener
port: 3478
protocol: TURN-TCP
- name: tls-listener
port: 5349
protocol: TURN-TLS
tls:
certificateRefs:
- kind: Secret
name: stunner-tls
namespace: stunner
- name: dtls-listener
port: 5349
protocol: TURN-DTLS
tls:
certificateRefs:
- kind: Secret
name: stunner-tls
namespace: stunner
Now STUNner is running as a public TURN server. However, one final config needs to be added. STUNner by default limits the IP address where peers can connect, so we need to add these address prefixes. In a normal STUNner setup where we want to connect to media servers running inside Kubernetes, this can be done by adding UDProutes pointing to the services of the given media server deployment. But it a case of a public TURN server peers can come from anywhere on the internet, so we basically need to open STUNner to every possible IP address which can be done with a StaticService as follows:
apiVersion: stunner.l7mp.io/v1
kind: StaticService
metadata:
name: static-svc
namespace: stunner
spec:
prefixes:
- "0.0.0.0/0" # allow every IP on the Internet
---
apiVersion: stunner.l7mp.io/v1
kind: UDPRoute
metadata:
name: static-services
namespace: stunner
spec:
parentRefs:
- name: public-stunner-server
rules:
- backendRefs:
- group: stunner.l7mp.io
kind: StaticService
name: static-svc
Using these StaticService resources you can easily limit the IP ranges that you want to allow connections. Moreover, if you only want to support symmetric ICE mode (where both peers have to authenticate and connect to STUNner) you can just list the IPs of your public STUNner servers (e.g. use prefix:
Finally, if something does not work, there might be some firewalls blocking access, so ensure that ports used by STUN/TURN servers (such as 3478 for STUN/TURN and 5349 for TLS) are open on the server’s firewall. Also, the Traefik ingress needs to be accessible for Let’s Encrypt. Here’s an example of opening these ports:
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow 3478/tcp
sudo ufw allow 3478/udp
sudo ufw allow 5349/tcp
sudo ufw allow 5349/udp
That’s it, now you have a fully functional public TURN server on the internet. You can test your TURN server e.g. on this site.
5. Conclusion
In this blog post I showed how to run STUNner as a standalone public TURN server. It might seem like it takes a lot of steps, but you can just use my GitHub repo to run a simple script that collects all these into one file and you should have a working TURN server in a minute or so.
Obviously, for a production-grade public TURN server to support your media services, additional configuration is essential. This includes implementing ephemeral credentials for enhanced security, setting up monitoring and alerting to quickly identify and resolve issues, and deploying multiple STUNner instances to handle higher user traffic. Ideally, these instances should be geo-distributed to ensure peers connect to the nearest TURN server, minimizing round-trip time. You might also consider enabling autoscaling to dynamically adjust capacity as demand fluctuates. STUNner supports all of these advanced setups, allowing you to create a robust, scalable, and efficient TURN infrastructure. In Part 2 of this blog series you can get more information about the specific features in STUNner Membership and Enterprise tiers that support a highly scalable, cloud native TURN server setup. If you’re interested in exploring these options, feel free to reach out to us at L7mp!
Also, we basically use a similar configuration to test STUNner with ICEPerf. You can check the results here, and see for yourself that the numbers are quite compatible even with the most advanced hosted TURN service providers.
