diff --git a/data/templates/dns-dynamic/ddclient.conf.j2 b/data/templates/dns-dynamic/ddclient.conf.j2
index b209c8c810..d3f71968af 100644
--- a/data/templates/dns-dynamic/ddclient.conf.j2
+++ b/data/templates/dns-dynamic/ddclient.conf.j2
@@ -21,11 +21,6 @@ if{{ ipv }}={{ address }}, \
{{ host }}
{% endmacro %}
### Autogenerated by service_dns_dynamic.py ###
-ssl=yes
-{# ddclient default (web=dyndns) doesn't support ssl and results in process lockup #}
-web=googledomains
-{# ddclient default (use=ip) results in confusing warning message in log #}
-use=no
{% if name is vyos_defined %}
{% for service, config in name.items() %}
@@ -34,13 +29,9 @@ use=no
# {{ config.description }}
{% endif %}
{% for host in config.host_name if config.host_name is vyos_defined %}
-{# ip_suffixes can be either of ['v4'], ['v6'], ['v4', 'v6'] for all protocols except 'nsupdate'
- ip_suffixes must be [''] for nsupdate since it doesn't support usevX/wantipvX yet #}
+{# ip_suffixes can be either of ['v4'], ['v6'], ['v4', 'v6'] for all protocols #}
{% set ip_suffixes = ['v4', 'v6'] if config.ip_version == 'both'
- else ([config.ip_version[2:]] if config.protocol != 'nsupdate'
- else ['']) %}
-{% set password = config.key if config.protocol == 'nsupdate'
- else config.password %}
+ else [config.ip_version[2:]] %}
{% set address = 'web' if config.address.web is vyos_defined
else config.address.interface %}
{% set web_options = config.address.web | default({}) %}
@@ -48,7 +39,7 @@ use=no
# Web service dynamic DNS configuration for {{ service }}: [{{ config.protocol }}, {{ host }}]
{{ render_config(host, address, web_options, ip_suffixes,
protocol=config.protocol, server=config.server, zone=config.zone,
- login=config.username, password=password, ttl=config.ttl,
+ login=config.username, password=config.password, ttl=config.ttl,
min_interval=config.wait_time, max_interval=config.expiry_time) }}
{% endfor %}
{% endfor %}
diff --git a/interface-definitions/include/version/dns-dynamic-version.xml.i b/interface-definitions/include/version/dns-dynamic-version.xml.i
index 346385ccbd..98123035d1 100644
--- a/interface-definitions/include/version/dns-dynamic-version.xml.i
+++ b/interface-definitions/include/version/dns-dynamic-version.xml.i
@@ -1,3 +1,3 @@
-
+
diff --git a/interface-definitions/service_dns_dynamic.xml.in b/interface-definitions/service_dns_dynamic.xml.in
index 99103ec736..9ed0adfea1 100644
--- a/interface-definitions/service_dns_dynamic.xml.in
+++ b/interface-definitions/service_dns_dynamic.xml.in
@@ -123,7 +123,7 @@
- DNS zone to be updated
+ DNS zone or root domain to be updated
txt
Name of DNS zone
diff --git a/smoketest/scripts/cli/test_service_dns_dynamic.py b/smoketest/scripts/cli/test_service_dns_dynamic.py
index c68b831af0..79fe378014 100755
--- a/smoketest/scripts/cli/test_service_dns_dynamic.py
+++ b/smoketest/scripts/cli/test_service_dns_dynamic.py
@@ -40,6 +40,7 @@
ttl = '300'
interface = 'eth0'
+
class TestServiceDDNS(VyOSUnitTestSHIM.TestCase):
@classmethod
def setUpClass(cls):
@@ -64,9 +65,11 @@ def tearDown(self):
# IPv4 standard DDNS service configuration
def test_01_dyndns_service_standard(self):
- services = {'cloudflare': {'protocol': 'cloudflare'},
- 'freedns': {'protocol': 'freedns', 'username': username},
- 'zoneedit': {'protocol': 'zoneedit1', 'username': username}}
+ services = {
+ 'cloudflare': {'protocol': 'cloudflare'},
+ 'freedns': {'protocol': 'freedns', 'username': username},
+ 'zoneedit': {'protocol': 'zoneedit1', 'username': username},
+ }
for svc, details in services.items():
self.cli_set(name_path + [svc, 'address', 'interface', interface])
@@ -102,7 +105,7 @@ def test_01_dyndns_service_standard(self):
ddclient_conf = cmd(f'sudo cat {DDCLIENT_CONF}')
self.assertIn(f'usev4=ifv4', ddclient_conf)
self.assertIn(f'ifv4={interface}', ddclient_conf)
- self.assertIn(f'password=\'{password}\'', ddclient_conf)
+ self.assertIn(f"password='{password}'", ddclient_conf)
# Check default interval of 300 seconds
systemd_override = read_file(DDCLIENT_SYSTEMD_UNIT)
@@ -152,7 +155,7 @@ def test_02_dyndns_service_ipv6(self):
self.assertIn(f'protocol={proto}', ddclient_conf)
self.assertIn(f'server={server}', ddclient_conf)
self.assertIn(f'login={username}', ddclient_conf)
- self.assertIn(f'password=\'{password}\'', ddclient_conf)
+ self.assertIn(f"password='{password}'", ddclient_conf)
self.assertIn(f'min-interval={wait_time}', ddclient_conf)
self.assertIn(f'max-interval={expiry_time_good}', ddclient_conf)
@@ -162,9 +165,11 @@ def test_02_dyndns_service_ipv6(self):
# IPv4+IPv6 dual DDNS service configuration
def test_03_dyndns_service_dual_stack(self):
- services = {'cloudflare': {'protocol': 'cloudflare', 'zone': zone},
- 'freedns': {'protocol': 'freedns', 'username': username},
- 'google': {'protocol': 'googledomains', 'username': username}}
+ services = {
+ 'cloudflare': {'protocol': 'cloudflare', 'zone': zone},
+ 'freedns': {'protocol': 'freedns', 'username': username},
+ 'changeip': {'protocol': 'changeip', 'username': username},
+ }
ip_version = 'both'
for name, details in services.items():
@@ -174,7 +179,7 @@ def test_03_dyndns_service_dual_stack(self):
for opt, value in details.items():
self.cli_set(name_path + [name, opt, value])
- # Dual stack is supported by 'cloudflare' and 'freedns' but not 'googledomains'
+ # Dual stack is supported by 'cloudflare' and 'freedns' but not 'changeip'
# exception is raised for unsupported ones
self.cli_set(name_path + [name, 'ip-version', ip_version])
if details['protocol'] not in ['cloudflare', 'freedns']:
@@ -189,13 +194,15 @@ def test_03_dyndns_service_dual_stack(self):
ddclient_conf = cmd(f'sudo cat {DDCLIENT_CONF}')
if details['protocol'] not in ['cloudflare', 'freedns']:
self.assertIn(f'usev4=ifv4', ddclient_conf)
+ self.assertNotIn(f'usev6=ifv6', ddclient_conf)
self.assertIn(f'ifv4={interface}', ddclient_conf)
+ self.assertNotIn(f'ifv6={interface}', ddclient_conf)
else:
self.assertIn(f'usev4=ifv4', ddclient_conf)
self.assertIn(f'usev6=ifv6', ddclient_conf)
self.assertIn(f'ifv4={interface}', ddclient_conf)
self.assertIn(f'ifv6={interface}', ddclient_conf)
- self.assertIn(f'password=\'{password}\'', ddclient_conf)
+ self.assertIn(f"password='{password}'", ddclient_conf)
for opt in details.keys():
if opt == 'username':
@@ -205,6 +212,9 @@ def test_03_dyndns_service_dual_stack(self):
tmp = details[opt]
self.assertIn(f'{opt}={tmp}', ddclient_conf)
+ # cleanup for next iteration
+ self.cli_delete(name_path + [name])
+
def test_04_dyndns_rfc2136(self):
# Check if DDNS service can be configured and runs
svc_path = name_path + ['vyos']
@@ -220,18 +230,21 @@ def test_04_dyndns_rfc2136(self):
self.cli_set(svc_path + ['key', key_file.name])
self.cli_set(svc_path + ['ttl', ttl])
self.cli_set(svc_path + ['host-name', hostname])
+ self.cli_set(svc_path + ['ip-version', 'both'])
# commit changes
self.cli_commit()
# Check some generating config parameters
ddclient_conf = cmd(f'sudo cat {DDCLIENT_CONF}')
- self.assertIn(f'use=if', ddclient_conf)
- self.assertIn(f'if={interface}', ddclient_conf)
+ self.assertIn(f'usev4=ifv4', ddclient_conf)
+ self.assertIn(f'usev6=ifv6', ddclient_conf)
+ self.assertIn(f'ifv4={interface}', ddclient_conf)
+ self.assertIn(f'ifv6={interface}', ddclient_conf)
self.assertIn(f'protocol={proto}', ddclient_conf)
self.assertIn(f'server={server}', ddclient_conf)
self.assertIn(f'zone={zone}', ddclient_conf)
- self.assertIn(f'password=\'{key_file.name}\'', ddclient_conf)
+ self.assertIn(f"password='{key_file.name}'", ddclient_conf)
self.assertIn(f'ttl={ttl}', ddclient_conf)
def test_05_dyndns_hostname(self):
@@ -256,7 +269,7 @@ def test_05_dyndns_hostname(self):
self.assertIn(f'protocol={proto}', ddclient_conf)
self.assertIn(f'server={server}', ddclient_conf)
self.assertIn(f'login={username}', ddclient_conf)
- self.assertIn(f'password=\'{password}\'', ddclient_conf)
+ self.assertIn(f"password='{password}'", ddclient_conf)
self.assertIn(f'{name}', ddclient_conf)
def test_06_dyndns_web_options(self):
@@ -294,10 +307,10 @@ def test_06_dyndns_web_options(self):
ddclient_conf = cmd(f'sudo cat {DDCLIENT_CONF}')
self.assertIn(f'usev4=webv4', ddclient_conf)
self.assertIn(f'webv4={web_url}', ddclient_conf)
- self.assertIn(f'webv4-skip=\'{web_skip}\'', ddclient_conf)
+ self.assertIn(f"webv4-skip='{web_skip}'", ddclient_conf)
self.assertIn(f'protocol={proto}', ddclient_conf)
self.assertIn(f'zone={zone}', ddclient_conf)
- self.assertIn(f'password=\'{password}\'', ddclient_conf)
+ self.assertIn(f"password='{password}'", ddclient_conf)
self.assertIn(f'{hostname}', ddclient_conf)
def test_07_dyndns_dynamic_interface(self):
@@ -326,7 +339,7 @@ def test_07_dyndns_dynamic_interface(self):
self.assertIn(f'protocol={proto}', ddclient_conf)
self.assertIn(f'server={server}', ddclient_conf)
self.assertIn(f'login={username}', ddclient_conf)
- self.assertIn(f'password=\'{password}\'', ddclient_conf)
+ self.assertIn(f"password='{password}'", ddclient_conf)
self.assertIn(f'{hostname}', ddclient_conf)
def test_08_dyndns_vrf(self):
@@ -350,9 +363,12 @@ def test_08_dyndns_vrf(self):
# Check for process in VRF
systemd_override = read_file(DDCLIENT_SYSTEMD_UNIT)
- self.assertIn(f'ExecStart=ip vrf exec {vrf_name} /usr/bin/ddclient ' \
- f'--file {DDCLIENT_CONF} --cache {DDCLIENT_CONF.replace("conf", "cache")} ' \
- f'--foreground --daemon {default_interval}', systemd_override)
+ self.assertIn(
+ f'ExecStart=ip vrf exec {vrf_name} /usr/bin/ddclient '
+ f'--file {DDCLIENT_CONF} --cache {DDCLIENT_CONF.replace("conf", "cache")} '
+ f'--foreground --daemon {default_interval}',
+ systemd_override,
+ )
# Check for process in VRF
proc = cmd(f'ip vrf pids {vrf_name}')
@@ -361,5 +377,6 @@ def test_08_dyndns_vrf(self):
# Cleanup VRF
self.cli_delete(['vrf', 'name', vrf_name])
+
if __name__ == '__main__':
unittest.main(verbosity=2, failfast=VyOSUnitTestSHIM.TestCase.debug_on())
diff --git a/src/completion/list_ddclient_protocols.sh b/src/completion/list_ddclient_protocols.sh
index 0c8c2712de..d92239270a 100755
--- a/src/completion/list_ddclient_protocols.sh
+++ b/src/completion/list_ddclient_protocols.sh
@@ -14,4 +14,4 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
-echo -n $(ddclient -list-protocols | grep -vE 'cloudns|porkbun')
+echo -n $(ddclient --list-protocols | grep -vE 'cloudns|directnic|emailonly')
diff --git a/src/conf_mode/service_dns_dynamic.py b/src/conf_mode/service_dns_dynamic.py
index b321d5f51b..53561e2469 100755
--- a/src/conf_mode/service_dns_dynamic.py
+++ b/src/conf_mode/service_dns_dynamic.py
@@ -27,33 +27,89 @@
from vyos.utils.network import interface_exists
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_necessary = ['cloudflare', 'digitalocean', 'godaddy', 'hetzner', 'gandi',
- 'nfsn', 'nsupdate']
-zone_supported = zone_necessary + ['dnsexit2', 'zoneedit1']
+zone_necessary = [
+ 'cloudflare',
+ 'digitalocean',
+ 'godaddy',
+ 'hetzner',
+ 'gandi',
+ 'nfsn',
+ 'nsupdate',
+]
+zone_supported = zone_necessary + ['dnsexit2', 'porkbun', 'zoneedit1']
# Protocols that do not require username
-username_unnecessary = ['1984', 'cloudflare', 'cloudns', 'digitalocean', 'dnsexit2',
- 'duckdns', 'freemyip', 'hetzner', 'keysystems', 'njalla',
- 'nsupdate', 'regfishde']
+username_unnecessary = [
+ '1984',
+ 'cloudflare',
+ 'cloudns',
+ 'ddns.fm',
+ 'digitalocean',
+ 'dnsexit2',
+ 'duckdns',
+ 'freemyip',
+ 'gandi',
+ 'he.net',
+ 'hetzner',
+ 'keysystems',
+ 'njalla',
+ 'nsupdate',
+ 'regfishde',
+]
# Protocols that support TTL
-ttl_supported = ['cloudflare', 'dnsexit2', 'gandi', 'hetzner', 'godaddy', 'nfsn',
- 'nsupdate']
+ttl_supported = [
+ 'cloudflare',
+ 'dnsexit2',
+ 'gandi',
+ 'hetzner',
+ 'godaddy',
+ 'nfsn',
+ 'nsupdate',
+ 'porkbun',
+]
# Protocols that support both IPv4 and IPv6
-dualstack_supported = ['cloudflare', 'digitalocean', 'dnsexit2', 'duckdns',
- 'dyndns2', 'easydns', 'freedns', 'hetzner', 'infomaniak',
- 'njalla']
+dualstack_supported = [
+ 'cloudflare',
+ 'ddns.fm',
+ 'digitalocean',
+ 'dnsexit2',
+ 'domeneshop',
+ 'duckdns',
+ 'dyndns2',
+ 'easydns',
+ 'freedns',
+ 'gandi',
+ 'godaddy',
+ 'he.net',
+ 'hetzner',
+ 'infomaniak',
+ 'inwx',
+ 'mythicdyn',
+ 'njalla',
+ 'noip',
+ 'nsupdate',
+ 'porkbun',
+ 'regfishde',
+]
# dyndns2 protocol in ddclient honors dual stack for selective servers
# because of the way it is implemented in ddclient
-dyndns_dualstack_servers = ['members.dyndns.org', 'dynv6.com']
+dyndns_dualstack_servers = [
+ 'app.luadns.com',
+ 'dynv6.com',
+ 'members.dyndns.org',
+ 'update.dedyn.io',
+]
+
def get_config(config=None):
if config:
@@ -65,14 +121,18 @@ def get_config(config=None):
if not conf.exists(base):
return None
- dyndns = conf.get_config_dict(base, key_mangling=('-', '_'),
- no_tag_node_value_mangle=True,
- get_first_key=True,
- with_recursive_defaults=True)
+ dyndns = conf.get_config_dict(
+ base,
+ key_mangling=('-', '_'),
+ no_tag_node_value_mangle=True,
+ get_first_key=True,
+ with_recursive_defaults=True,
+ )
dyndns['config_file'] = config_file
return dyndns
+
def verify(dyndns):
# bail out early - looks like removal from running config
if not dyndns or 'name' not in dyndns:
@@ -88,11 +148,15 @@ def verify(dyndns):
raise ConfigError(f'"{field.replace("_", "-")}" {error_msg_req}')
if not any(x in config['address'] for x in ['interface', 'web']):
- raise ConfigError(f'Either "interface" or "web" {error_msg_req} '
- f'with protocol "{config["protocol"]}"')
+ raise ConfigError(
+ f'Either "interface" or "web" {error_msg_req} '
+ f'with protocol "{config["protocol"]}"'
+ )
if all(x in config['address'] for x in ['interface', 'web']):
- raise ConfigError(f'Both "interface" and "web" at the same time {error_msg_uns} '
- f'with protocol "{config["protocol"]}"')
+ raise ConfigError(
+ f'Both "interface" and "web" at the same time {error_msg_uns} '
+ f'with protocol "{config["protocol"]}"'
+ )
# If dyndns address is an interface, ensure that the interface exists
# and warn if a non-active dynamic interface is used
@@ -101,22 +165,23 @@ def verify(dyndns):
# exclude check interface for dynamic interfaces
if tmp.match(config['address']['interface']):
if not interface_exists(config['address']['interface']):
- Warning(f'Interface "{config["address"]["interface"]}" does not exist yet and '
- f'cannot be used for Dynamic DNS service "{service}" until it is up!')
+ Warning(
+ f'Interface "{config["address"]["interface"]}" does not exist yet and '
+ f'cannot be used for Dynamic DNS service "{service}" until it is up!'
+ )
else:
verify_interface_exists(dyndns, config['address']['interface'])
if 'web' in config['address']:
# If 'skip' is specified, 'url' is required as well
- if 'skip' in config['address']['web'] and 'url' not in config['address']['web']:
- raise ConfigError(f'"url" along with "skip" {error_msg_req} '
- f'with protocol "{config["protocol"]}"')
- if 'url' in config['address']['web']:
- # Warn if using checkip.dyndns.org, as it does not support HTTPS
- # See: https://github.com/ddclient/ddclient/issues/597
- if re.search("^(https?://)?checkip\.dyndns\.org", config['address']['web']['url']):
- Warning(f'"checkip.dyndns.org" does not support HTTPS requests for IP address '
- f'lookup. Please use a different IP address lookup service.')
+ if (
+ 'skip' in config['address']['web']
+ and 'url' not in config['address']['web']
+ ):
+ raise ConfigError(
+ f'"url" along with "skip" {error_msg_req} '
+ f'with protocol "{config["protocol"]}"'
+ )
# RFC2136 uses 'key' instead of 'password'
if config['protocol'] != 'nsupdate' and 'password' not in config:
@@ -125,47 +190,94 @@ def verify(dyndns):
# Other RFC2136 specific configuration validation
if config['protocol'] == 'nsupdate':
if 'password' in config:
- raise ConfigError(f'"password" {error_msg_uns} with protocol "{config["protocol"]}"')
+ raise ConfigError(
+ f'"password" {error_msg_uns} with protocol "{config["protocol"]}"'
+ )
for field in ['server', 'key']:
if field not in config:
- raise ConfigError(f'"{field}" {error_msg_req} with protocol "{config["protocol"]}"')
+ raise ConfigError(
+ f'"{field}" {error_msg_req} with protocol "{config["protocol"]}"'
+ )
if config['protocol'] in zone_necessary and 'zone' not in config:
- raise ConfigError(f'"zone" {error_msg_req} with protocol "{config["protocol"]}"')
+ raise ConfigError(
+ f'"zone" {error_msg_req} with protocol "{config["protocol"]}"'
+ )
if config['protocol'] not in zone_supported and 'zone' in config:
- raise ConfigError(f'"zone" {error_msg_uns} with protocol "{config["protocol"]}"')
+ raise ConfigError(
+ f'"zone" {error_msg_uns} with protocol "{config["protocol"]}"'
+ )
if config['protocol'] not in username_unnecessary and 'username' not in config:
- raise ConfigError(f'"username" {error_msg_req} with protocol "{config["protocol"]}"')
+ raise ConfigError(
+ f'"username" {error_msg_req} with protocol "{config["protocol"]}"'
+ )
if config['protocol'] not in ttl_supported and 'ttl' in config:
- raise ConfigError(f'"ttl" {error_msg_uns} with protocol "{config["protocol"]}"')
+ raise ConfigError(
+ f'"ttl" {error_msg_uns} with protocol "{config["protocol"]}"'
+ )
if config['ip_version'] == 'both':
if config['protocol'] not in dualstack_supported:
- raise ConfigError(f'Both IPv4 and IPv6 at the same time {error_msg_uns} '
- f'with protocol "{config["protocol"]}"')
+ raise ConfigError(
+ f'Both IPv4 and IPv6 at the same time {error_msg_uns} '
+ f'with protocol "{config["protocol"]}"'
+ )
# dyndns2 protocol in ddclient honors dual stack only for dyn.com (dyndns.org)
- if config['protocol'] == 'dyndns2' and 'server' in config and config['server'] not in dyndns_dualstack_servers:
- raise ConfigError(f'Both IPv4 and IPv6 at the same time {error_msg_uns} '
- f'for "{config["server"]}" with protocol "{config["protocol"]}"')
-
- if {'wait_time', 'expiry_time'} <= config.keys() and int(config['expiry_time']) < int(config['wait_time']):
- raise ConfigError(f'"expiry-time" must be greater than "wait-time" for '
- f'Dynamic DNS service "{service}"')
+ if (
+ config['protocol'] == 'dyndns2'
+ and 'server' in config
+ and config['server'] not in dyndns_dualstack_servers
+ ):
+ raise ConfigError(
+ f'Both IPv4 and IPv6 at the same time {error_msg_uns} '
+ f'for "{config["server"]}" with protocol "{config["protocol"]}"'
+ )
+
+ if {'wait_time', 'expiry_time'} <= config.keys() and int(
+ config['expiry_time']
+ ) < int(config['wait_time']):
+ raise ConfigError(
+ f'"expiry-time" must be greater than "wait-time" for '
+ f'Dynamic DNS service "{service}"'
+ )
return None
+
def generate(dyndns):
# bail out early - looks like removal from running config
if not dyndns or 'name' not in dyndns:
return None
+ # Adjust protocol specific keys
+ for name in dyndns['name']:
+ # nsupdate (RFC2136) uses:
+ # - 'password' in ddclient.conf instead of 'key' in vyos conf
+ if dyndns['name'][name]['protocol'] == 'nsupdate':
+ dyndns['name'][name]['password'] = dyndns['name'][name].pop('key')
+
+ # porkbun uses:
+ # - 'root-domain' in ddclient.conf instead of 'zone' in vyos conf
+ # - 'apikey' in ddclient.conf instead of 'username' in vyos conf
+ # - 'secretapikey' in ddclient.conf instead of 'password' in vyos conf
+ if dyndns['name'][name]['protocol'] == 'porkbun':
+ dyndns['name'][name]['apikey'] = dyndns['name'][name].pop('username')
+ dyndns['name'][name]['secretapikey'] = dyndns['name'][name].pop('password')
+ if 'zone' in dyndns['name'][name]:
+ dyndns['name'][name]['root-domain'] = dyndns['name'][name].pop('zone')
+
+ # Gandi API key is deprecated, enforce using personal access token
+ if dyndns['name'][name]['protocol'] == 'gandi':
+ dyndns['name'][name]['use-personal-access-token'] = 'yes'
+
render(config_file, 'dns-dynamic/ddclient.conf.j2', dyndns, permission=0o600)
render(systemd_override, 'dns-dynamic/override.conf.j2', dyndns)
return None
+
def apply(dyndns):
systemd_service = 'ddclient.service'
# Reload systemd manager configuration
@@ -181,6 +293,7 @@ def apply(dyndns):
return None
+
if __name__ == '__main__':
try:
c = get_config()
diff --git a/src/migration-scripts/dns-dynamic/4-to-5 b/src/migration-scripts/dns-dynamic/4-to-5
new file mode 100644
index 0000000000..3873c00afe
--- /dev/null
+++ b/src/migration-scripts/dns-dynamic/4-to-5
@@ -0,0 +1,53 @@
+# Copyright VyOS maintainers and contributors
+#
+# This library is free software; you can redistribute it and/or
+# modify it under the terms of the GNU Lesser General Public
+# License as published by the Free Software Foundation; either
+# version 2.1 of the License, or (at your option) any later version.
+#
+# This library 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
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with this library. If not, see .
+
+# T6981:
+# - remove "service dns dynamic name protocol googledomains"
+# - remove "service dns dynamic name protocol woima"
+# - remove "service dns dynamic name address web url ..."
+# when url is https://domains.google.com/*
+
+import re
+from vyos.base import Warning
+from vyos.configtree import ConfigTree
+
+base_path = ['service', 'dns', 'dynamic', 'name']
+
+def migrate(config: ConfigTree) -> None:
+ if not config.exists(base_path):
+ # Nothing to do
+ return
+
+ for service in config.list_nodes(base_path):
+
+ service_path = base_path + [service]
+
+ # Remove configurations using protocol 'googledomains' and 'woima'
+ if config.exists(service_path + ['protocol']):
+ protocol = config.return_value(service_path + ['protocol'])
+ if protocol in ['googledomains', 'woima']:
+ Warning(f'Removing {service} using protocol "{protocol}" because the service has been shutdown')
+ config.delete(service_path)
+
+ # Look for 'address web' with 'url' set to 'googledomains' or 'https://domains.google.com'
+ # and remove them so that ddclient defaults apply for 'address web'
+ if config.exists(service_path + ['address', 'web']):
+ web_addr_path = service_path + ['address', 'web']
+ if config.exists(web_addr_path + ['url']):
+ url = config.return_value(web_addr_path + ['url'])
+ if url == 'googledomains' or re.search(r'^(https?://)?domains\.google\.com', url):
+ config.delete(web_addr_path + ['url'])
+ if config.exists(web_addr_path + ['skip']):
+ config.delete(web_addr_path + ['skip'])
diff --git a/src/op_mode/dns.py b/src/op_mode/dns.py
index 7c9f769f1f..da064e22f7 100755
--- a/src/op_mode/dns.py
+++ b/src/op_mode/dns.py
@@ -24,7 +24,6 @@
from tabulate import tabulate
from vyos.configquery import ConfigTreeQuery
from vyos.utils.process import cmd, rc_cmd
-from vyos.template import is_ipv4, is_ipv6
_dynamic_cache_file = r'/run/ddclient/ddclient.cache'
@@ -84,20 +83,9 @@ def _get_dynamic_host_records_raw() -> dict:
# we pick up the ones we are interested in
for kvraw in line.split(' ')[0].split(','):
k, v = kvraw.split('=')
- if k in list(_dynamic_status_columns.keys()) + ['ip', 'status']: # ip and status are legacy keys
+ if k in list(_dynamic_status_columns.keys()):
props[k] = v
- # Extract IPv4 and IPv6 address and status from legacy keys
- # Dual-stack isn't supported in legacy format, 'ip' and 'status' are for one of IPv4 or IPv6
- if 'ip' in props:
- if is_ipv4(props['ip']):
- props['ipv4'] = props['ip']
- props['status-ipv4'] = props['status']
- elif is_ipv6(props['ip']):
- props['ipv6'] = props['ip']
- props['status-ipv6'] = props['status']
- del props['ip']
-
# Convert mtime to human readable format
if 'mtime' in props:
props['mtime'] = time.strftime(
diff --git a/src/validators/ddclient-protocol b/src/validators/ddclient-protocol
index 0d28039d3d..bcb2ab8ed2 100755
--- a/src/validators/ddclient-protocol
+++ b/src/validators/ddclient-protocol
@@ -14,7 +14,7 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
-ddclient -list-protocols | grep -vE 'cloudns|porkbun' | grep -qw $1
+ddclient --list-protocols | grep -vE 'cloudns|directnic|emailonly' | grep -qw $1
if [ $? -gt 0 ]; then
echo "Error: $1 is not a valid protocol, please choose from the supported list of protocols"