opnsense / opnsense/plugins

os-ddclient: native backend skips update after CARP failback when the node gets its previous IP back

Open
#5,718 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
PHP
Stars
1.2k
Forks
863
Avg merge
2d 6h
Merged PRs (30d)
10

Description

Important notices
Before you add a new report, we ask you kindly to acknowledge the following:

Describe the bug

On a CARP HA pair where only the MASTER has the WAN address (PPPoE with "Disconnect dialup interfaces" enabled), the native backend can leave the DNS record pointing at the other node's old address and never correct it. No known working version: the native backend has compared only against its local status file since it was added (ef91a6b4f9).

The native backend decides whether to update by comparing the current address with the address this node last published, stored in /var/tmp/ddclient_opn.status (lib/account/__init__.py#L130-L134). That file is per node and is not part of the HA sync, which only copies the OPNsense.DynDNS settings. Each node therefore only knows what it published itself, not what the record currently contains.

Sequence:

  1. Node 1 is MASTER with WAN address A and publishes A. Its status file holds A.
  2. Failover: node 2 becomes MASTER, gets address B and publishes B. The record now holds B.
  3. Failback: node 1 becomes MASTER again and gets A back from the ISP. Its status file still holds A, so the account is "not modified" and nothing is sent. The record stays on B until node 1's address changes.

If node 1 gets a new address in step 3, the update happens normally, so the problem only appears when a node gets back the address it last published.

Restarting the service does not help, because the stale state is reloaded from the status file at startup (lib/poller.py#L121). What recovers it is deleting the status file and restarting the service on the MASTER. configctl ddclient force does both and corrected the record here, but it reports Error (1) with the native backend. The status file removal and the restart succeed; the exit status comes from the last step, /usr/local/sbin/ddclient -force, which runs the ddclient (Perl) client against a ddclient.conf that has no hosts with the native backend and exits 1 without output.

To Reproduce

  1. Set up a CARP HA pair with a dynamic WAN address that exists only on the MASTER (PPPoE with "Disconnect dialup interfaces" in High Availability settings), and os-ddclient with the native backend and an account bound to that WAN, synced to both nodes.
  2. Let node 1 publish its address A.
  3. Put node 1 into persistent CARP maintenance mode. Node 2 takes over, gets address B and publishes it.
  4. Leave maintenance mode on node 1. It becomes MASTER again and gets A back.
  5. /var/tmp/ddclient_opn.status on node 1 contains A, but the DNS record still resolves to B and no update is sent.

Expected behavior

After a CARP takeover, the node that becomes MASTER publishes its address even if it matches what it published before its last failover.

Suggested fix: ship a hook in /usr/local/etc/rc.syshook.d/carp/ with the plugin that clears the status file and restarts the service on a MASTER transition. It changes nothing for single-node setups, and costs at most one update per takeover. It is the direction suggested in #702 for the old plugin. A local hook doing rm -f /var/tmp/ddclient_opn.status; /usr/local/etc/rc.d/ddclient_opn restart works as a workaround here.

Related improvements, which would also cover #5488 and #2862:

  • A maximum interval between updates, as both predecessors had: ddclient's max-interval (default 25 days, can only be lowered) and os-dyndns' _dnsMaxCacheAgeDays = 25. Its purpose was keeping accounts alive: os-dyndns notes that "some providers (e.g. No-IP free accounts) need to have at least 1 address change every month" and even pushed a dummy address for noip-free. The native backend supports noip but has no equivalent, so a host whose address never changes is never updated. The same interval also repairs a stale record eventually. The status file already stores mtime, so this is one more condition in BaseAccount.execute() (time.time() - self.mtime > max_interval), defaulting to 25 days.
  • A "Force update" button in the GUI/API calling the existing force action. #5488 asks for exactly this, and it is what #2862 originally asked for. The action already exists for cron but is not exposed in the UI.
  • Skip /usr/local/sbin/ddclient -force in the force action when the native backend is selected, so it does not report Error (1) after succeeding.

Not proposed: checking the live DNS record on every poll. It would catch any mismatch, but a false mismatch means an update on every poll. Proxied Cloudflare records, resolver caching and providers whose hostnames are not FQDNs (DuckDNS) would all produce one, which is the repeated-update behaviour the status file exists to prevent (#3344, #3631).

Screenshots

n/a

Relevant log files

set new ip lines from /var/log/ddclient/ on both nodes plus the CARP transitions, one Cloudflare account, one A record (addresses replaced by placeholders):

Time Event Node 1 Node 2
01:04:35 failover to node 2 set new ip W
01:16:06 failback to node 1, new address set new ip X
01:19:37 node 1 reconnects, new address set new ip A
01:22:09 failover to node 2, new address "no global IP address detected" set new ip B
01:25:07 failback to node 1, gets A again nothing sent
01:32:59, 01:33:14 service restarted twice on node 1 nothing sent
01:42:53 configctl ddclient force on node 1 set new ip A

The earlier takeovers updated correctly because each node got a new address. The record stayed on B for about 18 minutes, through two service restarts, until the status file was deleted.

Additional context

The same HA problem was raised for the old os-dyndns plugin in #702, which was closed after timing out. The native backend has replaced that code since, but the gap is the same.

Environment

OPNsense 26.7.4_1 (amd64), CARP HA pair
os-ddclient 1.31_1, native backend, Cloudflare account
WAN: PPPoE, "Disconnect dialup interfaces" enabled


AI disclosure, following the fields of this repository's pull request template:

  • Model used: Claude Opus 5 (Anthropic), via Claude Code
  • Extent of AI involvement: code reading, log analysis and drafting of this report. The logs are from my own HA pair, the code references were checked against the plugin source, and I reviewed the whole report before posting.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with dns/ddclient/src/opnsense/scripts/ddclient/lib/account/init.py around BaseAccount.execute(), dns/ddclient/src/opnsense/scripts/ddclient/lib/poller.py, and the existing force action. Then inspect the CARP hook entry points under /usr/local/etc/rc.syshook.d/carp/ and the ddclient service restart path. Done means a MASTER transition causes the native backend to republish when needed, while single-node operation remains unchanged.

Written by the indexing model from the issue text.

Assessment

Tech stack
perl, python
Domain
backend, devops, networking
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
55/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.