SOLVED: os-ddclient and DNSMadeEasy?

Started by MattSF23, Today at 12:33:53 AM

Previous topic - Next topic
Today at 12:33:53 AM Last Edit: Today at 02:29:07 AM by MattSF23
Hi folks, just moved from pfsense to OPNsense and it was a bit bumpy but now things are working pretty well. One thing I forgot to check was what dynamic DNS services are supported by the os-ddclient plugin. I see 'DNS Made Easy (digicert) in the source code for os-ddclient:
https://github.com/opnsense/plugins/blob/acb1d44b1109617e70b505b8800753266f5a2099/dns/ddclient/src/opnsense/mvc/app/models/OPNsense/DynDNS/DynDNS.xml#L51
But then it looks like it was (partially?) removed (no longer in ddclient.conf in master/main)

It doesn't appear to be an active choice in the GUI console under Service in the plugin settings. I'm guessing the code didn't work or there wasn't much demand to maintain it?

Does anyone know if the functionality is duplicated in another service name, or should I look for a new DNS provider? 😔 Thanks!

Oh I should have said: I'm using Firmware version 26.7.3_11 and os-ddclient version 1.31_1

wow, what a rabbit hole that was 😛

I asked my close, personal friend Claude to investigate and here's some info below. The TL;DR on this is: you can use the older ddclient backend or use 'native' for the built-in Python-based dynamic DNS support in OPNSense. Select whichever under Services: Dynamic DNS: Settings: General Settings. If you're going with the native version, there is some funky formatting you'll need to do for 'Server'
In which case you'll have to ... READ 😁

OPNSENSE OS-DDCLIENT + DNS MADE EASY (DIGICERT) - FINDINGS & CONFIGURATION GUIDE
================================================================================

SUMMARY
-------
DNS Made Easy (DME) support in os-ddclient is not missing or abandoned - it is
hidden behind a backend setting people might not know exists. Switching
Backend to "ddclient" in General Settings makes it reappear in the account
dropdown. If the legacy backend does not work cleanly, DME can also be
configured on the newer "native" backend using its generic custom-URL escape
hatch, with the DME API URL hand-built in the Server field. This second
method (embedded URI) is the one confirmed working.


BACKGROUND: THREE CONFUSINGLY SIMILAR NAMES
--------------------------------------------
- os-dyndns   : the original, now fully removed OPNsense plugin. Predates
                ddclient entirely; had its own separate implementation.

- ddclient    : the upstream Perl dynamic-DNS utility (FreeBSD package
                ddclient-devel). As of ~2023, upstream ddclient is
                unmaintained/archived (see ddclient/ddclient issue #528 on
                GitHub).

- os-ddclient : the current OPNsense plugin (GUI + service). Despite the
                name, it ships TWO independent backends under one plugin:
                the legacy Perl ddclient binary, and a from-scratch Python
                reimplementation labeled "native" in the GUI.


ROOT CAUSE OF "DNS MADE EASY ISN'T IN THE LIST"
------------------------------------------------
- DME was added to os-ddclient back in Feb 2022 (commit 6a6882e0) and is
  still fully present today in the plugin's protocol dropdown definition -
  confirmed directly against current master source.

- In Jan 2023 (commit ef91a6b4), the plugin gained a second, from-scratch
  Python backend ("native"), intended to eventually replace the unmaintained
  Perl ddclient.

- The Python backend only reimplements a small subset of providers:
  allinkl, aws, azure, cloudflare, digitalocean, dnspod_cn, domeneshop,
  duckdns, dyndns2, gandi, hetzner, hostinger, netcup, powerdns.
  DNS Made Easy was never ported to it.

- The GUI filters the protocol dropdown based on which backend is selected.
  If Backend = opnsense (native), the dropdown shows ONLY that ~14-provider
  list, completely hiding DME (and every other legacy-only provider). The
  default backend in current master is now "opnsense" - so a fresh install
  hides DME by default.

- FIX: General Settings -> Backend -> "ddclient". This restores the full
  legacy protocol list, including DME.


KNOWN ISSUE ON THE LEGACY BACKEND
-----------------------------------
GitHub opnsense/plugins issue #3494: a user configured DME correctly per
DME's own docs (Service = DNS Made Easy (digicert), resourceID = numeric
Dynamic DNS ID, Password = per-record Dynamic DNS Password, Hostname(s) =
real hostname) and still got this log entry:

    FAILED: Updating HOSTNAME: Server said: '0':

This was never confirmed resolved for DME specifically in that thread. Test
carefully and check the ddclient log after saving. If you hit this same
failure, it is a known unresolved bug in an unmaintained dependency, not a
configuration mistake.


