From 7cf47a389edecc6c05590468a10b6aa916881cc5 Mon Sep 17 00:00:00 2001 From: Indrajit Raychaudhuri Date: Wed, 25 Dec 2024 22:01:13 -0600 Subject: ddclient: T6981: Remove defunct googledomains Since googledomains (domains.google.com) has been shut down, we need to remove any configuration path that refers to googledomains. 1. Remove 'googledomains' as the default web resolver in ddclient.conf.j2 2. Apply migration to remove googledomains from dynamic DNS service configurations. The migration is also necessary to undo a previous migration where we added 'googledomains' as default web resolver to work around lack of tls (ssl) support with default web resolver 'dyndns' (checkip.dyndns.org) in ddclient v3.x. --- src/migration-scripts/dns-dynamic/4-to-5 | 52 ++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 src/migration-scripts/dns-dynamic/4-to-5 (limited to 'src') 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 000000000..3a055a122 --- /dev/null +++ b/src/migration-scripts/dns-dynamic/4-to-5 @@ -0,0 +1,52 @@ +# 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 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' + if config.exists(service_path + ['protocol']): + protocol = config.return_value(service_path + ['protocol']) + if protocol == 'googledomains': + Warning(f'Removing {service} using protocol "googledomains" 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']) -- cgit v1.2.3 From e0c4655c6c06f6fd24adddea7371c6d68615c54c Mon Sep 17 00:00:00 2001 From: Indrajit Raychaudhuri Date: Tue, 7 Jan 2025 11:18:12 -0600 Subject: ddclient: T6981: Remove defunct woima Since woima (woima.fi) has been shut down, we need to remove any configuration path that refers to woima. Apply migration to remove woima from dynamic DNS service configurations. --- src/migration-scripts/dns-dynamic/4-to-5 | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/migration-scripts/dns-dynamic/4-to-5 b/src/migration-scripts/dns-dynamic/4-to-5 index 3a055a122..3873c00af 100644 --- a/src/migration-scripts/dns-dynamic/4-to-5 +++ b/src/migration-scripts/dns-dynamic/4-to-5 @@ -15,6 +15,7 @@ # 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/* @@ -33,11 +34,11 @@ def migrate(config: ConfigTree) -> None: service_path = base_path + [service] - # Remove configurations using protocol 'googledomains' + # Remove configurations using protocol 'googledomains' and 'woima' if config.exists(service_path + ['protocol']): protocol = config.return_value(service_path + ['protocol']) - if protocol == 'googledomains': - Warning(f'Removing {service} using protocol "googledomains" because the service has been shutdown') + 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' -- cgit v1.2.3 From f1907b023ae3363f32b21d1515a6b47174220b74 Mon Sep 17 00:00:00 2001 From: Indrajit Raychaudhuri Date: Wed, 25 Dec 2024 23:16:07 -0600 Subject: ddclient: T6981: No need to warn for checkip.dyndns.org In ddclient v4, there is no need to warn for the usage of checkip.dyndns.org even if it does not support http over tls. This is because the protocol specifier (http://) is honored even when tls (ssl) is enabled. --- src/conf_mode/service_dns_dynamic.py | 6 ------ 1 file changed, 6 deletions(-) (limited to 'src') diff --git a/src/conf_mode/service_dns_dynamic.py b/src/conf_mode/service_dns_dynamic.py index b321d5f51..08ee80a6b 100755 --- a/src/conf_mode/service_dns_dynamic.py +++ b/src/conf_mode/service_dns_dynamic.py @@ -111,12 +111,6 @@ def verify(dyndns): 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.') # RFC2136 uses 'key' instead of 'password' if config['protocol'] != 'nsupdate' and 'password' not in config: -- cgit v1.2.3 From 18a44a804ba0c4d70760d927e44b3ded0ab2524a Mon Sep 17 00:00:00 2001 From: Indrajit Raychaudhuri Date: Sun, 29 Dec 2024 19:47:47 -0600 Subject: ddclient: T6981: Remove processing legacy cache fields In ddclient v4, the 'ip' and 'status' fields are no longer supported. They were used to store IPv4 addresses and their statuses in a backward compatible way in ddclient v3. --- src/op_mode/dns.py | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) (limited to 'src') diff --git a/src/op_mode/dns.py b/src/op_mode/dns.py index ee213d5da..a495deff8 100755 --- a/src/op_mode/dns.py +++ b/src/op_mode/dns.py @@ -24,7 +24,6 @@ import vyos.opmode from tabulate import tabulate from vyos.configquery import ConfigTreeQuery from vyos.utils.process import cmdl, 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( -- cgit v1.2.3 From 65c8d1538d0fa68d4e7306aa152cdc39ba082b6b Mon Sep 17 00:00:00 2001 From: Indrajit Raychaudhuri Date: Mon, 30 Dec 2024 19:03:34 -0600 Subject: ddclient: T6981: Move nsupdate key mapping to Python Relocate key mapping for 'nsupdate' (rfc2136) from Jinja template to Python. This keeps the template simpler and allows for more complex dict handling in Python. --- data/templates/dns-dynamic/ddclient.conf.j2 | 10 ++++------ src/conf_mode/service_dns_dynamic.py | 7 +++++++ 2 files changed, 11 insertions(+), 6 deletions(-) (limited to 'src') diff --git a/data/templates/dns-dynamic/ddclient.conf.j2 b/data/templates/dns-dynamic/ddclient.conf.j2 index 98d1584a1..fd50a529d 100644 --- a/data/templates/dns-dynamic/ddclient.conf.j2 +++ b/data/templates/dns-dynamic/ddclient.conf.j2 @@ -29,19 +29,17 @@ if{{ ipv }}={{ address }}, \ # {{ 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 #} -{% set ip_suffixes = ['v4', 'v6'] if config.ip_version == 'both' - else [config.ip_version[2:]] %} -{% set password = config.key if config.protocol == 'nsupdate' - else config.password %} {% set address = 'web' if config.address.web is vyos_defined else config.address.interface %} {% set web_options = config.address.web | default({}) %} +{# 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:]] %} # 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/src/conf_mode/service_dns_dynamic.py b/src/conf_mode/service_dns_dynamic.py index 08ee80a6b..682daa8a2 100755 --- a/src/conf_mode/service_dns_dynamic.py +++ b/src/conf_mode/service_dns_dynamic.py @@ -156,6 +156,13 @@ def generate(dyndns): 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') + render(config_file, 'dns-dynamic/ddclient.conf.j2', dyndns, permission=0o600) render(systemd_override, 'dns-dynamic/override.conf.j2', dyndns) return None -- cgit v1.2.3 From 5faf2bd03d156df6fdd09ab8491266a5100ddea1 Mon Sep 17 00:00:00 2001 From: Indrajit Raychaudhuri Date: Wed, 25 Dec 2024 23:17:49 -0600 Subject: ddclient: T6981: Block additional unsupported protocols In ddclient v4, some new supported protocols, viz., directnic, emailonly have configuration that cannot be supported in current VyOS config mode yet. --- src/completion/list_ddclient_protocols.sh | 2 +- src/validators/ddclient-protocol | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/completion/list_ddclient_protocols.sh b/src/completion/list_ddclient_protocols.sh index 0c8c2712d..0e66b006a 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|porkbun') diff --git a/src/validators/ddclient-protocol b/src/validators/ddclient-protocol index 0d28039d3..217853939 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|porkbun' | grep -qw $1 if [ $? -gt 0 ]; then echo "Error: $1 is not a valid protocol, please choose from the supported list of protocols" -- cgit v1.2.3 From be738c1a3106d0f16d8efc55f2cf1b217978676b Mon Sep 17 00:00:00 2001 From: Indrajit Raychaudhuri Date: Wed, 25 Dec 2024 23:15:02 -0600 Subject: ddclient: T6981: Enhance/add protocol configuration These new protocols are supported in ddclient v4: 'ddns.fm', 'he.net', 'inwx' These existing protocols gained dual stack support in ddclient v4: 'domeneshop', 'gandi', 'godaddy', 'mythicdyn', 'noip', 'nsupdate', 'regfishde' --- src/conf_mode/service_dns_dynamic.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) (limited to 'src') diff --git a/src/conf_mode/service_dns_dynamic.py b/src/conf_mode/service_dns_dynamic.py index 682daa8a2..9cf56c6e3 100755 --- a/src/conf_mode/service_dns_dynamic.py +++ b/src/conf_mode/service_dns_dynamic.py @@ -38,18 +38,20 @@ zone_necessary = ['cloudflare', 'digitalocean', 'godaddy', 'hetzner', 'gandi', zone_supported = zone_necessary + ['dnsexit2', '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'] # 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', + 'regfishde'] # dyndns2 protocol in ddclient honors dual stack for selective servers # because of the way it is implemented in ddclient -- cgit v1.2.3 From ab09e337dc230f98e09992169c3ece308c5fe224 Mon Sep 17 00:00:00 2001 From: Indrajit Raychaudhuri Date: Mon, 30 Dec 2024 02:10:11 -0600 Subject: ddclient: T6981: Allow luadns and desec for dyndns dualstack Add `app.luadns.com` and `update.dedyn.io` to allow-list for dualstack support using 'dyndns2' protocol since they have been reported to work as expected. --- src/conf_mode/service_dns_dynamic.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/conf_mode/service_dns_dynamic.py b/src/conf_mode/service_dns_dynamic.py index 9cf56c6e3..167c67a6a 100755 --- a/src/conf_mode/service_dns_dynamic.py +++ b/src/conf_mode/service_dns_dynamic.py @@ -55,7 +55,8 @@ dualstack_supported = ['cloudflare', 'ddns.fm', 'digitalocean', 'dnsexit2', # 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 = ['members.dyndns.org', 'dynv6.com', 'app.luadns.com', + 'update.dedyn.io'] def get_config(config=None): if config: -- cgit v1.2.3 From 381869d06314bbe98c8f29f37548fd729cb58262 Mon Sep 17 00:00:00 2001 From: Indrajit Raychaudhuri Date: Mon, 30 Dec 2024 19:01:53 -0600 Subject: ddclient: T6981: Enable support for porkbun In ddclient, 'porkbun' protocol uses properties that are different from the ones used by VyOS configuration. Support for 'porkbun' is enabled by adding the necessary property remaping before applying them to the template to render the final ddclient config. --- data/templates/dns-dynamic/ddclient.conf.j2 | 11 +++++++---- interface-definitions/service_dns_dynamic.xml.in | 4 ++-- src/completion/list_ddclient_protocols.sh | 2 +- src/conf_mode/service_dns_dynamic.py | 16 +++++++++++++--- src/validators/ddclient-protocol | 2 +- 5 files changed, 24 insertions(+), 11 deletions(-) (limited to 'src') diff --git a/data/templates/dns-dynamic/ddclient.conf.j2 b/data/templates/dns-dynamic/ddclient.conf.j2 index fd50a529d..41aae729e 100644 --- a/data/templates/dns-dynamic/ddclient.conf.j2 +++ b/data/templates/dns-dynamic/ddclient.conf.j2 @@ -15,7 +15,7 @@ if{{ ipv }}={{ address }}, \ {% endfor %} {# Other service options with special treatment for password #} {% for k,v in kwargs.items() if v is vyos_defined %} -{{ k | replace('_', '-') }}={{ "'%s'" % (v) if k == 'password' else v }}{{ ',' if not loop.last }} \ +{{ k | replace('_', '-') }}={{ "'%s'" % (v) if k in ['password', 'secretapikey'] else v }}{{ ',' if not loop.last }} \ {% endfor %} {# Actual hostname for the service #} {{ host }} @@ -38,9 +38,12 @@ if{{ ipv }}={{ address }}, \ # 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=config.password, ttl=config.ttl, - min_interval=config.wait_time, max_interval=config.expiry_time) }} + protocol=config.protocol, server=config.server, + zone=config.zone, root_domain=config.root_domain, + login=config.username, apikey=config.apikey, + password=config.password, secretapikey=config.secretapikey, + ttl=config.ttl, min_interval=config.wait_time, + max_interval=config.expiry_time) }} {% endfor %} {% endfor %} {% endif %} diff --git a/interface-definitions/service_dns_dynamic.xml.in b/interface-definitions/service_dns_dynamic.xml.in index 99103ec73..e352f88e4 100644 --- a/interface-definitions/service_dns_dynamic.xml.in +++ b/interface-definitions/service_dns_dynamic.xml.in @@ -123,10 +123,10 @@ - DNS zone to be updated + DNS zone or root domain to be updated txt - Name of DNS zone + Name of DNS zone or root domain diff --git a/src/completion/list_ddclient_protocols.sh b/src/completion/list_ddclient_protocols.sh index 0e66b006a..d92239270 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|directnic|emailonly|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 167c67a6a..27b7e0690 100755 --- a/src/conf_mode/service_dns_dynamic.py +++ b/src/conf_mode/service_dns_dynamic.py @@ -35,7 +35,7 @@ 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_supported = zone_necessary + ['dnsexit2', 'porkbun', 'zoneedit1'] # Protocols that do not require username username_unnecessary = ['1984', 'cloudflare', 'cloudns', 'ddns.fm', 'digitalocean', @@ -44,14 +44,14 @@ username_unnecessary = ['1984', 'cloudflare', 'cloudns', 'ddns.fm', 'digitalocea # Protocols that support TTL ttl_supported = ['cloudflare', 'dnsexit2', 'gandi', 'hetzner', 'godaddy', 'nfsn', - 'nsupdate'] + 'nsupdate', 'porkbun'] # Protocols that support both IPv4 and IPv6 dualstack_supported = ['cloudflare', 'ddns.fm', 'digitalocean', 'dnsexit2', 'domeneshop', 'duckdns', 'dyndns2', 'easydns', 'freedns', 'gandi', 'godaddy', 'he.net', 'hetzner', 'infomaniak', 'inwx', 'mythicdyn', 'njalla', 'noip', 'nsupdate', - 'regfishde'] + 'porkbun', 'regfishde'] # dyndns2 protocol in ddclient honors dual stack for selective servers # because of the way it is implemented in ddclient @@ -166,6 +166,16 @@ def generate(dyndns): 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') + render(config_file, 'dns-dynamic/ddclient.conf.j2', dyndns, permission=0o600) render(systemd_override, 'dns-dynamic/override.conf.j2', dyndns) return None diff --git a/src/validators/ddclient-protocol b/src/validators/ddclient-protocol index 217853939..bcb2ab8ed 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|directnic|emailonly|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" -- cgit v1.2.3 From 98f83eb047c994c30cc254ea7a415b1135765d8b Mon Sep 17 00:00:00 2001 From: Indrajit Raychaudhuri Date: Mon, 30 Dec 2024 19:18:23 -0600 Subject: ddclient: T6981: Enforce using Gandi personal access token Since the API key has been deprecated by Gandi, enforce using personal access token for 'gandi' protocol. --- data/templates/dns-dynamic/ddclient.conf.j2 | 1 + src/conf_mode/service_dns_dynamic.py | 4 ++++ 2 files changed, 5 insertions(+) (limited to 'src') diff --git a/data/templates/dns-dynamic/ddclient.conf.j2 b/data/templates/dns-dynamic/ddclient.conf.j2 index 41aae729e..ef7432433 100644 --- a/data/templates/dns-dynamic/ddclient.conf.j2 +++ b/data/templates/dns-dynamic/ddclient.conf.j2 @@ -42,6 +42,7 @@ if{{ ipv }}={{ address }}, \ zone=config.zone, root_domain=config.root_domain, login=config.username, apikey=config.apikey, password=config.password, secretapikey=config.secretapikey, + use_personal_access_token=config.use_personal_access_token, ttl=config.ttl, min_interval=config.wait_time, max_interval=config.expiry_time) }} {% endfor %} diff --git a/src/conf_mode/service_dns_dynamic.py b/src/conf_mode/service_dns_dynamic.py index 27b7e0690..92b067dcc 100755 --- a/src/conf_mode/service_dns_dynamic.py +++ b/src/conf_mode/service_dns_dynamic.py @@ -176,6 +176,10 @@ def generate(dyndns): 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 -- cgit v1.2.3 From ced95444e5233c4fef5e274220dc665bbe836e69 Mon Sep 17 00:00:00 2001 From: Indrajit Raychaudhuri Date: Mon, 13 Apr 2026 03:11:35 -0500 Subject: ddclient: T6981: Reformat for ruff compliance --- src/conf_mode/service_dns_dynamic.py | 178 ++++++++++++++++++++++++++--------- 1 file changed, 136 insertions(+), 42 deletions(-) (limited to 'src') diff --git a/src/conf_mode/service_dns_dynamic.py b/src/conf_mode/service_dns_dynamic.py index 92b067dcc..b6daba6ff 100755 --- a/src/conf_mode/service_dns_dynamic.py +++ b/src/conf_mode/service_dns_dynamic.py @@ -27,36 +27,89 @@ from vyos.utils.process import call 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_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', 'ddns.fm', 'digitalocean', - 'dnsexit2', 'duckdns', 'freemyip', 'gandi', 'he.net', - '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', 'porkbun'] +ttl_supported = [ + 'cloudflare', + 'dnsexit2', + 'gandi', + 'hetzner', + 'godaddy', + 'nfsn', + 'nsupdate', + 'porkbun', +] # Protocols that support both IPv4 and IPv6 -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'] +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', 'app.luadns.com', - 'update.dedyn.io'] +dyndns_dualstack_servers = [ + 'app.luadns.com', + 'dynv6.com', + 'members.dyndns.org', + 'update.dedyn.io', +] + def get_config(config=None): if config: @@ -68,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: @@ -91,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 @@ -104,16 +165,22 @@ 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 ( + 'skip' in config['address']['web'] + and 'url' not in config['address']['web'] + ): + raise ConfigError( + f'"url" along with "skip" {error_msg_req} with protocol "{config["protocol"]}"' + ) # RFC2136 uses 'key' instead of 'password' if config['protocol'] != 'nsupdate' and 'password' not in config: @@ -122,38 +189,63 @@ 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: @@ -184,6 +276,7 @@ def generate(dyndns): render(systemd_override, 'dns-dynamic/override.conf.j2', dyndns) return None + def apply(dyndns): systemd_service = 'ddclient.service' # Reload systemd manager configuration @@ -199,6 +292,7 @@ def apply(dyndns): return None + if __name__ == '__main__': try: c = get_config() -- cgit v1.2.3