The guest network blocks private ranges twice: a zone default on the gateway, and the guest hotspot's Post-Authorization Restrictions, which the access points enforce on their own. With only a firewall allow, a phone on Waters Guest worked for about two minutes and then timed out. docs/examples/waters-church-na/ gains the three scripts used to fix it: zone-allow-policy.py (allow Hotspot -> 10.1.1.4 on 80/443), guest-restrictions.py (drop 10.0.0.0/8 from the restriction list via the legacy guest_access setting, since system policies cannot be edited through the Integration API) and wifi-broadcast-devices.py (put the Waters Guest SSID on the Auditorium, Main Lobby and Offices APs). The runbook gets a section on all three, and PUBLIC-ACCESS.md points at it from the troubleshooting notes. Confirmed on site: translate.watersmedia.org loads on Waters Guest and stays up. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
168 lines
6.5 KiB
Markdown
168 lines
6.5 KiB
Markdown
# Letting people off-site watch
|
|
|
|
**Optional.** Everything works on the building's WiFi without any of this. Read
|
|
on only if you want people on cell data, or watching from home, to follow along.
|
|
|
|
Because each campus runs its own copy, this is a per-campus decision — you can
|
|
publish one campus and not another.
|
|
|
|
---
|
|
|
|
## The easy way: Cloudflare Tunnel
|
|
|
|
Free, needs no port forwarding, no firewall changes, no certificate, and no
|
|
static IP. It runs on the same Mac as the app.
|
|
|
|
1. **Install cloudflared**
|
|
|
|
```bash
|
|
brew install cloudflared
|
|
```
|
|
|
|
No Homebrew on that Mac? Download the `cloudflared-darwin-arm64.tgz` binary
|
|
from Cloudflare's releases page and drop it in `/usr/local/bin`.
|
|
|
|
2. **Log in and create the tunnel**
|
|
|
|
```bash
|
|
cloudflared tunnel login
|
|
cloudflared tunnel create translate-main
|
|
```
|
|
|
|
3. **Point a hostname at the app**
|
|
|
|
```bash
|
|
cloudflared tunnel route dns translate-main translate.watersmedia.org
|
|
```
|
|
|
|
4. **Run it**
|
|
|
|
```bash
|
|
cloudflared tunnel run --url http://127.0.0.1:3100 translate-main
|
|
```
|
|
|
|
To keep it running after a reboot, `sudo cloudflared service install`.
|
|
|
|
5. **Tell the app its public name**
|
|
|
|
Operator page → **Public address** → `https://translate.watersmedia.org` →
|
|
Save. That is what the QR code encodes, so it must be the public name or the
|
|
code only works for people already on your WiFi.
|
|
|
|
WebSockets pass through Cloudflare tunnels with nothing extra to configure,
|
|
which is the main reason this is the lower-risk option.
|
|
|
|
For a second campus, repeat with its own name — `translate-north`,
|
|
`north.translate.watersmedia.org`.
|
|
|
|
---
|
|
|
|
## The other way: a name on your existing reverse proxy
|
|
|
|
You already run a reverse proxy on the NAS at `10.1.1.4` (Boba-Fett, Synology
|
|
DSM) fronting `check.watersmedia.org`. It can front this too, though it means
|
|
the traffic goes through the NAS network to reach the booth Mac.
|
|
|
|
This is what North Attleboro runs as of 2026-10-15, in the **LAN-only** form:
|
|
`http://translate.watersmedia.org` works on the building's WiFi at both the
|
|
Apollo Beach and North Attleboro sites and nowhere else. Nothing was opened to
|
|
the WAN. The pieces, with the exact scripts and a step-by-step runbook under
|
|
[`examples/waters-church-na/`](examples/waters-church-na/README.md):
|
|
|
|
| Piece | Where | What |
|
|
| --- | --- | --- |
|
|
| Local DNS | UniFi consoles at AB and NA (Settings → Policy Table → DNS) | A record `translate.watersmedia.org` → `10.1.1.4`, TTL 300, created with `unifi-dns-record.py` and a per-console Network API key |
|
|
| Proxy rule | DSM → Control Panel → Login Portal → Advanced → Reverse Proxy | HTTP `translate.watersmedia.org:80` → HTTP `10.1.7.39:3100`, WebSocket headers, `X-Forwarded-Host: $http_host`, read timeout 3600 s |
|
|
| HTTPS | `/usr/local/etc/nginx/sites-enabled/translate-redirect.conf` on the NAS | `https://` answers a 301 to `http://`; the name has no certificate, so browsers warn before the redirect |
|
|
|
|
Both NAS pieces come from `boba-fett-translate-proxy.sh`, run as
|
|
`ssh -t boba-fett sudo bash translate-proxy.sh` because DSM's reverse-proxy API
|
|
is root-only. Going through DSM rather than a hand-edited nginx file means the
|
|
rule shows up in the DSM UI and survives DSM updates. DSM adds `Host`,
|
|
`X-Real-IP`, `X-Forwarded-For` and `X-Forwarded-Proto` on its own.
|
|
|
|
Then: Operator page → **Public address** → `http://translate.watersmedia.org`
|
|
(no trailing slash), so the QR code and poster use the name instead of the
|
|
IP and port.
|
|
|
|
To let people off-site use the same name later: add a public DNS record for
|
|
`translate.watersmedia.org` pointing at the NA WAN address, get a certificate
|
|
in DSM (Control Panel → Security → Certificate → Let's Encrypt), replace the
|
|
redirect with an HTTPS rule to the same destination, and switch Public address
|
|
to `https://`. The app behaves the same either way, including the `/operator`
|
|
404 through a proxy.
|
|
|
|
### If the proxy is plain nginx instead of DSM
|
|
|
|
DSM writes most of this for you. On a hand-managed nginx the server block
|
|
needs all of it:
|
|
|
|
```nginx
|
|
server {
|
|
listen 443 ssl http2;
|
|
server_name translate.watersmedia.org;
|
|
|
|
ssl_certificate /path/to/fullchain.pem;
|
|
ssl_certificate_key /path/to/privkey.pem;
|
|
|
|
location / {
|
|
proxy_pass http://10.1.7.39:3100; # the booth Mac
|
|
|
|
proxy_set_header Host $host;
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
proxy_set_header X-Forwarded-Proto $scheme;
|
|
proxy_set_header X-Forwarded-Host $host;
|
|
|
|
# ---- required: captions ride a WebSocket ----
|
|
proxy_http_version 1.1;
|
|
proxy_set_header Upgrade $http_upgrade;
|
|
proxy_set_header Connection "upgrade";
|
|
|
|
# A sermon is long and mostly silent between sentences. Without this the
|
|
# proxy closes the caption socket after 60s of quiet and every phone in
|
|
# the room reconnects at once.
|
|
proxy_read_timeout 3600s;
|
|
proxy_send_timeout 3600s;
|
|
|
|
# Live audio: do not hold HLS segments back.
|
|
proxy_buffering off;
|
|
}
|
|
}
|
|
```
|
|
|
|
Either way the booth Mac needs a fixed address or a DHCP reservation, or the
|
|
proxy breaks the next time DHCP moves it.
|
|
|
|
---
|
|
|
|
## Checking it worked
|
|
|
|
```bash
|
|
curl -s http://translate.watersmedia.org/healthz # https:// once published
|
|
```
|
|
|
|
Should return `{"ok":true,…}`, the same as `curl -s http://10.1.7.39:3100/healthz`.
|
|
A proxy that passes the WebSocket answers `HTTP/1.1 101 Switching Protocols` to:
|
|
|
|
```bash
|
|
curl -si -N -H 'Connection: Upgrade' -H 'Upgrade: websocket' \
|
|
-H 'Sec-WebSocket-Version: 13' -H 'Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==' \
|
|
http://translate.watersmedia.org/ws | head -1
|
|
```
|
|
|
|
Then open the page on a phone: for the LAN-only setup on the guest WiFi, for a
|
|
published one **with WiFi turned off**. That is the real test. If the page loads
|
|
but captions never appear, the WebSocket headers are missing. `/operator`
|
|
returning 404 through the proxy is by design; staff use
|
|
`http://10.1.7.39:3100/operator` on the LAN.
|
|
|
|
**Guest WiFi on UniFi.** Guest networks block every private range by default,
|
|
so the proxy is unreachable from them until two things change: a firewall
|
|
policy that allows the guest zone to reach the proxy on 80/443, and the
|
|
proxy's range removed from the guest hotspot's *Post-Authorization
|
|
Restrictions*. The second one is easy to miss: the access points enforce that
|
|
list themselves, so with only the firewall policy a phone works for a minute or
|
|
two and then times out. North Attleboro's fix, with scripts, is in
|
|
[`examples/waters-church-na/`](examples/waters-church-na/README.md) section 3.
|