diff --git a/data/templates/dns-dynamic/ddclient.conf.j2 b/data/templates/dns-dynamic/ddclient.conf.j2
new file mode 100644
index 000000000..a19b79c00
--- /dev/null
+++ b/data/templates/dns-dynamic/ddclient.conf.j2
@@ -0,0 +1,72 @@
+{% macro render_config(host, address, web_options, ip_suffixes=['']) %}
+{# Address: use=if, if=ethX, usev6=ifv6, ifv6=ethX, usev6=webv6, webv6=https://v6.example.com #}
+{% for ipv in ip_suffixes %}
+use{{ ipv }}={{ address if address == 'web' else 'if' }}{{ ipv }}, \
+{%     if address == 'web' %}
+{%         if web_options.url is vyos_defined %}
+web{{ ipv }}={{ web_options.url }}, \
+{%         endif %}
+{%         if web_options.skip is vyos_defined %}
+web-skip{{ ipv }}='{{ web_options.skip }}', \
+{%         endif %}
+{%     else %}
+if{{ ipv }}={{ address }}, \
+{%     endif %}
+{% endfor %}
+{# Other service options #}
+{% for k,v in kwargs.items() %}
+{%     if v is vyos_defined %}
+{{ k }}={{ v }}{{ ',' if not loop.last }} \
+{%     endif %}
+{% endfor %}
+{# Actual hostname for the service #}
+{{ host }}
+{% endmacro %}
+### Autogenerated by dns_dynamic.py ###
+daemon=1m
+syslog=yes
+ssl=yes
+pid={{ config_file | replace('.conf', '.pid') }}
+cache={{ config_file | replace('.conf', '.cache') }}
+
+{% if address is vyos_defined %}
+{%     for address, service_cfg in address.items() %}
+{%         if service_cfg.rfc2136 is vyos_defined %}
+{%             for name, config in service_cfg.rfc2136.items() %}
+{%                 if config.description is vyos_defined %}
+# {{ config.description }}
+
+{%                 endif %}
+{%                 for host in config.host_name if config.host_name is vyos_defined %}
+# RFC2136 dynamic DNS configuration for {{ name }}: [{{ config.zone }}, {{ host }}]
+{# Don't append 'new-style' compliant suffix ('usev4', 'usev6', 'ifv4', 'ifv6' etc.)
+   to the properties since 'nsupdate' doesn't support that yet. #}
+{{ render_config(host, address, service_cfg.web_options,
+                 protocol='nsupdate', server=config.server, zone=config.zone,
+                 password=config.key, ttl=config.ttl) }}
+
+{%                 endfor %}
+{%             endfor %}
+{%         endif %}
+{%         if service_cfg.service is vyos_defined %}
+{%             for name, config in service_cfg.service.items() %}
+{%                 if config.description is vyos_defined %}
+# {{ config.description }}
+
+{%                 endif %}
+{%                 for host in config.host_name if config.host_name is vyos_defined %}
+{%                     set ip_suffixes = ['v4', 'v6'] if config.ip_version == 'both'
+                                                      else (['v6'] if config.ip_version == 'ipv6' else ['']) %}
+# Web service dynamic DNS configuration for {{ name }}: [{{ config.protocol }}, {{ host }}]
+{# For ipv4 only setup, don't append 'new-style' compliant suffix ('usev4', 'ifv4', 'webv4' etc.)
+   to the properties and instead live through the deprecation warnings for better compatibility
+   with most ddclient protocols. #}
+{{ render_config(host, address, service_cfg.web_options, ip_suffixes,
+                 protocol=config.protocol, server=config.server, zone=config.zone,
+                 login=config.username, password=config.password) }}
+
+{%                 endfor %}
+{%             endfor %}
+{%         endif %}
+{%     endfor %}
+{% endif %}
diff --git a/data/templates/dns-dynamic/override.conf.j2 b/data/templates/dns-dynamic/override.conf.j2
new file mode 100644
index 000000000..8a9dfcd70
--- /dev/null
+++ b/data/templates/dns-dynamic/override.conf.j2
@@ -0,0 +1,11 @@
+{% set vrf_command = 'ip vrf exec ' ~ vrf ~ ' ' if vrf is vyos_defined else '' %}
+[Unit]
+ConditionPathExists={{ config_file }}
+After=vyos-router.service
+
+[Service]
+PIDFile=
+PIDFile={{ config_file | replace('.conf', '.pid') }}
+EnvironmentFile=
+ExecStart=
+ExecStart=/usr/bin/ddclient -file {{ config_file }}
diff --git a/data/templates/dynamic-dns/ddclient.conf.j2 b/data/templates/dynamic-dns/ddclient.conf.j2
deleted file mode 100644
index e8ef5ac90..000000000
--- a/data/templates/dynamic-dns/ddclient.conf.j2
+++ /dev/null
@@ -1,53 +0,0 @@
-### Autogenerated by dynamic_dns.py ###
-daemon=1m
-syslog=yes
-ssl=yes
-
-{% if interface is vyos_defined %}
-{%     for iface, iface_config in interface.items() %}
-# ddclient configuration for interface "{{ iface }}"
-{%         if iface_config.use_web is vyos_defined %}
-{%             set web_skip = ", web-skip='" ~ iface_config.use_web.skip ~ "'" if iface_config.use_web.skip is vyos_defined else '' %}
-use=web, web='{{ iface_config.use_web.url }}'{{ web_skip }}
-{%         else %}
-{{ 'usev6=ifv6' if iface_config.ipv6_enable is vyos_defined else 'use=if' }}, if={{ iface }}
-{%         endif %}
-
-{%         if iface_config.rfc2136 is vyos_defined %}
-{%             for rfc2136, config in iface_config.rfc2136.items() %}
-{%                 for dns_record in config.record if config.record is vyos_defined %}
-# RFC2136 dynamic DNS configuration for {{ rfc2136 }}, {{ config.zone }}, {{ dns_record }}
-server={{ config.server }}
-protocol=nsupdate
-password={{ config.key }}
-ttl={{ config.ttl }}
-zone={{ config.zone }}
-{{ dns_record }}
-
-{%                 endfor %}
-{%             endfor %}
-{%         endif %}
-
-{%         if iface_config.service is vyos_defined %}
-{%             for service, config in iface_config.service.items() %}
-{%                 for dns_record in config.host_name %}
-# DynDNS provider configuration for {{ service }}, {{ dns_record }}
-protocol={{ config.protocol }},
-max-interval=28d,
-{%                     if config.login is vyos_defined %}
-login={{ config.login }},
-{%                     endif %}
-password='{{ config.password }}',
-{%                     if config.server is vyos_defined %}
-server={{ config.server }},
-{%                     endif %}
-{%                     if config.zone is vyos_defined %}
-zone={{ config.zone }},
-{%                     endif %}
-{{ dns_record }}
-
-{%                 endfor %}
-{%             endfor %}
-{%         endif %}
-{%     endfor %}
-{% endif %}
diff --git a/interface-definitions/dns-dynamic.xml.in b/interface-definitions/dns-dynamic.xml.in
index 48c101d73..292c50603 100644
--- a/interface-definitions/dns-dynamic.xml.in
+++ b/interface-definitions/dns-dynamic.xml.in
@@ -1,205 +1,159 @@
 <?xml version="1.0"?>
 <interfaceDefinition>
   <node name="service">
     <children>
       <node name="dns">
         <properties>
           <help>Domain Name System related services</help>
         </properties>
         <children>
-          <node name="dynamic" owner="${vyos_conf_scripts_dir}/dynamic_dns.py">
+          <node name="dynamic" owner="${vyos_conf_scripts_dir}/dns_dynamic.py">
             <properties>
               <help>Dynamic DNS</help>
             </properties>
             <children>
-              <tagNode name="interface">
+              <tagNode name="address">
                 <properties>
-                  <help>Interface to send Dynamic DNS updates for</help>
-                  <completionHelp>
-                    <script>${vyos_completion_dir}/list_interfaces</script>
-                  </completionHelp>
+                  <help>Obtain IP address to send Dynamic DNS update for</help>
                   <valueHelp>
                     <format>txt</format>
-                    <description>Interface name</description>
+                    <description>Use interface to obtain the IP address</description>
                   </valueHelp>
+                  <valueHelp>
+                    <format>web</format>
+                    <description>Use HTTP(S) web request to obtain the IP address</description>
+                  </valueHelp>
+                  <completionHelp>
+                    <script>${vyos_completion_dir}/list_interfaces</script>
+                    <list>web</list>
+                  </completionHelp>
                   <constraint>
                     #include <include/constraint/interface-name.xml.i>
+                    <regex>web</regex>
                   </constraint>
                 </properties>
                 <children>
-                  <tagNode name="rfc2136">
+                  <node name="web-options">
                     <properties>
-                      <help>RFC2136 Update name</help>
+                      <help>Options when using HTTP(S) web request to obtain the IP address</help>
                     </properties>
                     <children>
-                      <leafNode name="key">
+                      #include <include/url.xml.i>
+                      <leafNode name="skip">
                         <properties>
-                          <help>File containing the secret key shared with remote DNS server</help>
+                          <help>Pattern to skip from the HTTP(S) respose</help>
                           <valueHelp>
-                            <format>filename</format>
-                            <description>File in /config/auth directory</description>
+                            <format>txt</format>
+                            <description>Pattern to skip from the HTTP(S) respose to extract the external IP address</description>
                           </valueHelp>
                         </properties>
                       </leafNode>
-                      <leafNode name="record">
-                        <properties>
-                          <help>Record to be updated</help>
-                          <multi/>
-                        </properties>
-                      </leafNode>
-                      <leafNode name="server">
-                        <properties>
-                          <help>Server to be updated</help>
-                        </properties>
-                      </leafNode>
-                      <leafNode name="ttl">
+                    </children>
+                  </node>
+                  <tagNode name="rfc2136">
+                    <properties>
+                      <help>RFC2136 nsupdate configuration</help>
+                      <valueHelp>
+                        <format>txt</format>
+                        <description>RFC2136 nsupdate service name</description>
+                      </valueHelp>
+                    </properties>
+                    <children>
+                      #include <include/generic-description.xml.i>
+                      #include <include/dns/dynamic-service-host-name-server.xml.i>
+                      <leafNode name="key">
                         <properties>
-                          <help>Time To Live (default: 600)</help>
+                          <help>File containing the TSIG secret key shared with remote DNS server</help>
                           <valueHelp>
-                            <format>u32:1-86400</format>
-                            <description>DNS forwarding cache size</description>
+                            <format>filename</format>
+                            <description>File in /config/auth directory</description>
                           </valueHelp>
                           <constraint>
-                            <validator name="numeric" argument="--range 1-86400"/>
+                            <validator name="file-path" argument="--strict --parent-dir /config/auth"/>
                           </constraint>
                         </properties>
-                        <defaultValue>600</defaultValue>
                       </leafNode>
+                      #include <include/dns/time-to-live.xml.i>
                       <leafNode name="zone">
                         <properties>
-                          <help>Zone to be updated</help>
+                          <help>Forwarding zone to be updated</help>
+                          <valueHelp>
+                            <format>txt</format>
+                            <description>RFC2136 Zone to be updated</description>
+                          </valueHelp>
+                          <constraint>
+                            <validator name="fqdn"/>
+                          </constraint>
                         </properties>
                       </leafNode>
                     </children>
                   </tagNode>
                   <tagNode name="service">
                     <properties>
-                      <help>Service being used for Dynamic DNS</help>
-                      <completionHelp>
-                        <list>afraid changeip cloudflare dnspark dslreports dyndns easydns namecheap noip sitelutions zoneedit</list>
-                      </completionHelp>
+                      <help>Dynamic DNS configuration</help>
                       <valueHelp>
                         <format>txt</format>
-                        <description>Dynanmic DNS service with a custom name</description>
-                      </valueHelp>
-                      <valueHelp>
-                        <format>afraid</format>
-                        <description>afraid.org Services</description>
-                      </valueHelp>
-                      <valueHelp>
-                        <format>changeip</format>
-                        <description>changeip.com Services</description>
-                      </valueHelp>
-                      <valueHelp>
-                        <format>cloudflare</format>
-                        <description>cloudflare.com Services</description>
+                        <description>Dynamic DNS service name</description>
                       </valueHelp>
-                      <valueHelp>
-                        <format>dnspark</format>
-                        <description>dnspark.com Services</description>
-                      </valueHelp>
-                      <valueHelp>
-                        <format>dslreports</format>
-                        <description>dslreports.com Services</description>
-                      </valueHelp>
-                      <valueHelp>
-                        <format>dyndns</format>
-                        <description>dyndns.com Services</description>
-                      </valueHelp>
-                      <valueHelp>
-                        <format>easydns</format>
-                        <description>easydns.com Services</description>
-                      </valueHelp>
-                      <valueHelp>
-                        <format>namecheap</format>
-                        <description>namecheap.com Services</description>
-                      </valueHelp>
-                      <valueHelp>
-                        <format>noip</format>
-                        <description>noip.com Services</description>
-                      </valueHelp>
-                      <valueHelp>
-                        <format>sitelutions</format>
-                        <description>sitelutions.com Services</description>
-                      </valueHelp>
-                      <valueHelp>
-                        <format>zoneedit</format>
-                        <description>zoneedit.com Services</description>
-                      </valueHelp>
-                      <constraint>
-                        <regex>(custom|afraid|changeip|cloudflare|dnspark|dslreports|dyndns|easydns|namecheap|noip|sitelutions|zoneedit|\w+)</regex>
-                      </constraint>
-                      <constraintErrorMessage>You can use only predefined list of services or word characters (_, a-z, A-Z, 0-9) as service name</constraintErrorMessage>
                     </properties>
                     <children>
-                      <leafNode name="host-name">
-                        <properties>
-                          <help>Hostname to register with Dynamic DNS service</help>
-                          <constraint>
-                            #include <include/constraint/host-name.xml.i>
-                          </constraint>
-                          <constraintErrorMessage>Host-name must be alphanumeric and can contain hyphens</constraintErrorMessage>
-                          <multi/>
-                        </properties>
-                      </leafNode>
-                      <leafNode name="login">
-                        <properties>
-                          <help>Login/Username for Dynamic DNS service</help>
-                        </properties>
-                      </leafNode>
+                      #include <include/generic-description.xml.i>
+                      #include <include/dns/dynamic-service-host-name-server.xml.i>
+                      #include <include/generic-username.xml.i>
                       #include <include/generic-password.xml.i>
                       <leafNode name="protocol">
                         <properties>
                           <help>ddclient protocol used for Dynamic DNS service</help>
                           <completionHelp>
                             <script>${vyos_completion_dir}/list_ddclient_protocols.sh</script>
                           </completionHelp>
                           <constraint>
                             <validator name="ddclient-protocol"/>
                           </constraint>
                         </properties>
                       </leafNode>
-                      #include <include/server-ipv4-fqdn.xml.i>
                       <leafNode name="zone">
                         <properties>
                           <help>DNS zone to update (not used by all protocols)</help>
                           <valueHelp>
                             <format>txt</format>
                             <description>Name of DNS zone</description>
                           </valueHelp>
                         </properties>
                       </leafNode>
-                    </children>
-                  </tagNode>
-                  <node name="use-web">
-                    <properties>
-                      <help>Use HTTP(S) web request to obtain external IP address instead of the IP address associated with the interface</help>
-                    </properties>
-                    <children>
-                      <leafNode name="skip">
+                      <leafNode name="ip-version">
                         <properties>
-                          <help>Pattern to skip from the respose</help>
+                          <help>IP address version to use</help>
                           <valueHelp>
-                            <format>txt</format>
-                            <description>Pattern to skip from the respose of the given URL to extract the external IP address</description>
+                            <format>_ipv4</format>
+                            <description>Use only IPv4 address</description>
+                          </valueHelp>
+                          <valueHelp>
+                            <format>_ipv6</format>
+                            <description>Use only IPv6 address</description>
                           </valueHelp>
+                          <valueHelp>
+                            <format>both</format>
+                            <description>Use both IPv4 and IPv6 address</description>
+                          </valueHelp>
+                          <completionHelp>
+                            <list>ipv4 ipv6 both</list>
+                          </completionHelp>
+                          <constraint>
+                            <regex>(ipv[46]|both)</regex>
+                          </constraint>
+                          <constraintErrorMessage>IP Version must be literal 'ipv4', 'ipv6' or 'both'</constraintErrorMessage>
                         </properties>
+                        <defaultValue>ipv4</defaultValue>
                       </leafNode>
-                      #include <include/url.xml.i>
                     </children>
-                  </node>
-                  <leafNode name="ipv6-enable">
-                    <properties>
-                      <help>Explicitly use IPv6 address instead of IPv4 address to update the Dynamic DNS IP address</help>
-                      <valueless/>
-                    </properties>
-                  </leafNode>
+                  </tagNode>
                 </children>
               </tagNode>
             </children>
           </node>
         </children>
       </node>
     </children>
   </node>
 </interfaceDefinition>
diff --git a/interface-definitions/include/dns/dynamic-service-host-name-server.xml.i b/interface-definitions/include/dns/dynamic-service-host-name-server.xml.i
new file mode 100644
index 000000000..ee1af2a36
--- /dev/null
+++ b/interface-definitions/include/dns/dynamic-service-host-name-server.xml.i
@@ -0,0 +1,34 @@
+<!-- include start from dns/dynamic-service-host-name-server.xml.i -->
+<leafNode name="host-name">
+  <properties>
+    <help>Hostname to register with Dynamic DNS service</help>
+    <constraint>
+        #include <include/constraint/host-name.xml.i>
+    </constraint>
+    <constraintErrorMessage>Host-name must be alphanumeric and can contain hyphens</constraintErrorMessage>
+    <multi/>
+  </properties>
+</leafNode>
+<leafNode name="server">
+  <properties>
+    <help>Remote Dynamic DNS server to send updates to</help>
+    <valueHelp>
+      <format>ipv4</format>
+      <description>IPv4 address of the remote server</description>
+    </valueHelp>
+    <valueHelp>
+      <format>ipv6</format>
+      <description>IPv6 address of the remote server</description>
+    </valueHelp>
+    <valueHelp>
+      <format>hostname</format>
+      <description>Fully qualified domain name of the remote server</description>
+    </valueHelp>
+    <constraint>
+      <validator name="ip-address"/>
+      <validator name="fqdn"/>
+    </constraint>
+    <constraintErrorMessage>Remote server must be IP address or fully qualified domain name</constraintErrorMessage>
+  </properties>
+</leafNode>
+<!-- include end -->
diff --git a/interface-definitions/include/version/dns-dynamic-version.xml.i b/interface-definitions/include/version/dns-dynamic-version.xml.i
new file mode 100644
index 000000000..b25fc6e76
--- /dev/null
+++ b/interface-definitions/include/version/dns-dynamic-version.xml.i
@@ -0,0 +1,3 @@
+<!-- include start from include/version/dns-dynamic-version.xml.i -->
+<syntaxVersion component='dns-dynamic' version='1'></syntaxVersion>
+<!-- include end -->
diff --git a/interface-definitions/xml-component-version.xml.in b/interface-definitions/xml-component-version.xml.in
index e05f64643..8c9e816d1 100644
--- a/interface-definitions/xml-component-version.xml.in
+++ b/interface-definitions/xml-component-version.xml.in
@@ -1,48 +1,49 @@
 <?xml version="1.0"?>
 <interfaceDefinition>
   #include <include/version/bgp-version.xml.i>
   #include <include/version/broadcast-relay-version.xml.i>
   #include <include/version/cluster-version.xml.i>
   #include <include/version/config-management-version.xml.i>
   #include <include/version/conntrack-sync-version.xml.i>
   #include <include/version/conntrack-version.xml.i>
   #include <include/version/container-version.xml.i>
   #include <include/version/dhcp-relay-version.xml.i>
   #include <include/version/dhcp-server-version.xml.i>
   #include <include/version/dhcpv6-server-version.xml.i>
+  #include <include/version/dns-dynamic-version.xml.i>
   #include <include/version/dns-forwarding-version.xml.i>
   #include <include/version/firewall-version.xml.i>
   #include <include/version/flow-accounting-version.xml.i>
   #include <include/version/https-version.xml.i>
   #include <include/version/interfaces-version.xml.i>
   #include <include/version/ids-version.xml.i>
   #include <include/version/ipoe-server-version.xml.i>
   #include <include/version/ipsec-version.xml.i>
   #include <include/version/isis-version.xml.i>
   #include <include/version/l2tp-version.xml.i>
   #include <include/version/lldp-version.xml.i>
   #include <include/version/mdns-version.xml.i>
   #include <include/version/monitoring-version.xml.i>
   #include <include/version/nat66-version.xml.i>
   #include <include/version/nat-version.xml.i>
   #include <include/version/ntp-version.xml.i>
   #include <include/version/openconnect-version.xml.i>
   #include <include/version/ospf-version.xml.i>
   #include <include/version/policy-version.xml.i>
   #include <include/version/pppoe-server-version.xml.i>
   #include <include/version/pptp-version.xml.i>
   #include <include/version/qos-version.xml.i>
   #include <include/version/quagga-version.xml.i>
   #include <include/version/rip-version.xml.i>
   #include <include/version/rpki-version.xml.i>
   #include <include/version/salt-version.xml.i>
   #include <include/version/snmp-version.xml.i>
   #include <include/version/ssh-version.xml.i>
   #include <include/version/sstp-version.xml.i>
   #include <include/version/system-version.xml.i>
   #include <include/version/vrf-version.xml.i>
   #include <include/version/vrrp-version.xml.i>
   #include <include/version/vyos-accel-ppp-version.xml.i>
   #include <include/version/wanloadbalance-version.xml.i>
   #include <include/version/webproxy-version.xml.i>
 </interfaceDefinition>
diff --git a/op-mode-definitions/dns-dynamic.xml.in b/op-mode-definitions/dns-dynamic.xml.in
index 9c37874fb..8047d55cd 100644
--- a/op-mode-definitions/dns-dynamic.xml.in
+++ b/op-mode-definitions/dns-dynamic.xml.in
@@ -1,75 +1,75 @@
 <?xml version="1.0"?>
 <interfaceDefinition>
   <node name="show">
     <children>
       <node name="log">
         <children>
           <node name="dns">
             <children>
               <node name="dynamic">
                 <properties>
                   <help>Show log for dynamic DNS</help>
                 </properties>
                 <command>cat $(printf "%s\n" /var/log/messages* | sort -nr) | grep -e "ddclient"</command>
               </node>
             </children>
           </node>
         </children>
       </node>
       <node name="dns">
         <properties>
           <help>Show DNS information</help>
         </properties>
         <children>
           <node name="dynamic">
             <properties>
               <help>Show Dynamic DNS information</help>
             </properties>
             <children>
               <leafNode name="status">
                 <properties>
                   <help>Show Dynamic DNS status</help>
                 </properties>
-                <command>sudo ${vyos_op_scripts_dir}/dynamic_dns.py --status</command>
+                <command>sudo ${vyos_op_scripts_dir}/dns_dynamic.py --status</command>
               </leafNode>
             </children>
           </node>
         </children>
       </node>
     </children>
   </node>
   <node name="restart">
     <children>
       <node name="dns">
         <children>
           <node name="dynamic">
             <properties>
               <help>Restart Dynamic DNS service</help>
             </properties>
-            <command>sudo ${vyos_op_scripts_dir}/dynamic_dns.py --update</command>
+            <command>sudo ${vyos_op_scripts_dir}/dns_dynamic.py --update</command>
           </node>
         </children>
       </node>
     </children>
   </node>
   <node name="update">
     <properties>
       <help>Update data for a service</help>
     </properties>
     <children>
       <node name="dns">
         <properties>
           <help>Update DNS information</help>
         </properties>
         <children>
           <node name="dynamic">
             <properties>
               <help>Update Dynamic DNS information</help>
             </properties>
-            <command>sudo ${vyos_op_scripts_dir}/dynamic_dns.py --update</command>
+            <command>sudo ${vyos_op_scripts_dir}/dns_dynamic.py --update</command>
           </node>
         </children>
       </node>
     </children>
   </node>
 </interfaceDefinition>
diff --git a/smoketest/scripts/cli/test_service_dns_dynamic.py b/smoketest/scripts/cli/test_service_dns_dynamic.py
index 4a3c05a36..044d053b4 100755
--- a/smoketest/scripts/cli/test_service_dns_dynamic.py
+++ b/smoketest/scripts/cli/test_service_dns_dynamic.py
@@ -1,168 +1,175 @@
 #!/usr/bin/env python3
 #
 # Copyright (C) 2019-2020 VyOS maintainers and contributors
 #
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the GNU General Public License version 2 or later as
 # published by the Free Software Foundation.
 #
 # This program is distributed in the hope that it will be useful,
 # but WITHOUT ANY WARRANTY; without even the implied warranty of
 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 # GNU General Public License for more details.
 #
 # You should have received a copy of the GNU General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 import re
 import os
 import unittest
+import tempfile
 
 from base_vyostest_shim import VyOSUnitTestSHIM
 
 from vyos.configsession import ConfigSessionError
 from vyos.util import cmd
 from vyos.util import process_running
-from vyos.util import read_file
 
 DDCLIENT_CONF = '/run/ddclient/ddclient.conf'
 DDCLIENT_PID = '/run/ddclient/ddclient.pid'
 
 base_path = ['service', 'dns', 'dynamic']
 hostname = 'test.ddns.vyos.io'
 interface = 'eth0'
 
 def get_config_value(key):
     tmp = cmd(f'sudo cat {DDCLIENT_CONF}')
     tmp = re.findall(r'\n?{}=+(.*)'.format(key), tmp)
-    tmp = tmp[0].rstrip(',')
+    tmp = tmp[0].rstrip(', \\')
     return tmp
 
 class TestServiceDDNS(VyOSUnitTestSHIM.TestCase):
     def tearDown(self):
         # Check for running process
         self.assertTrue(process_running(DDCLIENT_PID))
 
         # Delete DDNS configuration
         self.cli_delete(base_path)
         self.cli_commit()
 
         # PID file must no londer exist after process exited
         self.assertFalse(os.path.exists(DDCLIENT_PID))
 
     def test_dyndns_service(self):
-        from itertools import product
-        ddns = ['interface', interface, 'service']
-        users = [None, 'vyos_user']
-        services = ['cloudflare', 'afraid', 'dyndns', 'zoneedit']
-
-        for user, service in product(users, services):
-            password = 'vyos_pass'
-            zone = 'vyos.io'
+        ddns = ['address', interface, 'service']
+        services = {'cloudflare': {'protocol': 'cloudflare', 'zone': 'vyos.io'},
+                    'freedns': {'protocol': 'freedns', 'username': 'vyos_user'},
+                    'zoneedit': {'protocol': 'zoneedit1', 'username': 'vyos_user'}}
+        password = 'vyos_pass'
+        zone = 'vyos.io'
+
+        for svc, details in services.items():
             self.cli_delete(base_path)
-            self.cli_set(base_path + ddns + [service, 'host-name', hostname])
-            if user is not None:
-                self.cli_set(base_path + ddns + [service, 'login', user])
-            self.cli_set(base_path + ddns + [service, 'password', password])
-            self.cli_set(base_path + ddns + [service, 'zone', zone])
+            self.cli_set(base_path + ddns + [svc, 'host-name', hostname])
+            for opt, value in details.items():
+                self.cli_set(base_path + ddns + [svc, opt, value])
+            self.cli_set(base_path + ddns + [svc, 'password', password])
+            self.cli_set(base_path + ddns + [svc, 'zone', zone])
 
             # commit changes
-            if service == 'cloudflare':
+            if details['protocol'] == 'cloudflare':
                 self.cli_commit()
-            elif user is None:
-                # not set user is only allowed for cloudflare
-                with self.assertRaises(ConfigSessionError):
-                    # remove zone to test not set user
-                    self.cli_delete(base_path + ddns + [service, 'zone', 'vyos.io'])
-                    self.cli_commit()
-                # this case is fininshed, user not set is not allowed when service isn't cloudflare
-                continue
             else:
-                # zone option only works on cloudflare, an exception is raised
-                # for all others
+                # zone option does not work on all protocols, an exception is
+                # raised for all others
                 with self.assertRaises(ConfigSessionError):
                     self.cli_commit()
-                self.cli_delete(base_path + ddns + [service, 'zone', 'vyos.io'])
+                self.cli_delete(base_path + ddns + [svc, 'zone', zone])
                 # commit changes again - now it should work
                 self.cli_commit()
 
-            # we can only read the configuration file when we operate as 'root'
-            protocol = get_config_value('protocol')
-            login = None if user is None else get_config_value('login')
-            pwd = get_config_value('password')
-
-            # some services need special treatment
-            protoname = service
-            if service == 'cloudflare':
-                tmp = get_config_value('zone')
-                self.assertTrue(tmp == zone)
-            elif service == 'afraid':
-                protoname = 'freedns'
-            elif service == 'dyndns':
-                protoname = 'dyndns2'
-            elif service == 'zoneedit':
-                protoname = 'zoneedit1'
-
-            self.assertTrue(protocol == protoname)
-            self.assertTrue(login == user)
-            self.assertTrue(pwd == "'" + password + "'")
+            for opt in details.keys():
+                if opt == 'username':
+                    self.assertTrue(get_config_value('login') == details[opt])
+                else:
+                    self.assertTrue(get_config_value(opt) == details[opt])
+
+            self.assertTrue(get_config_value('use') == 'if')
+            self.assertTrue(get_config_value('if') == interface)
 
     def test_dyndns_rfc2136(self):
         # Check if DDNS service can be configured and runs
-        ddns = ['interface', interface, 'rfc2136', 'vyos']
-        ddns_key_file = '/config/auth/my.key'
+        ddns = ['address', interface, 'rfc2136', 'vyos']
+        srv = 'ns1.vyos.io'
+        zone = 'vyos.io'
+        ttl = '300'
 
-        self.cli_set(base_path + ddns + ['key', ddns_key_file])
-        self.cli_set(base_path + ddns + ['record', 'test.ddns.vyos.io'])
-        self.cli_set(base_path + ddns + ['server', 'ns1.vyos.io'])
-        self.cli_set(base_path + ddns + ['ttl', '300'])
-        self.cli_set(base_path + ddns + ['zone', 'vyos.io'])
+        with tempfile.NamedTemporaryFile(prefix='/config/auth/') as key_file:
+            key_file.write(b'S3cretKey')
 
-        # ensure an exception will be raised as no key is present
-        if os.path.exists(ddns_key_file):
-            os.unlink(ddns_key_file)
+            self.cli_set(base_path + ddns + ['key', key_file.name])
+            self.cli_set(base_path + ddns + ['host-name', hostname])
+            self.cli_set(base_path + ddns + ['server', srv])
+            self.cli_set(base_path + ddns + ['ttl', ttl])
+            self.cli_set(base_path + ddns + ['zone', zone])
 
-        # check validate() - the key file does not exist yet
-        with self.assertRaises(ConfigSessionError):
+            # commit changes
             self.cli_commit()
 
-        with open(ddns_key_file, 'w') as f:
-            f.write('S3cretKey')
+            # Check some generating config parameters
+            self.assertEqual(get_config_value('protocol'), 'nsupdate')
+            self.assertTrue(get_config_value('password') == key_file.name)
+            self.assertTrue(get_config_value('server') == srv)
+            self.assertTrue(get_config_value('zone') == zone)
+            self.assertTrue(get_config_value('ttl') == ttl)
+            self.assertEqual(get_config_value('use'), 'if')
+            self.assertEqual(get_config_value('if'), interface)
+
+    def test_dyndns_dual(self):
+        ddns = ['address', interface, 'service']
+        services = {'cloudflare': {'protocol': 'cloudflare', 'zone': 'vyos.io'},
+                    'freedns': {'protocol': 'freedns', 'username': 'vyos_user'}}
+        password = 'vyos_pass'
+        ip_version = 'both'
+
+        for svc, details in services.items():
+            self.cli_delete(base_path)
+            self.cli_set(base_path + ddns + [svc, 'host-name', hostname])
+            for opt, value in details.items():
+                self.cli_set(base_path + ddns + [svc, opt, value])
+            self.cli_set(base_path + ddns + [svc, 'password', password])
+            self.cli_set(base_path + ddns + [svc, 'ip-version', ip_version])
 
-        # commit changes
-        self.cli_commit()
+            # commit changes
+            self.cli_commit()
 
-        # TODO: inspect generated configuration file
+            # Check some generating config parameters
+            for opt in details.keys():
+                if opt == 'username':
+                    self.assertTrue(get_config_value('login') == details[opt])
+                else:
+                    self.assertTrue(get_config_value(opt) == details[opt])
+
+            self.assertTrue(get_config_value('usev4') == 'ifv4')
+            self.assertTrue(get_config_value('usev6') == 'ifv6')
+            self.assertTrue(get_config_value('ifv4') == interface)
+            self.assertTrue(get_config_value('ifv6') == interface)
 
     def test_dyndns_ipv6(self):
-        ddns = ['interface', interface, 'service', 'dynv6']
+        ddns = ['address', interface, 'service', 'dynv6']
         proto = 'dyndns2'
         user = 'none'
         password = 'paSS_4ord'
         srv = 'ddns.vyos.io'
+        ip_version = 'ipv6'
 
-        self.cli_set(base_path + ['interface', interface, 'ipv6-enable'])
         self.cli_set(base_path + ddns + ['host-name', hostname])
-        self.cli_set(base_path + ddns + ['login', user])
+        self.cli_set(base_path + ddns + ['username', user])
         self.cli_set(base_path + ddns + ['password', password])
         self.cli_set(base_path + ddns + ['protocol', proto])
         self.cli_set(base_path + ddns + ['server', srv])
+        self.cli_set(base_path + ddns + ['ip-version', ip_version])
 
         # commit changes
         self.cli_commit()
 
-        protocol = get_config_value('protocol')
-        login = get_config_value('login')
-        pwd = get_config_value('password')
-        server = get_config_value('server')
-        usev6 = get_config_value('usev6')
-
         # Check some generating config parameters
-        self.assertEqual(protocol, proto)
-        self.assertEqual(login, user)
-        self.assertEqual(pwd, f"'{password}'")
-        self.assertEqual(server, srv)
-        self.assertEqual(usev6, f"ifv6, if={interface}")
+        self.assertEqual(get_config_value('protocol'), proto)
+        self.assertEqual(get_config_value('login'), user)
+        self.assertEqual(get_config_value('password'), password)
+        self.assertEqual(get_config_value('server'), srv)
+        self.assertEqual(get_config_value('usev6'), 'ifv6')
+        self.assertEqual(get_config_value('ifv6'), interface)
 
 if __name__ == '__main__':
     unittest.main(verbosity=2)
diff --git a/src/conf_mode/dns_dynamic.py b/src/conf_mode/dns_dynamic.py
new file mode 100755
index 000000000..f97225370
--- /dev/null
+++ b/src/conf_mode/dns_dynamic.py
@@ -0,0 +1,134 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2018-2020 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+import os
+
+from sys import exit
+
+from vyos.config import Config
+from vyos.configdict import dict_merge
+from vyos.template import render
+from vyos.util import call
+from vyos.xml import defaults
+from vyos import ConfigError
+from vyos import airbag
+airbag.enable()
+
+config_file = r'/run/ddclient/ddclient.conf'
+systemd_override = r'/run/systemd/system/ddclient.service.d/override.conf'
+
+# Protocols that require zone
+zone_allowed = ['cloudflare', 'godaddy', 'hetzner', 'gandi', 'nfsn']
+
+# Protocols that do not require username
+username_unnecessary = ['1984', 'cloudflare', 'cloudns', 'duckdns', 'freemyip', 'hetzner', 'keysystems', 'njalla']
+
+# Protocols that support both IPv4 and IPv6
+dualstack_supported = ['cloudflare', 'dyndns2', 'freedns', 'njalla']
+
+def get_config(config=None):
+    if config:
+        conf = config
+    else:
+        conf = Config()
+
+    base_level = ['service', 'dns', 'dynamic']
+    if not conf.exists(base_level):
+        return None
+
+    dyndns = conf.get_config_dict(base_level, key_mangling=('-', '_'), get_first_key=True)
+
+    for address in dyndns['address']:
+        # Apply service specific defaults (stype = ['rfc2136', 'service'])
+        for svc_type in dyndns['address'][address]:
+            default_values = defaults(base_level + ['address', svc_type])
+            for svc_cfg in dyndns['address'][address][svc_type]:
+                dyndns['address'][address][svc_type][svc_cfg] = dict_merge(
+                    default_values, dyndns['address'][address][svc_type][svc_cfg])
+
+    dyndns['config_file'] = config_file
+    return dyndns
+
+def verify(dyndns):
+    # bail out early - looks like removal from running config
+    if not dyndns:
+        return None
+
+    for address in dyndns['address']:
+        # RFC2136 - configuration validation
+        if 'rfc2136' in dyndns['address'][address]:
+            for config in dyndns['address'][address]['rfc2136'].values():
+                for field in ['host_name', 'zone', 'server', 'key']:
+                    if field not in config:
+                        raise ConfigError(f'"{field.replace("_", "-")}" is required for RFC2136 '
+                                          f'based Dynamic DNS service on "{address}"')
+
+        # Dynamic DNS service provider - configuration validation
+        if 'service' in dyndns['address'][address]:
+            for service, config in dyndns['address'][address]['service'].items():
+                error_msg = f'is required for Dynamic DNS service "{service}" on "{address}"'
+
+                for field in ['host_name', 'password', 'protocol']:
+                    if field not in config:
+                        raise ConfigError(f'"{field.replace("_", "-")}" {error_msg}')
+
+                if config['protocol'] in zone_allowed and 'zone' not in config:
+                        raise ConfigError(f'"zone" {error_msg}')
+
+                if config['protocol'] not in zone_allowed and 'zone' in config:
+                        raise ConfigError(f'"{config["protocol"]}" does not support "zone"')
+
+                if config['protocol'] not in username_unnecessary:
+                    if 'username' not in config:
+                        raise ConfigError(f'"username" {error_msg}')
+
+                if config['ip_version'] == 'both':
+                    if config['protocol'] not in dualstack_supported:
+                        raise ConfigError(f'"{config["protocol"]}" does not support IPv4 and IPv6 at the same time')
+                    # dyndns2 protocol in ddclient honors dual stack only for dyn.com (dyndns.org)
+                    if config['protocol'] == 'dyndns2' and 'server' in config and config['server'] != 'members.dyndns.org':
+                        raise ConfigError(f'"{config["protocol"]}" for "{config["server"]}" does not support IPv4 and IPv6 at the same time')
+
+    return None
+
+def generate(dyndns):
+    # bail out early - looks like removal from running config
+    if not dyndns:
+        return None
+
+    render(config_file, 'dns-dynamic/ddclient.conf.j2', dyndns)
+    render(systemd_override, 'dns-dynamic/override.conf.j2', dyndns)
+    return None
+
+def apply(dyndns):
+    if not dyndns:
+        call('systemctl stop ddclient.service')
+        if os.path.exists(config_file):
+            os.unlink(config_file)
+    else:
+        call('systemctl restart ddclient.service')
+
+    return None
+
+if __name__ == '__main__':
+    try:
+        c = get_config()
+        verify(c)
+        generate(c)
+        apply(c)
+    except ConfigError as e:
+        print(e)
+        exit(1)
diff --git a/src/conf_mode/dynamic_dns.py b/src/conf_mode/dynamic_dns.py
deleted file mode 100755
index 426e3d693..000000000
--- a/src/conf_mode/dynamic_dns.py
+++ /dev/null
@@ -1,156 +0,0 @@
-#!/usr/bin/env python3
-#
-# Copyright (C) 2018-2020 VyOS maintainers and contributors
-#
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the GNU General Public License version 2 or later as
-# published by the Free Software Foundation.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
-
-import os
-
-from sys import exit
-
-from vyos.config import Config
-from vyos.configdict import dict_merge
-from vyos.template import render
-from vyos.util import call
-from vyos.xml import defaults
-from vyos import ConfigError
-from vyos import airbag
-airbag.enable()
-
-config_file = r'/run/ddclient/ddclient.conf'
-
-# Mapping of service name to service protocol
-default_service_protocol = {
-    'afraid': 'freedns',
-    'changeip': 'changeip',
-    'cloudflare': 'cloudflare',
-    'dnspark': 'dnspark',
-    'dslreports': 'dslreports1',
-    'dyndns': 'dyndns2',
-    'easydns': 'easydns',
-    'namecheap': 'namecheap',
-    'noip': 'noip',
-    'sitelutions': 'sitelutions',
-    'zoneedit': 'zoneedit1'
-}
-
-def get_config(config=None):
-    if config:
-        conf = config
-    else:
-        conf = Config()
-
-    base_level = ['service', 'dns', 'dynamic']
-    if not conf.exists(base_level):
-        return None
-
-    dyndns = conf.get_config_dict(base_level, key_mangling=('-', '_'), get_first_key=True)
-
-    # We have gathered the dict representation of the CLI, but there are default
-    # options which we need to update into the dictionary retrived.
-    for interface in dyndns['interface']:
-        if 'service' in dyndns['interface'][interface]:
-            # 'Autodetect' protocol used by DynDNS service
-            for service in dyndns['interface'][interface]['service']:
-                if service in default_service_protocol:
-                    dyndns['interface'][interface]['service'][service].update(
-                        {'protocol' : default_service_protocol.get(service)})
-                else:
-                    dyndns['interface'][interface]['service'][service].update(
-                        {'custom': ''})
-
-        if 'rfc2136' in dyndns['interface'][interface]:
-            default_values = defaults(base_level + ['interface', 'rfc2136'])
-            for rfc2136 in dyndns['interface'][interface]['rfc2136']:
-                dyndns['interface'][interface]['rfc2136'][rfc2136] = dict_merge(
-                    default_values, dyndns['interface'][interface]['rfc2136'][rfc2136])
-
-    return dyndns
-
-def verify(dyndns):
-    # bail out early - looks like removal from running config
-    if not dyndns:
-        return None
-
-    # A 'node' corresponds to an interface
-    if 'interface' not in dyndns:
-        return None
-
-    for interface in dyndns['interface']:
-        # RFC2136 - configuration validation
-        if 'rfc2136' in dyndns['interface'][interface]:
-            for rfc2136, config in dyndns['interface'][interface]['rfc2136'].items():
-
-                for tmp in ['record', 'zone', 'server', 'key']:
-                    if tmp not in config:
-                        raise ConfigError(f'"{tmp}" required for rfc2136 based '
-                                          f'DynDNS service on "{interface}"')
-
-                if not os.path.isfile(config['key']):
-                    raise ConfigError(f'"key"-file not found for rfc2136 based '
-                                      f'DynDNS service on "{interface}"')
-
-        # DynDNS service provider - configuration validation
-        if 'service' in dyndns['interface'][interface]:
-            for service, config in dyndns['interface'][interface]['service'].items():
-                error_msg = f'required for DynDNS service "{service}" on "{interface}"'
-                if 'host_name' not in config:
-                    raise ConfigError(f'"host-name" {error_msg}')
-
-                if 'login' not in config:
-                    if service != 'cloudflare' and ('protocol' not in config or config['protocol'] != 'cloudflare'):
-                        raise ConfigError(f'"login" (username) {error_msg}, unless using CloudFlare')
-
-                if 'password' not in config:
-                    raise ConfigError(f'"password" {error_msg}')
-
-                if 'zone' in config:
-                    if service != 'cloudflare' and ('protocol' not in config or config['protocol'] != 'cloudflare'):
-                        raise ConfigError(f'"zone" option only supported with CloudFlare')
-
-                if 'custom' in config:
-                    if 'protocol' not in config:
-                        raise ConfigError(f'"protocol" {error_msg}')
-
-                    if 'server' not in config:
-                        raise ConfigError(f'"server" {error_msg}')
-
-    return None
-
-def generate(dyndns):
-    # bail out early - looks like removal from running config
-    if not dyndns:
-        return None
-
-    render(config_file, 'dynamic-dns/ddclient.conf.j2', dyndns)
-    return None
-
-def apply(dyndns):
-    if not dyndns:
-        call('systemctl stop ddclient.service')
-        if os.path.exists(config_file):
-            os.unlink(config_file)
-    else:
-        call('systemctl restart ddclient.service')
-
-    return None
-
-if __name__ == '__main__':
-    try:
-        c = get_config()
-        verify(c)
-        generate(c)
-        apply(c)
-    except ConfigError as e:
-        print(e)
-        exit(1)
diff --git a/src/etc/systemd/system/ddclient.service.d/override.conf b/src/etc/systemd/system/ddclient.service.d/override.conf
deleted file mode 100644
index 09d929d39..000000000
--- a/src/etc/systemd/system/ddclient.service.d/override.conf
+++ /dev/null
@@ -1,11 +0,0 @@
-[Unit]
-After=
-After=vyos-router.service
-
-[Service]
-WorkingDirectory=
-WorkingDirectory=/run/ddclient
-PIDFile=
-PIDFile=/run/ddclient/ddclient.pid
-ExecStart=
-ExecStart=/usr/bin/ddclient -cache /run/ddclient/ddclient.cache -pid /run/ddclient/ddclient.pid -file /run/ddclient/ddclient.conf
diff --git a/src/migration-scripts/dns-dynamic/0-to-1 b/src/migration-scripts/dns-dynamic/0-to-1
new file mode 100755
index 000000000..cf0983b01
--- /dev/null
+++ b/src/migration-scripts/dns-dynamic/0-to-1
@@ -0,0 +1,104 @@
+#!/usr/bin/env python3
+
+# Copyright (C) 2023 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+# T5144:
+# - migrate "service dns dynamic interface ..."
+#        to "service dns dynamic address ..."
+# - migrate "service dns dynamic interface <interface> use-web ..."
+#        to "service dns dynamic address <address> web-options ..."
+# - migrate "service dns dynamic interface <interface> rfc2136 <config> record ..."
+#        to "service dns dynamic address <address> rfc2136 <config> host-name ..."
+# - migrate "service dns dynamic interface <interface> service <config> login ..."
+#        to "service dns dynamic address <address> service <config> username ..."
+# - apply global 'ipv6-enable' to per <config> 'ip-version: ipv6'
+# - apply service protocol mapping upfront, they are not 'auto-detected' anymore
+
+import sys
+from vyos.configtree import ConfigTree
+
+service_protocol_mapping = {
+    'afraid': 'freedns',
+    'changeip': 'changeip',
+    'cloudflare': 'cloudflare',
+    'dnspark': 'dnspark',
+    'dslreports': 'dslreports1',
+    'dyndns': 'dyndns2',
+    'easydns': 'easydns',
+    'namecheap': 'namecheap',
+    'noip': 'noip',
+    'sitelutions': 'sitelutions',
+    'zoneedit': 'zoneedit1'
+}
+
+if (len(sys.argv) < 1):
+    print("Must specify file name!")
+    sys.exit(1)
+
+file_name = sys.argv[1]
+
+with open(file_name, 'r') as f:
+    config_file = f.read()
+
+config = ConfigTree(config_file)
+
+old_base_path = ['service', 'dns', 'dynamic', 'interface']
+new_base_path = ['service', 'dns', 'dynamic', 'address']
+
+if not config.exists(old_base_path):
+    # Nothing to do
+    sys.exit(0)
+
+# Migrate "service dns dynamic interface"
+#      to "service dns dynamic address"
+config.rename(old_base_path, new_base_path[-1])
+
+for address in config.list_nodes(new_base_path):
+    # Migrate "service dns dynamic interface <interface> rfc2136 <config> record"
+    #      to "service dns dynamic address <address> rfc2136 <config> host-name"
+    if config.exists(new_base_path + [address, 'rfc2136']):
+        for rfc_cfg in config.list_nodes(new_base_path + [address, 'rfc2136']):
+            if config.exists(new_base_path + [address, 'rfc2136', rfc_cfg, 'record']):
+                config.rename(new_base_path + [address, 'rfc2136', rfc_cfg, 'record'], 'host-name')
+
+    # Migrate "service dns dynamic interface <interface> service <config> login"
+    #      to "service dns dynamic address <address> service <config> username"
+    if config.exists(new_base_path + [address, 'service']):
+        for svc_cfg in config.list_nodes(new_base_path + [address, 'service']):
+            if config.exists(new_base_path + [address, 'service', svc_cfg, 'login']):
+                config.rename(new_base_path + [address, 'service', svc_cfg, 'login'], 'username')
+            # Apply global 'ipv6-enable' to per <config> 'ip-version: ipv6'
+            if config.exists(new_base_path + [address, 'ipv6-enable']):
+                config.set(new_base_path + [address, 'service', svc_cfg, 'ip-version'],
+                           value='ipv6', replace=False)
+                config.delete(new_base_path + [address, 'ipv6-enable'])
+            # Apply service protocol mapping upfront, they are not 'auto-detected' anymore
+            if svc_cfg in service_protocol_mapping:
+                config.set(new_base_path + [address, 'service', svc_cfg, 'protocol'],
+                           value=service_protocol_mapping.get(svc_cfg), replace=False)
+
+    # Migrate "service dns dynamic interface <interface> use-web"
+    #      to "service dns dynamic address <address> web-options"
+    # Also, rename <address> to 'web' literal for backward compatibility
+    if config.exists(new_base_path + [address, 'use-web']):
+        config.rename(new_base_path + [address], 'web')
+        config.rename(new_base_path + ['web', 'use-web'], 'web-options')
+
+try:
+    with open(file_name, 'w') as f:
+        f.write(config.to_string())
+except OSError as e:
+    print("Failed to save the modified config: {}".format(e))
+    sys.exit(1)
diff --git a/src/op_mode/dynamic_dns.py b/src/op_mode/dns_dynamic.py
similarity index 100%
rename from src/op_mode/dynamic_dns.py
rename to src/op_mode/dns_dynamic.py