Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,14 @@ ZMES_PORT=9000

# The address browsers use for ZoneMinder, with a hostname rather than an IP address. Needed for
# live view through go2rtc: set Options > System > GO2RTC_PATH to this plus /zm/go2rtc.
# go2rtc live streams need no login, even with ZoneMinder's authentication on. Anyone who can
# reach the web port can watch a monitor that has go2rtc enabled. Keep the port on a trusted network.
# ZM_PUBLIC_URL=http://zm.lan:8080

# Behind an https reverse proxy, ZoneMinder's own calls to go2rtc come through the proxy. List the
# proxy's address (IP or CIDR, space separated) so go2rtc accepts them. Leave empty otherwise.
# GO2RTC_API_ALLOW=172.18.0.0/16

# Size of /dev/shm in bytes (1 GiB). Raise it for many or high resolution monitors.
ZM_SHM_SIZE=1073741824

Expand Down
21 changes: 18 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,22 @@ to that address plus `/zm/go2rtc`, so `http://zm.lan:8080/zm/go2rtc`, and turn o
monitor. The container serves go2rtc under ZoneMinder's own address, so the browser's WebSocket
passes go2rtc's origin check. `ZM_PUBLIC_URL` also points that hostname at the container itself,
because ZoneMinder calls the same URL from inside it, and a host firewall often blocks a container
from reaching its own published port. With an IP address instead of a name, that part depends on
your firewall.
from reaching its own published port.

go2rtc's API has no login of its own, and ZoneMinder's login doesn't cover it. So from outside
the container, `/zm/go2rtc/` only serves what live view needs: the WebSocket for a monitor's
stream and the HLS fallback. The rest of the API is refused, including go2rtc's config, adding
streams, and its own web page. That means ZoneMinder's own calls must come from inside the container:
- **Hostname in `ZM_PUBLIC_URL`:** this works with no extra setup.
- **IP address:** ZoneMinder's calls arrive through the host, so go2rtc refuses them and live
view stays blank. Use a hostname.
- **https behind a reverse proxy:** set `GO2RTC_API_ALLOW` to the proxy's address.

**go2rtc live streams are not protected by ZoneMinder's login.** Anyone who can reach the web port
can watch any monitor with go2rtc enabled, because the browser connects without credentials and
the IDs are small numbers. Keep the port on a network you trust, or put an authenticating
reverse proxy or a VPN in front of it. Monitors without go2rtc use ZoneMinder's own streaming,
which does check the login.

`docker compose ps` shows the container's health. It reports unhealthy when Apache stops serving
`/zm/`, when the `zmdc.pl` daemon is gone, or when the database doesn't answer.
Expand Down Expand Up @@ -142,6 +156,7 @@ ZoneMinder container environment:
| `ZM_DB_SSL_CA_CERT`, `ZM_DB_SSL_CLIENT_CERT`, `ZM_DB_SSL_CLIENT_KEY` | | TLS to the database |
| `ZM_SERVER_HOST` | | Server name for multi-server installs |
| `ZM_PUBLIC_URL` | | The address browsers use, e.g. `http://zm.lan:8080`. Needed for go2rtc live view, see above. |
| `GO2RTC_API_ALLOW` | | IP addresses or CIDR ranges, space separated, that get go2rtc's whole API through `/zm/go2rtc/`. Set it to an https reverse proxy's address. |
| `TZ` | `UTC` | Applied to the OS and PHP. Give the database the same value, as the compose file does. |
| `ZM_ES_ENABLE` | `1` in `-es` images | Writes OPT_USE_EVENTNOTIFICATION to `conf.d`, which takes priority over the UI setting |
| `ZM_DB_WAIT_TIMEOUT` | `180` | Seconds to wait for the database |
Expand Down Expand Up @@ -202,7 +217,7 @@ Volumes:
| `/config` | `go2rtc.yaml`, event server config and certificates |
| `/dev/shm` | tmpfs, 1 GiB by default (`ZM_SHM_SIZE`). ZoneMinder keeps each monitor's frame buffer here. |

