os-ddclient: native backend skips update after CARP failback when the node gets its previous IP back
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:
- I have read the contributing guide lines at https://github.com/opnsense/plugins/blob/master/CONTRIBUTING.md
- I have searched the existing issues, open and closed, and I'm convinced that mine is new.
- The title contains the plugin to which this issue belongs
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:
- Node 1 is MASTER with WAN address A and publishes A. Its status file holds A.
- Failover: node 2 becomes MASTER, gets address B and publishes B. The record now holds B.
- 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
- 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.
- Let node 1 publish its address A.
- Put node 1 into persistent CARP maintenance mode. Node 2 takes over, gets address B and publishes it.
- Leave maintenance mode on node 1. It becomes MASTER again and gets A back.
/var/tmp/ddclient_opn.statuson 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 fornoip-free. The native backend supportsnoipbut 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 storesmtime, so this is one more condition inBaseAccount.execute()(time.time() - self.mtime > max_interval), defaulting to 25 days. - A "Force update" button in the GUI/API calling the existing
forceaction. #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 -forcein theforceaction when the native backend is selected, so it does not reportError (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
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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