Author SHA1 Message Date
Jason Ross fd23627547 change to Rust WebRTC TURN server 2026-04-02 20:10:21 -05:00
Jason Ross e08305516b Merge pull request #2 from JMR-dev/fix-image-pull-issue
fixed image pull issue
2026-04-01 20:32:08 -05:00
10 changed files with 132 additions and 66 deletions
+3 -3
View File
@@ -32,12 +32,12 @@ jobs:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push Coturn image
- name: Build and push TURN server image
uses: docker/build-push-action@v5
with:
context: ./docker/coturn
context: ./docker/turn-server
push: true
tags: ghcr.io/${{ env.OWNER_LC }}/stoat-coturn:4.9.0-trixie
tags: ghcr.io/${{ env.OWNER_LC }}/stoat-turn-server:4.0.1
- name: Build and push Caddy WAF image
uses: docker/build-push-action@v5
+62 -19
View File
@@ -8,8 +8,9 @@ jobs:
runs-on: ubuntu-latest
permissions:
contents: read
packages: read
env:
TF_VAR_gcp_project: ${{ secrets.GCP_PROJECT }}
TF_VAR_gcp_project: ${{ vars.GCP_PROJECT }}
TF_VAR_gcp_region: ${{ vars.GCP_REGION }}
TF_VAR_gcp_zone: ${{ vars.GCP_ZONE }}
@@ -17,17 +18,56 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v4
- name: Validate deploy configuration
shell: bash
env:
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
GCP_SA_KEY: ${{ secrets.GCP_SA_KEY }}
R2_ENDPOINT_URL: ${{ secrets.R2_ENDPOINT_URL }}
R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
TURN_SHARED_SECRET: ${{ secrets.TURN_SHARED_SECRET }}
GCP_PROJECT: ${{ vars.GCP_PROJECT }}
GCP_REGION: ${{ vars.GCP_REGION }}
GCP_ZONE: ${{ vars.GCP_ZONE }}
TURN_REALM: ${{ vars.TURN_REALM }}
CADDY_EMAIL: ${{ vars.CADDY_EMAIL }}
run: |
set -eu
require_value() {
local kind="$1"
local name="$2"
local value="$3"
if [ -z "${value}" ]; then
echo "::error::Missing required GitHub ${kind} ${name}." >&2
exit 1
fi
}
require_value "secret" "SSH_PRIVATE_KEY" "${SSH_PRIVATE_KEY}"
require_value "secret" "GCP_SA_KEY" "${GCP_SA_KEY}"
require_value "secret" "R2_ENDPOINT_URL" "${R2_ENDPOINT_URL}"
require_value "secret" "R2_ACCESS_KEY_ID" "${R2_ACCESS_KEY_ID}"
require_value "secret" "R2_SECRET_ACCESS_KEY" "${R2_SECRET_ACCESS_KEY}"
require_value "secret" "TURN_SHARED_SECRET" "${TURN_SHARED_SECRET}"
require_value "variable" "GCP_PROJECT" "${GCP_PROJECT}"
require_value "variable" "GCP_REGION" "${GCP_REGION}"
require_value "variable" "GCP_ZONE" "${GCP_ZONE}"
require_value "variable" "TURN_REALM" "${TURN_REALM}"
require_value "variable" "CADDY_EMAIL" "${CADDY_EMAIL}"
- name: Prepare SSH key for provisioning
shell: bash
env:
SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
run: |
install -m 700 -d "${HOME}/.ssh"
printf '%s\n' "${{ secrets.SSH_PRIVATE_KEY }}" > "${HOME}/.ssh/id_ed25519"
printf '%s\n' "${SSH_PRIVATE_KEY}" | tr -d '\r' > "${HOME}/.ssh/id_ed25519"
chmod 600 "${HOME}/.ssh/id_ed25519"
{
echo "TF_VAR_admin_ssh_public_key<<EOF"
ssh-keygen -y -f "${HOME}/.ssh/id_ed25519"
echo "EOF"
} >> "${GITHUB_ENV}"
admin_ssh_public_key="$(ssh-keygen -y -f "${HOME}/.ssh/id_ed25519")"
printf 'TF_VAR_admin_ssh_public_key=%s\n' "${admin_ssh_public_key}" >> "${GITHUB_ENV}"
- name: Prepare Google credentials file
shell: bash
@@ -47,7 +87,7 @@ jobs:
cat > backend.hcl <<EOF
key = "webrtc-relay/terraform.tfstate"
endpoints = {
s3 = ${{ secrets.R2_ENDPOINT_URL }}
s3 = "${{ secrets.R2_ENDPOINT_URL }}"
}
access_key = "${{ secrets.R2_ACCESS_KEY_ID }}"
secret_key = "${{ secrets.R2_SECRET_ACCESS_KEY }}"
@@ -60,18 +100,20 @@ jobs:
run: |
tofu init -backend-config=backend.hcl
tofu apply -auto-approve -input=false
echo "${{ secrets.STATIC_IP }}=$(tofu output -raw relay_ip)" >> "${GITHUB_ENV}"
relay_ip="$(tofu output -raw relay_ip)"
echo "STATIC_IP=${relay_ip}" >> "${GITHUB_ENV}"
echo "relay_ip=${relay_ip}" >> "${GITHUB_OUTPUT}"
- name: Wait for SSH
shell: bash
run: |
for attempt in {1..30}; do
if nc -z -w 5 "${${{ secrets.STATIC_IP }}}" 22; then
if nc -z -w 5 "${STATIC_IP}" 22; then
exit 0
fi
sleep 10
done
echo "SSH did not become reachable on ${${{ secrets.STATIC_IP }}}" >&2
echo "SSH did not become reachable on ${STATIC_IP}" >&2
exit 1
- name: Run Ansible Playbook
@@ -82,13 +124,13 @@ jobs:
key: ${{ secrets.SSH_PRIVATE_KEY }}
inventory: |
[turn_nodes]
${{ secrets.STATIC_IP }} ansible_user=ubuntu
${{ steps.tofu.outputs.relay_ip }} ansible_user=ubuntu
options: --ssh-common-args='-o StrictHostKeyChecking=no'
- name: SCP compose and config files
uses: appleboy/scp-action@v0.1.7
with:
host: ${{ secrets.STATIC_IP }}
host: ${{ steps.tofu.outputs.relay_ip }}
username: ubuntu
key: ${{ secrets.SSH_PRIVATE_KEY }}
source: "compose/*"
@@ -98,20 +140,21 @@ jobs:
- name: Deploy Podman compose stack
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ secrets.STATIC_IP }}
host: ${{ steps.tofu.outputs.relay_ip }}
username: ubuntu
key: ${{ secrets.SSH_PRIVATE_KEY }}
script: |
set -eu
cd /opt/stoat-turn
owner_lc="$(printf '%s' "${{ github.repository_owner }}" | tr '[:upper:]' '[:lower:]')"
cat > .env <<EOF
STATIC_IP = ${{ secrets.STATIC_IP }}
GITHUB_REPOSITORY_OWNER=${{ github.repository_owner }}
TURN_REALM=${{ secrets.TURN_REALM }}
STATIC_IP=${{ steps.tofu.outputs.relay_ip }}
GITHUB_REPOSITORY_OWNER=${owner_lc}
TURN_REALM=${{ vars.TURN_REALM }}
TURN_SHARED_SECRET=${{ secrets.TURN_SHARED_SECRET }}
CADDY_EMAIL=${{ secrets.CADDY_EMAIL }}
CADDY_EMAIL=${{ vars.CADDY_EMAIL }}
EOF
printf '%s\n' "${{ secrets.GITHUB_TOKEN }}" | podman login ghcr.io -u "${{ github.actor }}" --password-stdin
podman compose pull
podman compose up -d
podman compose up -d --remove-orphans
+5 -5
View File
@@ -1,13 +1,13 @@
# WebRTC Outpost
Configuration repository for a Coturn relay that supports a Stoat deployment on GCP. The stack provisions a `relay-main` Ubuntu 24.04 instance, hardens the host with `nftables`, `fail2ban`, and `unattended-upgrades`, then deploys Coturn plus a Coraza-enabled Caddy service with Podman Compose. Caddy handles ACME certificate issuance, HTTPS health checks, and deny-by-default web responses on the TURN hostname. Coturn handles STUN and TURN traffic directly on `3478` and `5349`, reusing the certificate that Caddy stores in the shared data volume.
Configuration repository for a TURN relay that supports a Stoat deployment on GCP. The stack provisions a `relay-main` Ubuntu 24.04 instance, hardens the host with `nftables`, `fail2ban`, and `unattended-upgrades`, then deploys a `turn-rs`-based TURN server plus a Coraza-enabled Caddy service with Podman Compose. Caddy handles ACME certificate issuance, HTTPS health checks, and deny-by-default web responses on the TURN hostname. The TURN server handles STUN and TURN traffic on `3478` and TURN over TLS on `5349/TCP`, reusing the certificate that Caddy stores in the shared data volume.
## Repository Layout
- `tofu/`: OpenTofu infrastructure for the static IP, VM, and GCP firewall rule.
- `ansible/`: Host preparation and hardening for Ubuntu 24.04.
- `compose/`: Runtime configuration for Coturn and Caddy.
- `docker/`: Custom images for Coturn and Caddy.
- `compose/`: Runtime configuration for the TURN server and Caddy.
- `docker/`: Custom images for the TURN server wrapper and Caddy.
- `.github/workflows/`: CI workflows for building images and deploying the stack.
## Required GitHub Secrets
@@ -31,8 +31,8 @@ Configuration repository for a Coturn relay that supports a Stoat deployment on
- The OpenTofu S3 backend is configured at deploy time so the Cloudflare R2 endpoint does not need to be committed to the repository.
- The Google provider reads service account credentials from the standard `GOOGLE_APPLICATION_CREDENTIALS` shell environment variable. Set it to the JSON key file path for local `tofu` runs.
- The custom Coturn image waits for the Caddy-managed certificate for `TURN_REALM` to appear in the shared `caddy_data` volume before starting the TLS listener on `5349`.
- Caddy on the relay host only answers `/health` and returns `403` for other HTTPS requests. TURN and STUN traffic does not pass through Caddy; Coturn receives it directly through host networking.
- The custom TURN wrapper image is built from `ghcr.io/mycrl/turn-server:4.0.1`. It waits for the Caddy-managed certificate for `TURN_REALM`, renders the final TOML config, and then starts `turn-server`.
- Caddy on the relay host only answers `/health` and returns `403` for other HTTPS requests. TURN and STUN traffic does not pass through Caddy; the TURN server receives it directly through host networking.
- The Ansible playbook lowers `net.ipv4.ip_unprivileged_port_start` to `80` so a rootless Podman-managed Caddy container can bind to `80` and `443`.
## DNS Setup
+1 -1
View File
@@ -15,7 +15,7 @@ table inet filter {
tcp dport 22 accept
tcp dport { 80, 443, 3478, 5349 } accept
udp dport { 3478, 5349 } accept
udp dport 3478 accept
udp dport 49152-65535 accept
}
+4 -4
View File
@@ -14,9 +14,9 @@ services:
- caddy_data:/data
- caddy_config:/config
coturn:
image: ghcr.io/${GITHUB_REPOSITORY_OWNER}/stoat-coturn:4.9.0-trixie
container_name: coturn-relay
turn:
image: ghcr.io/${GITHUB_REPOSITORY_OWNER}/stoat-turn-server:4.0.1
container_name: turn-relay
restart: always
depends_on:
- caddy
@@ -26,7 +26,7 @@ services:
TURN_REALM: ${TURN_REALM}
TURN_SHARED_SECRET: ${TURN_SHARED_SECRET}
volumes:
- ./turnserver.conf:/etc/coturn/turnserver.conf:ro
- ./turn-server.toml:/etc/turn-server/config.toml.tmpl:ro
- caddy_data:/caddy-certs:ro
volumes:
+32
View File
@@ -0,0 +1,32 @@
# Rendered at container start by docker/turn-server/entrypoint.sh.
[server]
port-range = "49152..65535"
realm = "__TURN_REALM__"
[[server.interfaces]]
transport = "udp"
listen = "0.0.0.0:3478"
external = "__EXTERNAL_IP__:3478"
[[server.interfaces]]
transport = "tcp"
listen = "0.0.0.0:3478"
external = "__EXTERNAL_IP__:3478"
[[server.interfaces]]
transport = "tcp"
listen = "0.0.0.0:5349"
external = "__EXTERNAL_IP__:5349"
[server.interfaces.ssl]
private-key = "__TLS_KEY_FILE__"
certificate-chain = "__TLS_CERT_FILE__"
[log]
level = "info"
stdout = true
[auth]
enable-hooks-auth = false
static-auth-secret = "__TURN_SHARED_SECRET__"
-14
View File
@@ -1,14 +0,0 @@
fingerprint
use-auth-secret
lt-cred-mech
listening-port=3478
tls-listening-port=5349
min-port=49152
max-port=65535
no-cli
stale-nonce=600
no-loopback-peers
no-multicast-peers
log-file=stdout
@@ -1,4 +1,4 @@
FROM coturn/coturn:4.9.0-trixie
FROM ghcr.io/mycrl/turn-server:4.0.1
COPY --chmod=0755 entrypoint.sh /usr/local/bin/entrypoint.sh
@@ -1,12 +1,13 @@
#!/bin/sh
set -eu
conf_source="/etc/coturn/turnserver.conf"
conf_rendered="/tmp/turnserver.conf"
conf_template="/etc/turn-server/config.toml.tmpl"
conf_rendered="/tmp/turn-server.toml"
caddy_cert_root="/caddy-certs/caddy/certificates"
: "${TURN_REALM:?TURN_REALM is required}"
: "${TURN_SHARED_SECRET:?TURN_SHARED_SECRET is required}"
: "${EXTERNAL_IP:?EXTERNAL_IP is required}"
discover_tls_files() {
cert_file=""
@@ -47,24 +48,28 @@ wait_for_tls_files() {
return 1
}
escape_toml_string() {
printf '%s' "$1" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g'
}
escape_sed_replacement() {
printf '%s' "$1" | sed -e 's/[&|]/\\&/g'
}
render_value() {
escape_sed_replacement "$(escape_toml_string "$1")"
}
tls_files=$(wait_for_tls_files)
tls_cert_file=$(printf '%s\n' "${tls_files}" | sed -n '1p')
tls_key_file=$(printf '%s\n' "${tls_files}" | sed -n '2p')
cp "${conf_source}" "${conf_rendered}"
sed \
-e "s|__TURN_REALM__|$(render_value "${TURN_REALM}")|g" \
-e "s|__TURN_SHARED_SECRET__|$(render_value "${TURN_SHARED_SECRET}")|g" \
-e "s|__EXTERNAL_IP__|$(render_value "${EXTERNAL_IP}")|g" \
-e "s|__TLS_CERT_FILE__|$(render_value "${tls_cert_file}")|g" \
-e "s|__TLS_KEY_FILE__|$(render_value "${tls_key_file}")|g" \
"${conf_template}" > "${conf_rendered}"
{
echo
echo "realm=${TURN_REALM}"
echo "static-auth-secret=${TURN_SHARED_SECRET}"
echo "cert=${tls_cert_file}"
echo "pkey=${tls_key_file}"
} >> "${conf_rendered}"
set -- turnserver -n -c "${conf_rendered}"
if [ -n "${EXTERNAL_IP:-}" ]; then
set -- "$@" --external-ip "${EXTERNAL_IP}"
fi
exec "$@"
exec turn-server --config="${conf_rendered}"
+1 -1
View File
@@ -48,7 +48,7 @@ resource "google_compute_firewall" "webrtc_rules" {
allow {
protocol = "udp"
ports = ["3478", "5349", "49152-65535"]
ports = ["3478", "49152-65535"]
}
target_tags = ["webrtc-outpost", "caddy-web"]