content

I'm trying to get a voice channel working for t/suki's Tuwunel matrix server. Originally, I tried setting up just Element Call. This allowed me and other users to join calls, but no one could talk to each other. From what I've gathered in my previous experiments trying to get this to work, I think a TURN server is required for Element Call to work at all. So, I've decided to set up Coturn as recommended by the Tuwunel guide:

/opt/matrix-rtc/docker-compose.yml:

services:
  coturn:
    image: docker.io/coturn/coturn:4.8.0-alpine
    container_name: coturn
    restart: unless-stopped
    network_mode: host
    volumes:
      - ./coturn.conf:/etc/coturn/turnserver.conf

  matrix-rtc-jwt:
    image: ghcr.io/element-hq/lk-jwt-service:latest
    container_name: matrix-rtc-jwt
    environment:
      - LIVEKIT_JWT_BIND=:8081
      - LIVEKIT_URL=wss://matrix-rtc.tsuki.games
      - LIVEKIT_KEY=<livekit key>
      - LIVEKIT_SECRET=<livekit secret>
      - LIVEKIT_FULL_ACCESS_HOMESERVERS=tsuki.games
    restart: unless-stopped
    network_mode: host

  matrix-rtc-livekit:
    image: livekit/livekit-server:latest
    container_name: matrix-rtc-livekit
    command: --config /etc/livekit.yaml
    restart: unless-stopped
    volumes:
      - ./livekit.yaml:/etc/livekit.yaml:ro
    network_mode: host

I'm using host networking as suggested in the official Tuwunel guide, although I'm using it for all of the services instead of just coturn and matrix-rtc-livekit since it appears that matrix-rtc-jwt also needs to be on the same network as matrix-rtc-livekit in order to work.

/opt/matrix-rtc/coturn.conf:

use-auth-secret
static-auth-secret=<coturn secret>
realm=matrix-rtc.tsuki.games

/opt/matrix-rtc/livekit.yaml:

port: 7880
bind_addresses:
  - ""
rtc:
  tcp_port: 7881
  port_range_start: 50100
  port_range_end: 50200
  use_external_ip: true
  enable_loopback_candidate: false
keys:
  <livekit key>:<livekit secret>

/opt/matrix/tuwunel.toml

[global]
server_name = "tsuki.games"
address = "0.0.0.0"
query_over_tcp_only = true
allow_registration = false
require_auth_for_profile_requests = true
allow_public_room_directory_over_federation = true
show_all_local_users_in_user_directory = true
trusted_servers = [
  "matrix.expiredpopsicle.com",
]
turn_uris = [
  "turns:matrix-rtc.tsuki.games?transport=udp",
  "turns:matrix-rtc.tsuki.games?transport=tcp",
]
turn_secret = "<coturn secret>"
auto_join_rooms = [ "#tsuki:tsuki.games", "#general:tsuki.games" ]
single_sso = true

[[global.identity_provider]]
brand = "Discourse"
client_id = "matrix"
client_secret = "<oidc secret>"
issuer_url = "https://forum.tsuki.games/oauth2"
callback_url = "https://matrix.tsuki.games/_matrix/client/unstable/login/sso/callback/matrix"
base_path = "oauth2"

[global.well_known]
client = "https://matrix.tsuki.games"
server = "matrix.tsuki.games:443"

[[global.well_known.rtc_transports]]
type = "livekit"
livekit_service_url = "https://matrix-rtc.tsuki.games"

However, I use a reverse proxy called NGINX, which is running in a docker container provided by SWAG. This means I have to somehow tell the reverse proxy how to route requests to the host container.

I found docker/for-linux#264, in which a user points out that this can only be done on linux by adding the following to the container's configuration...

services:
  myservice:
    extra_hosts:
      - host.docker.internal:host-gateway

...where host.docker.internal could be anything. I copy this exact configuration and note that the /etc/hosts file has been updated:

exodrifter@moonlight:/opt/swag$ sudo docker compose exec swag cat /etc/hosts
[sudo: authenticate] Password:
127.0.0.1	localhost
::1	localhost ip6-localhost ip6-loopback
fe00::	ip6-localnet
ff00::	ip6-mcastprefix
ff02::1	ip6-allnodes
ff02::2	ip6-allrouters
172.17.0.1	host.docker.internal
172.18.0.3	619aca4f5894

However, for some reason, nslookup still fails to resolve host.docker.internal. I decide to ignore this issue and update the nginx configuration to go to the specified IP address:

/opt/swag/config/nginx/proxy-confs/matrix-rtc.subdomain.conf:

server {
    listen 443 ssl;
    listen [::]:443 ssl;
    http2 on;
    server_name matrix-rtc.*;

    include /config/nginx/ssl.conf;

    # lk-jwt-service
    location ~ ^(/sfu/get|/healthz) {
        include /config/nginx/proxy.conf;
        include /config/nginx/resolver.conf;
        set $upstream_app 172.17.0.1;
        set $upstream_port 8081;
        set $upstream_proto http;
        proxy_pass $upstream_proto://$upstream_app:$upstream_port;
    }

    # livekit
    location / {
        include /config/nginx/proxy.conf;
        include /config/nginx/resolver.conf;
        set $upstream_app 172.17.0.1;
        set $upstream_port 7880;
        set $upstream_proto http;
        proxy_pass $upstream_proto://$upstream_app:$upstream_port;

        proxy_set_header Connection "upgrade";
        proxy_set_header Upgrade $http_upgrade;
    }
}

However, requests still fail and trying to load https://matrix-rtc.tsuki.games/healthz in my web browser results in a timeout. Another comment from the earlier issue suggests UFW may be the problem. Checking the UFW log at /var/log/ufw.log I can see that requests from my SWAG container running NGINX is being blocked from connecting to my docker network.

2026-02-17T22:28:25.907194+00:00 moonlight kernel: [UFW BLOCK] IN=br-f0ffa4389059 OUT= MAC=8a:42:46:12:b8:7b:3e:54:03:87:ee:e5:08:00 SRC=172.18.0.3 DST=172.17.0.1 LEN=60 TOS=0x00 PREC=0x00 TTL=64 ID=65380 DF PROTO=TCP SPT=38856 DPT=8081 WINDOW=64240 RES=0x00 SYN URGP=0
2026-02-17T22:28:47.936020+00:00 moonlight kernel: [UFW BLOCK] IN=br-f0ffa4389059 OUT= MAC=8a:42:46:12:b8:7b:3e:54:03:87:ee:e5:08:00 SRC=172.18.0.3 DST=172.17.0.1 LEN=60 TOS=0x00 PREC=0x00 TTL=64 ID=49979 DF PROTO=TCP SPT=54202 DPT=8081 WINDOW=64240 RES=0x00 SYN URGP=0

So, I ran ufw allow from 172.18.0.3 and tried to load https://matrix-rtc.tsuki.games/healthz in my web browser. This time the request succeeded!

However, if I try to join a voice call, the websocket connection disconnects after a few seconds and I still cannot talk to anyone. I realize at this point that I never set up a reverse proxy for Coturn, and I have no idea how that would work since it's a websocket connection.

I find coturn/coturn#702, in which someone posted their nginx configuration for Coturn and mentioned that they were using NGINX through SWAG, which is exactly what I'm doing.

meta

created:

backlinks: t/suki logs

commit: e4c2d32a