ALTERNATIVE: STAY ON THE NATIVE/PYTHON BACKEND (CONFIRMED WORKING)
---------------------------------------------------------------------
The native backend has a generic escape hatch for providers it does not
implement natively. The dyndns2.py handler supports:

    Service  = Custom
    Protocol = Custom GET / Custom POST / Custom PUT

...which lets you supply a fully custom URL. Simplified source:

    if protocol in ['get', 'post', 'put']:
        url = self.settings.get('server')
        url = url.replace('__MYIP__', self.current_address)
        url = url.replace('__HOSTNAME__', self.settings.get('hostnames'))
        req = requests.request(method=protocol, url=url, ...,
            auth=HTTPBasicAuth(self.settings.get('username'),
                                self.settings.get('password')))

Important limitations of this mechanism:

1. Service and Protocol are two separate dropdowns. "Custom GET/POST/PUT" in
   the Protocol list is the same generic handler just picking an HTTP verb;
   you still need Service = Custom for the account to route to this handler
   at all.

2. The code only substitutes __MYIP__ and __HOSTNAME__ inside the Server
   string. There is NO __USERNAME__ or __PASSWORD__ placeholder. The
   Username/Password form fields are ONLY ever used to build an HTTP Basic
   Auth header - they are never merged into the URL.

3. DME's real API (per ddclient's own upstream Perl source) expects
   username= and password= as literal query-string parameters, not Basic
   Auth. Since there is no way to inject the form fields into the URL, the
   real credentials must be hardcoded directly into the Server string, and
   Username/Password left blank (harmless - they just produce an unused,
   ignored Basic Auth header).

4. Consequence: the DDNS password ends up stored in plaintext in config.xml
   as part of the Server field (a plain text field, not masked like the
   dedicated Password field). Low risk for a DME-record-specific DDNS
   password, but worth knowing.


DME'S REAL API SHAPE (FROM DDCLIENT'S OWN PROTOCOL DEFINITION)
------------------------------------------------------------------
    server = cp.dnsmadeeasy.com
    script = /servlet/updateip

    Full request:
    https://cp.dnsmadeeasy.com/servlet/updateip?username=...&password=...&ip=...&id=...

- id= is a NUMERIC Dynamic DNS Record ID, generated per-record in the DME
  control panel - NOT a hostname. This is the single biggest DME-specific
  gotcha: every other provider in this plugin expects a real hostname in the
  Hostnames field; DME expects that field to hold the numeric record ID
  instead (comma-separated if updating more than one record).

- password= is the record's Dynamic DNS Password, a separate value DME
  generates per record - NOT the main DME account login password.


CONFIGURATION - NATIVE BACKEND, CUSTOM ESCAPE HATCH (CONFIRMED WORKING)
---------------------------------------------------------------------------
    Service              : Custom
    Protocol             : Custom GET
    Server               : https://cp.dnsmadeeasy.com/servlet/updateip?username=DME_USERNAME&password=YOUR_DDNS_PASSWORD&ip=__MYIP__&id=__HOSTNAME__
    Username / Password  : leave blank (unused for this provider)
    Hostname(s)          : your numeric DME Dynamic DNS Record ID(s),
                            e.g. 1007  or  1007,1008

Remember to URL-encode special characters in the embedded username/password if needed
(e.g. "@" becomes "%40").


CONFIGURATION - LEGACY DDCLIENT BACKEND, NATIVE DME SUPPORT (UNTESTED / KNOWN BUG RISK)
-------------------------------------------------------------------------------------------
    Backend (General Settings) : ddclient
    Service                    : DNS Made Easy (digicert)
    resourceID                 : your numeric DME Dynamic DNS Record ID
    Username                   : your DME account login email
    Password                   : the record's Dynamic DNS Password
                                  (not your account password)
    Hostname(s)                : your real hostname


FALLBACK
--------
If neither plugin approach works, DME's own KB shell-script-on-cron approach
is a fine, simple, proven fallback:
https://support.dnsmadeeasy.com/hc/en-us/articles/34327247093531-The-DDNS-Shell-Script

It is functionally identical to the "native backend custom GET" approach
above, just running outside OPNsense's plugin framework via cron instead.
You lose the GUI status/last-updated indicator and automatic re-trigger on
WAN IP change, but gain independence from either of os-ddclient's two
backends and their respective rough edges.


OUTCOME
-------
The embedded-URI method (native backend, Service = Custom, Protocol =
Custom GET, credentials and record ID baked into the Server field) has been
tested and confirmed working.