diff --git a/README.md b/README.md index 63a05bd..3c92190 100644 --- a/README.md +++ b/README.md @@ -72,12 +72,27 @@ docker compose -f infra/docker-compose.yml up -d | API | 8080 | | LiveKit | 7880 | +If the host runs a firewall, open the LiveKit ports: + +``` +sudo ./infra/firewall.sh +``` + +It allows 7880 and 7881 TCP, 3478 UDP and 50000-50100 UDP from your LAN, handling ufw and +firewalld. Pass `--dry-run` to see the commands first, `--subnet` to name a different network or +`--open-to-all` to drop the source restriction. The website and API need no rules, since Docker +publishes those ports with its own firewall rules. LiveKit runs on the host network and does not +get that treatment, so a firewalled host will load the site and then fail to connect to a room. + Sign in at `/admin` with the bootstrap admin details from your `.env`. -If you are serving anything other than localhost, set `PUBLIC_API_URL` and -`PUBLIC_LIVEKIT_URL` in `infra/.env` to addresses your devices can actually reach, and change -`turn.domain` in `infra/livekit.yaml` to the same host. LiveKit reads that file literally, so it -does not pick up environment variables. +If you are serving anything other than localhost, set `PUBLIC_API_URL`, `PUBLIC_LIVEKIT_URL` and +`PUBLIC_WEB_URL` in `infra/.env` to addresses your devices can actually reach, add +`PUBLIC_WEB_URL` to `CORS_ORIGINS`, and change `turn.domain` in `infra/livekit.yaml` to the same +host. LiveKit reads that file literally, so it does not pick up environment variables. + +Leaving `CORS_ORIGINS` on its localhost default while browsing by LAN address is the usual cause +of requests failing with a missing `Access-Control-Allow-Origin` header. The LiveKit container uses host networking, because WebRTC needs to advertise an address your devices can reach and a container on a bridge network has none. That is Linux only; on macOS or @@ -128,7 +143,8 @@ Set in `infra/.env`: | `PIN_ROTATION_SECONDS` | How often join codes change, 30 to 60 | | `PIN_GRACE_SECONDS` | How long an old code keeps working after it changes | | `PUBLIC_API_URL` / `PUBLIC_LIVEKIT_URL` | Addresses browsers and kiosks use to reach you | -| `CORS_ORIGINS` | Browser origins allowed to call the API. Kiosks are exempt, since a desktop app has no fixed web origin | +| `PUBLIC_WEB_URL` | The address the website is served on. Must match what you type in the browser, scheme included, or server rendered pages disagree with the browser | +| `CORS_ORIGINS` | Browser origins allowed to call the API, comma separated. Must include `PUBLIC_WEB_URL`. Kiosks are exempt, since a desktop app has no fixed web origin | Video encoding on the kiosk is tunable per device, without rebuilding: diff --git a/infra/firewall.sh b/infra/firewall.sh new file mode 100755 index 0000000..dd09abf --- /dev/null +++ b/infra/firewall.sh @@ -0,0 +1,153 @@ +#!/usr/bin/env bash +set -euo pipefail + +SUBNET="" +OPEN_TO_ALL="no" +DRY_RUN="no" + +log() { printf '\033[1;34m==>\033[0m %s\n' "$*"; } +warn() { printf '\033[1;33m==>\033[0m %s\n' "$*" >&2; } +fail() { printf '\033[1;31m==>\033[0m %s\n' "$*" >&2; exit 1; } + +usage() { + cat <<'USAGE' +PiStation host firewall setup + + sudo ./infra/firewall.sh [options] + +Opens the ports LiveKit needs. The web and API containers publish their ports +through Docker, which writes its own rules and is not affected by the host +firewall. LiveKit runs on the host network, so it is. + +Options + --subnet Allow only this network, defaults to the LAN behind your default route + --open-to-all Allow from any address instead of a single network + --dry-run Print the commands without running them + --help Show this message +USAGE +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --subnet) SUBNET="${2:-}"; shift 2 ;; + --subnet=*) SUBNET="${1#*=}"; shift ;; + --open-to-all) OPEN_TO_ALL="yes"; shift ;; + --dry-run) DRY_RUN="yes"; shift ;; + --help|-h) usage; exit 0 ;; + *) fail "unknown option: $1" ;; + esac +done + +PORTS_TCP=(7880 7881) +PORTS_UDP=(3478) +RANGE_UDP_START=50000 +RANGE_UDP_END=50100 + +detect_subnet() { + local interface + interface="$(ip route show default 2>/dev/null | awk '/default/ {print $5; exit}')" + [[ -n "$interface" ]] || return 1 + ip -o -f inet route show scope link dev "$interface" 2>/dev/null | awk '{print $1; exit}' +} + +run() { + if [[ "$DRY_RUN" == "yes" ]]; then + printf ' %s\n' "$*" + else + "$@" >/dev/null + fi +} + +if [[ "$OPEN_TO_ALL" == "no" && -z "$SUBNET" ]]; then + SUBNET="$(detect_subnet || true)" + [[ -n "$SUBNET" ]] || fail "could not work out your LAN subnet, pass --subnet or --open-to-all" +fi + +if [[ "$DRY_RUN" == "no" && $EUID -ne 0 ]]; then + fail "run this with sudo, or pass --dry-run to see what it would do" +fi + +apply_ufw() { + if [[ "$OPEN_TO_ALL" == "yes" ]]; then + for port in "${PORTS_TCP[@]}"; do + run ufw allow "${port}/tcp" + done + for port in "${PORTS_UDP[@]}"; do + run ufw allow "${port}/udp" + done + run ufw allow "${RANGE_UDP_START}:${RANGE_UDP_END}/udp" + return + fi + + for port in "${PORTS_TCP[@]}"; do + run ufw allow from "$SUBNET" to any port "$port" proto tcp + done + for port in "${PORTS_UDP[@]}"; do + run ufw allow from "$SUBNET" to any port "$port" proto udp + done + run ufw allow from "$SUBNET" to any port "${RANGE_UDP_START}:${RANGE_UDP_END}" proto udp +} + +apply_firewalld() { + if [[ "$OPEN_TO_ALL" == "yes" ]]; then + for port in "${PORTS_TCP[@]}"; do + run firewall-cmd --permanent "--add-port=${port}/tcp" + done + for port in "${PORTS_UDP[@]}"; do + run firewall-cmd --permanent "--add-port=${port}/udp" + done + run firewall-cmd --permanent "--add-port=${RANGE_UDP_START}-${RANGE_UDP_END}/udp" + else + for port in "${PORTS_TCP[@]}"; do + run firewall-cmd --permanent "--add-rich-rule=rule family=ipv4 source address=${SUBNET} port port=${port} protocol=tcp accept" + done + for port in "${PORTS_UDP[@]}"; do + run firewall-cmd --permanent "--add-rich-rule=rule family=ipv4 source address=${SUBNET} port port=${port} protocol=udp accept" + done + run firewall-cmd --permanent "--add-rich-rule=rule family=ipv4 source address=${SUBNET} port port=${RANGE_UDP_START}-${RANGE_UDP_END} protocol=udp accept" + fi + run firewall-cmd --reload +} + +print_manual() { + cat </dev/null 2>&1; then + BACKEND="ufw" +elif command -v firewall-cmd >/dev/null 2>&1; then + BACKEND="firewalld" +else + BACKEND="none" +fi + +if [[ "$BACKEND" == "ufw" && $EUID -eq 0 ]] && ! ufw status 2>/dev/null | grep -q "^Status: active"; then + warn "ufw is installed but not active, the rules will apply once you run: ufw enable" +fi + +if [[ "$OPEN_TO_ALL" == "yes" ]]; then + log "opening LiveKit ports to any address" +else + log "opening LiveKit ports to $SUBNET" +fi + +case "$BACKEND" in + ufw) apply_ufw ;; + firewalld) apply_firewalld ;; + none) print_manual; exit 0 ;; +esac + +log "done via $BACKEND" +echo +echo " tcp 7880 LiveKit signalling, the websocket browsers connect to" +echo " tcp 7881 LiveKit media over TCP, used where UDP is blocked" +echo " udp 3478 TURN relay" +echo " udp 50000-50100 LiveKit media, the normal path for audio and video" +echo