Why OpenWrt DDNS does not start on boot
Edit on 2025-06-08: My patch to fix this was merged into upstream OpenWrt LuCI master and backported to 24.10, the issue should be gone since 24.10.2.
On OpenWrt, DDNS functionality is provided by the opt-in ddns-scripts package (and optionally ddns-scripts-[provider] packages), which provides both an rc-init script /etc/init.d/ddns and a hotplug.d hook /etc/hotplug.d/iface/95-ddns to start it automatically:
-
The
rc-initscript, instead of starting a “daemon” and maintaining it like other scripts that usesprocd, mainly just calls/usr/lib/ddns/dynamic_dns_updater.sh, which without explicit interface names afterstart/stop/reloadjust double forks the workers and quits. Note it has an emptyboot()function which shadowsstart()on boot, i.e./usr/lib/ddns/dynamic_dns_updater.sh -- startwould not be run on boot.cat /etc/init.d/ddns#!/bin/sh /etc/rc.common START=95 STOP=10 boot() { return 0 } reload() { /usr/lib/ddns/dynamic_dns_updater.sh -- reload return 0 } restart() { /usr/lib/ddns/dynamic_dns_updater.sh -- stop sleep 1 # give time to shutdown /usr/lib/ddns/dynamic_dns_updater.sh -- start } start() { /usr/lib/ddns/dynamic_dns_updater.sh -- start } stop() { /usr/lib/ddns/dynamic_dns_updater.sh -- stop return 0 } -
The
hotplug.dhook starts instances for “interfaces” when they’re brought up bynetifdand triggershotplugevent (e.g. when youifupmanually, orreconnectan interface from LuCI, or they start up automatically on boot afternetifdis up and running):cat /etc/hotplug.d/iface/95-ddns#!/bin/sh # there are other ACTIONs like ifupdate we don't need case "$ACTION" in ifup) # OpenWrt is giving a network not phys. Interface /etc/init.d/ddns enabled && /usr/lib/ddns/dynamic_dns_updater.sh -n "$INTERFACE" -- start ;; ifdown) /usr/lib/ddns/dynamic_dns_updater.sh -n "$INTERFACE" -- stop ;; esac
Both the rc-init script and the hotplug.d maintain nothing: they just spawn workers for interfaces, either fork and run /usr/lib/ddns/dynamic_dns_updater.sh -n "$INTERFACE" -- start by itself, or from a convenient shortcut provided by /usr/lib/ddns/dynamic_dns_updater.sh -- start which iterates uci config ddns internally to do the work.
So on boot the intended logic that dynamic_dns_updater shall be spawned on interfaces is as follows:
- The early init stage
- The procd
exec-ed by early init and becomes new PID 1 - The
ubusdbecomes ready - The
/etc/init.d/networkstarts, and spawnsnetifdinprocd - The
/etc/init.d/ddnsstarts, and due to emptyboot()it does nothing - The wan interface becomes ready in
netifd - The
/etc/hotplug.d/iface/95-ddnshook triggers on interface(s) that you have configuredddnson, and the corresponding worker(s) would be spawned.
Note that the hotplug.d hook uses the internal name used by netifd. That is, an “physical” “interface” might e.g. be called as br-lan in the scope of Linux, but would be called lan in the scope of netifd, uci, LuCI, etc and of course hotplug.d.
Now let’s discuss about an “issue”: many with a PPPoE wan might find a strange phenomenon: even though they have “enabled” the ddns service and configured it on pppoe-wan “interface”, the ddns worker would not correctly start on boot on their PPPoE wan interface. The reason this issue happens is due to the combination of following factors:
- In OpenWrt, software-based “interface”s are named in the style of
[protocol]-[network], e.g. for PPPoE-based “wan” interface/network, the actual Linux interface name that’s created would bepppoe-wanconfig interface 'wan' option device 'eth5' option proto 'pppoe' option username 'xxxxxxxx' option password 'yyyyyy' option keepalive '10 60' option ipv6 'auto> ip l | grep wan 19: pppoe-wan: <POINTOPOINT,MULTICAST,NOARP,UP,LOWER_UP> mtu 1492 qdisc fq_codel state UNKNOWN mode DEFAULT group default qlen 3 - In luci-app-ddns, the “interface” attribute is derived from the current source network/interface, e.g. when you configure “network” “wan”, this would be
wan; when you configure “interface” “pppoe-wan”, this would bepppoe-wan:o = s.taboption('advanced', form.DummyValue, '_interface', _("Event Network"), _("Network on which the ddns-updater scripts will be started")); o.depends("ip_source", "interface"); o.depends("ip_source", "network"); o.forcewrite = true; o.modalonly = true; o.cfgvalue = function(section_id) { return uci.get('ddns', section_id, 'interface') || _('This will be autoset to the selected interface'); }; o.write = function(section_id) { var opt = this.section.formvalue(section_id, 'ip_source'); var val = this.section.formvalue(section_id, 'ip_'+opt); return uci.set('ddns', section_id, 'interface', val); }; - In dynamic_dns_updater.sh i.e. the actual updater worker, the uci attribute
interfaceneeds to be the OpenWrt/netifd internal name that’s put on the “network” / Openwrt “interface”, not the Linux “interface”:# interface network interface used by hotplug.d i.e. 'wan' or 'wan6' - In dynamic_dns_functions.sh, the
start_daemon_for_all_ddns_sectionstakes the “network” name as argument and tries to get oneddnssection withinterfaceequalling it (notewanis the fallback name):# starts updater script for all given sections or only for the one given # $1 = interface (Optional: when given only scripts are started # configured for that interface) # used by /etc/hotplug.d/iface/95-ddns on IFUP # and by /etc/init.d/ddns start start_daemon_for_all_ddns_sections() { local event_if sections section_id configured_if event_if="$1" load_all_service_sections sections for section_id in $sections; do config_get configured_if "$section_id" interface "wan" [ -z "$event_if" ] || [ "$configured_if" = "$event_if" ] || continue /usr/lib/ddns/dynamic_dns_updater.sh -v "$VERBOSE" -S "$section_id" -- start & done } - When retrieving network information from
netifd, the “interface” must be the Openwrt “interface” / network, not the Linux “interface”. There’s no internal fallback logic to get the info from a Linux “interface”.> ubus call network.interface status '{"interface":"wan"}' | jsonfilter -e '@["ipv4-address"][0].address' xxx.xxx.xxx.xxx > ubus call network.interface status '{"interface":"pppoe-wan"}' | jsonfilter -e '@["ipv4-address"][0].address' Command failed: Not found Failed to parse json data: unexpected end of data - Likewise, the hotplug event only triggers on
wan, not onpppoe-wan - So, the
hotplugevent actually triggers and it runs/usr/lib/ddns/dynamic_dns_updater.sh -n wan -- startto start the worker for interfacewan, but as it could not find any config section in/etc/config/ddnswithinterface=wan(which in reality isinterface=pppoe-wan), it just quits and nevers spawns the actual/usr/lib/ddns/dynamic_dns_updater.sh -S SECTION -- startworker.
Note that while hotplug.d logic fails, you can still run /etc/init.d/ddns start to effectively run /usr/lib/ddns/dynamic_dns_updater.sh -- start, which just iterates the whole /etc/config/ddns config and would start all workers for all sections (as -n NETWORK is skipped and -S SECTION is run directly).
This of course does not only affect PPPoE wan, but in general affects any interface that’s named differently from the corresponding network name.
There are two correct way to fix the issue, one is simply LuCI-only, and another one needs some uci (or manual config editting) but does not touch logic codes:
- The simple way is, without touching any of the above code, to configure your DDNS instance with source as “network” “wan”, instead of “interface” “pppoe-wan”, so you have
interface=wanin your/etc/config/ddnsand this way thehotplugevent would correctly starts on “network” “wan” - Another way is, to modify the
interfacevalue (you can also edit/etc/config/ddnsmanually)uci set ddns.cfv4.interface=wan uci commit ddns
Now with this knowledge you shall know that why the following “band-aid” “hacks” seem to “fix” the “problem” but they are very unreliable.
- By removing
boot()function in/etc/init.d/ddns, you can force/usr/lib/ddns/dynamic_dns_updater.sh -- startto run on boot, which would spawn workers for each configured section. The workers are there, but if another ifdown & ifup occurs then they could break. As the intended way with hotplug.d is that the worker shall be brought up after ifup and brough down before ifdown. - By putting
/etc/init.d/ddns restartin your/etc/rc.local, you’re basically doing the same thing as removingboot() - By putting both
/etc/init.d/ddns restartin your/etc/rc.local, and asleepbefore it, you have the addtional hope thatpppoe-wandefinitely becomes online after that timeout, however it’s not guaranteed. - By removing
boot()function in/etc/init.d/ddns, puttingsleepand/etc/init.d/ddns restartin your/etc/rc.local. You’re combining “band-aid”s which makes your device more and more non-reproducible. - Things can still fail after the above “band-aids” if your
pppoe-wanconnection is not there and you have configuredretry_max_countfor DDNS sections. If you usehotplug.dthen the worker is guaranteed to be started onpppoe-wancreation and stopped onpppoe-wandestruction.