Repository navigation
fix: validate alarm packets in zmalarm-server.py - #5211
Merged
connortechnology merged 3 commits intoOct 12, 2026
Merged
Conversation
The alarm server accepted any packet from any host and mapped it to a monitor by the Address field inside the packet, so a host on the network could raise an alarm or a zmtrigger event on any configured camera. Every malformed packet (short header, non-JSON body, unknown address) raised an uncaught exception that ended the process, and a client that connected without sending blocked the accept loop indefinitely. - only accept an alarm whose Address matches the address it came from and a monitor that uses that address - read the header and body with a 5 s timeout, cap the body at 64 KiB, log and drop bad packets instead of exiting - resolve camera hostnames in monitor Paths and skip monitors without a camera address instead of failing at startup - pass the alarm description to the zmtrigger thread instead of reading a global, and talk to zmtrigger over a plain socket (telnetlib was removed in Python 3.13) - honour the bare port number zmpkg.pl passes from ZM_OPT_ALS_PORT and add --bind= to choose the listening address (default unchanged) - move the pyzm imports into main() so the parser can be tested; add tests/python/test_zmalarm_server.py Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The source check required the packet's Address field to equal the address the packet came from. A camera behind NAT reports its LAN address there while its packets arrive from the NAT address, so its alarms were dropped. The Address field is no credential, so the monitor is now chosen by the sender: an alarm is accepted only from an address that belongs to a monitor, the camera address in its Path or one listed for it under [alarmserver] in secrets.ini, and it goes to one of that address's monitors. The Address field only picks among several monitors on one address, for cameras behind a shared NAT address. A host that belongs to no monitor still cannot raise an alarm, and a camera cannot raise one on another camera's monitor. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An alarm packet armed the monitor, slept 60 s in a thread and disarmed it. When zmdc stopped or restarted the server inside that window (SIGTERM, which Python's default handler ends at once) the disarm never ran and the monitor stayed in forced alarm, recording, until someone cancelled it by hand. Keep the forced alarms the server raised in one place, one per monitor: a new alarm for a monitor in forced alarm arms it again, as every packet did, and moves its end instead of starting a second timer that would cut it short. On the way out, SIGTERM included, the server cancels every alarm still running and exits 0, so zmdc no longer logs the stop as abnormal. An arm that fails is logged instead of ending the server. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
TL;DR
scripts/zmalarm-server.pytrusts theAddressfield inside the alarm packet to pick the monitor, so any host that can reach the port can force a 60 s alarm on any camera. It also exits on any malformed packet, stalls on an idle client, ignores the portzmpkg.plpasses, and leaves a forced alarm on if zmdc stops it during the 60 s window.[alarmserver]insecrets.ini), so cameras behind NAT keep working and other hosts are ignored. Reads time out and are size-capped, bad packets are dropped, and forced alarms are tracked per monitor and cancelled on SIGTERM. 3 commits, 2 files, +564/-122, with a Python unit test (25 cases).ZM_OPT_USE_ALARMSERVERis off by default, so only hand-installed copies change. Nothing in the web UI or the API clients (pyzmNg, zm-py/ha-zoneminder, zmNinjaNg, zmeventnotificationNg) reads or starts it; its only caller iszmpkg.pl. Behaviour changes are in the table below.Details: reproduction, behaviour changes, checks
Before
Upstream master, script installed by hand,
ZM_OPT_USE_ALARMSERVER=1:zmc_m3 Gone into alarm state ... Cause:Forced Web, an event on monitor 3, 60 s.0.0.0.0:15002regardless ofZM_OPT_ALS_PORT.Can't exec: No such file or directoryand restarts it in a loop. Not changed here; installing or removing the script is a separate question.Behaviour changes
[alarmserver]insecrets.ini)--bind=)forged: 0 eventsAddressonly chooses among several monitors on one addressAddressother than the one they send from (NAT)Address= sender (drops every camera behind NAT); an opt-out flag (reopens forged alarms, and zmpkg can't pass it)[alarmserver]for what it can't decidenat: 1 eventothermonitor_<id> = <LAN address>lines, or their alarms go to the last monitor of that addressconfig--alarm=yusersstop: alarm 2 → 0 within 5 s, event closed--alarm=yusersextendexited normally, wasexited, signal 14)stopZM_OPT_ALS_PORThonoured;--bind=Release note (for anyone who installed the script by hand): "zmalarm-server.py now accepts an alarm only from an address that belongs to a monitor: the camera address in its Path, or an address listed for it in
/etc/zm/secrets.iniunder[alarmserver]asmonitor_<id> = <address> .... Cameras behind NAT work when the monitor's Path holds the NAT address; several cameras behind one NAT address need their LAN addresses listed. Stopping the server now ends the forced alarms it started.ZM_OPT_ALS_PORTis now honoured: if you changed it, point the cameras at that port."Checks
tests/python/test_zmalarm_server.py(plainpython3)zmdc.plas zmpkg doeszmu -s/-xchecks (not in this PR)Not run: Python 3.13 (the script needs
telnetliband the pyzm 0.x API, so 3.12 or older; unchanged here). The same three commits apply cleanly to release-1.38, where the file is identical.🤖 Generated with Claude Code