Skip to content

Add MAC Watch module - keep camera IP addresses up to date - #2510

Open
kingwap99 wants to merge 1 commit into
AlexxIT:masterfrom
kingwap99:feature/macwatch
Open

kingwap99 wants to merge 1 commit into
AlexxIT:masterfrom
kingwap99:feature/macwatch

Conversation

@kingwap99

@kingwap99 kingwap99 commented Sep 20, 2026 •

Copy link
Copy Markdown

Problem

Cameras get their IP address from a DHCP server, so it can change after a router
reboot, a long power outage or a hardware swap. Stream links in the config are
pinned to that address:

streams:
  dahua1: rtsp://admin:password@192.168.1.64:554/cam/realmonitor?channel=1

When the camera moves to another address the link stops working and go2rtc
cannot recover, because it only knows the IP address and has no idea which
physical device that address belongs to.

Solution

A new macwatch module. It is opt-in and does nothing unless it is configured.
It remembers the MAC address of every camera, resolves the actual IP address
from the ARP table and rewrites the config file when the address has changed:

macwatch:
  interval: 60          # seconds between checks, 0 = only at startup
  sweep: true           # sweep the local subnet when a MAC is not in the ARP table
  sweep_interval: 300   # minimum seconds between sweeps
  verify: true          # check the new IP on the stream port before writing
  dry_run: false        # report only, never touch the config file
  cameras:
    dahua1: aa:bb:cc:dd:ee:ff

What happens on every check:

  1. Compare the configured IP address against the ARP table (sends nothing).
  2. If the MAC address does not match, sweep the local subnet with UDP probes
    and read the table again.
  3. When the MAC address is found at a new IP, verify that the port from the
    stream URL answers there.
  4. Rewrite the config (host only - credentials, port, path, query and fragment
    are kept) and update the in-memory stream URL, so the running connection
    reconnects to the new address without a restart.

MAC addresses do not have to be collected by hand:

GET /api/macwatch/discover

returns a ready to use config section for every stream that has an IP address
in the source URL. GET /api/macwatch reports the state of every camera and
POST /api/macwatch forces a rescan. There is also a "Rescan IP" button on the config web page: it reports the
cameras that were changed and lists everything that needs attention, including
dry_run mode with the address it would use.

ARP table

Platform Source
Linux /proc/net/arp
macOS, BSD route socket (golang.org/x/net/route)
Windows arp -a

The macOS/BSD path uses a route socket instead of the arp tool, because the
tool needs SystemConfiguration and returns empty output in restricted
environments (services with a minimal environment, sandboxes). No new
dependency: golang.org/x/net is already required.

Implementation

  • internal/macwatch - the new module (ARP lookup, scheduling, config rewrite, API)
  • internal/streams - Stream.ReplaceSource and Producer.ReplaceURL to swap
    the source URL and let the existing reconnect loop pick it up
  • main.go - module registration (can be disabled with app.modules)
  • www/config.html - the Rescan IP button
  • website/api/openapi.yaml - API description
  • internal/macwatch/README.md - documentation (picked up by the website build)

The config rewrite uses the existing app.PatchConfig / pkg/yaml.Patch, so
the file is edited in place: comments, indentation and every other key are
preserved. If a stream name is not found in the config file (a second config
file, streams added dynamically by the Home Assistant integration), the module
never touches it and reports an error for that camera instead.

Testing

  • go test ./internal/macwatch/... - 12 tests: MAC normalization (macOS single
    digit octets, Windows dashes, Cisco dots, plain hex), ARP output parsing for
    macOS/Linux/Windows, host replacement in URLs (including @ inside
    credentials, with ports, queries and fragments), in-place rewrite of all three
    config forms (string, list, {url: ...}), the resolve flow and subnet
    expansion
  • builds for all 12 release targets: darwin amd64/arm64, freebsd amd64/arm64,
    linux 386/amd64/arm/arm64/mipsle, windows 386/amd64/arm64
  • go vet and gofmt clean
  • live on a production instance with 21 cameras: every camera reports ok;
    after deleting the ARP entries of 5 cameras a forced check resolved all of
    them again in about 2 seconds; a stream with a deliberately wrong IP was
    rewritten to the correct address in the config file and in memory, and the
    diff against the previous config contained nothing but the new section
  • running on that instance for a few days now with about 21 cameras. A camera whose
    DHCP lease moved was reported with the exact target address while the module was in
    dry_run mode (192.168.31.111 -> 192.168.31.223), and the config
    now points at the address where the camera actually answers (ONVIF port open, frames
    served - 2528x2528 JPEG, HTTP 200). A camera that dropped off the network was
    reported as not_found and picked up again on its own when it came back,
    without touching the config file in the meantime.

One finding worth sharing: sending all 254 UDP probes of a /24 at once does not
fill the ARP table reliably - the kernel has a limit on unresolved neighbours
and drops the queued packets, which left 5 cameras at (incomplete). Pacing
the sweep (4 hosts, then a 20 ms pause) resolved every device on that network.

Notes

  • The camera must be in the same L2 segment as go2rtc. A MAC address behind a
    router is not visible in the local ARP table, so Docker users need
    network_mode: host.
  • A config file is required (-c go2rtc.yaml). A config passed as a command
    line string cannot be rewritten.
  • Port verification is skipped for schemes with a dynamic port (homekit) or
    without a TCP port at all (xiaomi).
  • A device that answers on two addresses with the same MAC address (for example
    a camera connected over both Wi-Fi bands) is supported: the module keeps the
    address that is already in the config while it is still in the ARP table.
  • Sweeping only happens when a MAC address is missing, and at most once per
    sweep_interval (5 minutes by default).

Cameras get their IP address from a DHCP server, so the address can change
after a router reboot. All links to the camera in the config file are pinned
to the old address and stop working.

The new macwatch module remembers the MAC address of every camera, finds its
actual IP address in the ARP table and rewrites the config file when the
address has changed. The running stream will be reconnected to the new
address without a go2rtc restart.

ARP table is read from /proc/net/arp (Linux), route socket (macOS, BSD) or
the arp tool (Windows). Also added API endpoint and a "Rescan IP" button on
the config web page.
@kingwap99
kingwap99 marked this pull request as ready for review September 22, 2026 20:07
@AlexxIT AlexxIT added the enhancement New feature or request label Oct 1, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants