The STUNner blog

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 “.nip.io” domain name.

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: /32).

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.