Ports: 80 (web, with go2rtc's API and page under `/zm/go2rtc/`), 8555 tcp/udp (WebRTC),
Ports: 80 (web, with go2rtc's live view under `/zm/go2rtc/`), 8555 tcp/udp (WebRTC),
9000 (event server).

### Hardware decoding
Expand Down
1 change: 1 addition & 0 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ services:
PGID: ${PGID:-}
# The address browsers use, e.g. http://zm.lan:8080. Needed for go2rtc live view (README).
ZM_PUBLIC_URL: ${ZM_PUBLIC_URL:-}
GO2RTC_API_ALLOW: ${GO2RTC_API_ALLOW:-}
# Outgoing mail. Leave SMTP_HOST empty to skip.
SMTP_HOST: ${SMTP_HOST:-}
SMTP_PORT: ${SMTP_PORT:-}
Expand Down
2 changes: 1 addition & 1 deletion dockerhub/zoneminder.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ To run the event server, set `ZM_TAG=latest-es` in `.env`. For object detection,
| `ZM_DB_NAME` / `ZM_DB_USER` | `zm` / `zmuser` | |
| `ZM_DB_PASS` / `ZM_DB_PASS_FILE` | required | Use the `_FILE` form with Docker secrets |
| `TZ` | `UTC` | Give the database the same value |
| `ZM_PUBLIC_URL` | | Address browsers use, e.g. `http://zm.lan:8080`. Set GO2RTC_PATH to it plus `/zm/go2rtc` for go2rtc live view. |
| `ZM_PUBLIC_URL` | | Address browsers use, e.g. `http://zm.lan:8080`. Set GO2RTC_PATH to it plus `/zm/go2rtc` for go2rtc live view. go2rtc streams need no login, so keep the port on a trusted network. |
| `PUID` / `PGID` | `33` / `33` | Match the owner of your bind mounts |
| `SMTP_HOST`, `SMTP_USER`, `SMTP_PASS` | | Outgoing mail through msmtp |

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,33 @@ RedirectMatch ^/$ /zm/
ServerTokens Prod
ServerSignature Off

# go2rtc (API, WebSocket and its own web page) at /zm/go2rtc/, on the same address as ZoneMinder.
# Browsers then make same-origin requests, which go2rtc's WebSocket origin check accepts, and
# go2rtc's port stays inside the container. Set Options > System > GO2RTC_PATH to
# <ZM_PUBLIC_URL>/zm/go2rtc.
# go2rtc at /zm/go2rtc/, on the same address as ZoneMinder. Browsers then make same-origin
# requests, which go2rtc's WebSocket origin check accepts, and go2rtc's port stays inside the
# container. Set Options > System > GO2RTC_PATH to <ZM_PUBLIC_URL>/zm/go2rtc.
ProxyPreserveHost On
ProxyPass /zm/go2rtc/ http://127.0.0.1:1984/ upgrade=websocket
ProxyPassReverse /zm/go2rtc/ http://127.0.0.1:1984/

# go2rtc's API has no authentication, and ZoneMinder's login doesn't cover it. Anyone who could
# reach it could rewrite go2rtc's config or point it at any source. So only processes inside the
# container get the whole API: zmc registers streams through this URL, reaching it over loopback
# via the /etc/hosts entry that init-config writes for ZM_PUBLIC_URL. init-config writes
# zm-go2rtc-allow.conf from GO2RTC_API_ALLOW, for a reverse proxy that zmc's calls come through.
<Location /zm/go2rtc/>
<RequireAny>
Require ip 127.0.0.1 ::1
Include /etc/apache2/zm-go2rtc-allow.conf
</RequireAny>
</Location>
# Browsers get the live view WebSocket, only for ZoneMinder's stream names (monitor ID plus an
# optional channel suffix), and the HLS fallback that a WebSocket session hands out.
<LocationMatch "^/zm/go2rtc/api/ws$">
<RequireAny>
Require ip 127.0.0.1 ::1
Include /etc/apache2/zm-go2rtc-allow.conf
Require expr "%{QUERY_STRING} =~ /^src=[0-9]+(_(CameraDirectPrimary|CameraDirectSecondary|ZoneMinderPrimary))?$/"
</RequireAny>
</LocationMatch>
<LocationMatch "^/zm/go2rtc/api/hls/">
Require all granted
</LocationMatch>
1 change: 1 addition & 0 deletions release/zoneminder/rootfs/etc/apache2/zm-go2rtc-allow.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Written by init-config from GO2RTC_API_ALLOW
18 changes: 17 additions & 1 deletion release/zoneminder/rootfs/etc/s6-overlay/scripts/init-config
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,8 @@ if [[ ! -f /config/go2rtc.yaml ]]; then
cat > /config/go2rtc.yaml <<'YAML'
# go2rtc config. ZoneMinder adds camera streams through the API at runtime.
# Browsers reach the API through Apache at /zm/go2rtc/, so it only listens inside the container.
# Apache only lets outside clients open live view streams. Those streams need no login, even
# with ZoneMinder's authentication on, so keep ZoneMinder's port on a trusted network.
# Docs: https://github.com/AlexxIT/go2rtc#configuration
api:
listen: "127.0.0.1:1984"
Expand All @@ -131,8 +133,10 @@ if [[ -n "${ZM_PUBLIC_URL:-}" ]]; then
pub_scheme=${BASH_REMATCH[1]} pub_host=${BASH_REMATCH[2]} pub_port=${BASH_REMATCH[4]:-}
if [[ "$pub_scheme" == https ]]; then
log "ZM_PUBLIC_URL is https, so a proxy in front of the container answers it. ZoneMinder's daemons must be able to reach $pub_host."
[[ -n "${GO2RTC_API_ALLOW:-}" ]] \
|| log "WARNING: set GO2RTC_API_ALLOW to the proxy's address, or go2rtc refuses zmc's stream registrations and live view stays blank."
elif [[ "$pub_host" =~ ^[0-9.]+$ ]]; then
log "WARNING: ZM_PUBLIC_URL uses an IP address. ZoneMinder's daemons then reach it through the host, which a host firewall can block. A hostname avoids that."
log "WARNING: ZM_PUBLIC_URL uses an IP address. ZoneMinder's daemons then reach it through the host, not over loopback, so go2rtc refuses their stream registrations and live view stays blank. Use a hostname."
else
grep -qE "^127\.0\.0\.1[[:space:]]+${pub_host//./\\.}\$" /etc/hosts || echo "127.0.0.1 $pub_host" >> /etc/hosts
if [[ -n "$pub_port" && "$pub_port" != 80 ]]; then
Expand All @@ -142,6 +146,18 @@ if [[ -n "${ZM_PUBLIC_URL:-}" ]]; then
fi
fi

# Addresses besides the container itself that get go2rtc's whole API through /zm/go2rtc/.
# zm-docker.conf includes this file in its access rules.
allow_conf=/etc/apache2/zm-go2rtc-allow.conf
echo "# Written by init-config from GO2RTC_API_ALLOW" > "$allow_conf"
if [[ -n "${GO2RTC_API_ALLOW:-}" ]]; then
for a in $GO2RTC_API_ALLOW; do
[[ "$a" =~ ^[0-9A-Fa-f.:]+(/[0-9]+)?$ ]] || die "GO2RTC_API_ALLOW takes IP addresses or CIDR ranges, not '$a'"
done
echo "Require ip $GO2RTC_API_ALLOW" >> "$allow_conf"
log "go2rtc API also open to: $GO2RTC_API_ALLOW"
fi

shm_kb=$(df -k --output=size /dev/shm | tail -1 | tr -d ' ')
if (( shm_kb < 262144 )); then
log "WARNING: /dev/shm is only $((shm_kb / 1024)) MB. ZoneMinder needs roughly width*height*4*buffer_frames per monitor."
Expand Down
16 changes: 16 additions & 0 deletions tests/smoke.sh
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,22 @@ zm 'url=$ZM_PUBLIC_URL/zm/go2rtc/api
-H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" "$url/ws?src=test" || true)
[[ "$code" == 101 ]] || { echo "go2rtc WebSocket answered $code"; exit 1; }' \
|| fail "go2rtc live view through /zm/go2rtc/"
# From outside the container, go2rtc's unauthenticated API is closed except the live view
# WebSocket for ZoneMinder's stream names.
outside() { # outside <expected code> <description> <curl args...>
local want=$1 what=$2 code; shift 2
code=$(curl -s -o /dev/null -w "%{http_code}" --max-time 5 "$@" || true)
[[ "$code" == "$want" ]] || fail "outside: $what answered $code, expected $want"
}
g2=http://127.0.0.1:${ZM_HTTP_PORT:-18080}/zm/go2rtc
wsh=(-H "Connection: Upgrade" -H "Upgrade: websocket" -H "Sec-WebSocket-Version: 13"
-H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" -H "Origin: http://127.0.0.1:${ZM_HTTP_PORT:-18080}")
outside 403 "go2rtc config" "$g2/api/config"
outside 403 "go2rtc stream list" "$g2/api/streams"
outside 403 "adding a go2rtc stream" -X PUT "$g2/api/streams?name=9&src=rtsp://example"
outside 403 "go2rtc web page" "$g2/"
outside 403 "WebSocket with a URL as source" "${wsh[@]}" "$g2/api/ws?src=rtsp://example"
outside 101 "WebSocket for a monitor stream" "${wsh[@]}" "$g2/api/ws?src=1_CameraDirectPrimary"
# The usage statistics prompt blocks image URLs until answered. Test shortcut for clicking "decline".
zm 'source /etc/s6-overlay/scripts/common.sh; sql "UPDATE Config SET Value=0 WHERE Name=\"ZM_SHOW_PRIVACY\""'
api monitors.json -X POST \
Expand Down
Loading