Add host firewall script for LiveKit ports

This commit is contained in:
2026-08-09 21:06:31 -04:00
parent 6883cedaf7
commit 07508504ac
2 changed files with 174 additions and 5 deletions
+21 -5
View File
@@ -72,12 +72,27 @@ docker compose -f infra/docker-compose.yml up -d
| API | 8080 | | API | 8080 |
| LiveKit | 7880 | | 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`. 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 If you are serving anything other than localhost, set `PUBLIC_API_URL`, `PUBLIC_LIVEKIT_URL` and
`PUBLIC_LIVEKIT_URL` in `infra/.env` to addresses your devices can actually reach, and change `PUBLIC_WEB_URL` in `infra/.env` to addresses your devices can actually reach, add
`turn.domain` in `infra/livekit.yaml` to the same host. LiveKit reads that file literally, so it `PUBLIC_WEB_URL` to `CORS_ORIGINS`, and change `turn.domain` in `infra/livekit.yaml` to the same
does not pick up environment variables. 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 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 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_ROTATION_SECONDS` | How often join codes change, 30 to 60 |
| `PIN_GRACE_SECONDS` | How long an old code keeps working after it changes | | `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 | | `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: Video encoding on the kiosk is tunable per device, without rebuilding:
+153
View File
@@ -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 <cidr> 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 <<MANUAL
No supported firewall front end was found. If you are running nftables or
iptables directly, allow these from ${SUBNET:-any address}:
tcp ${PORTS_TCP[*]}
udp ${PORTS_UDP[*]}
udp ${RANGE_UDP_START}-${RANGE_UDP_END}
MANUAL
}
if command -v ufw >/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