From f2392055a33e537ce88c6a3124106bfd3b8b8e57 Mon Sep 17 00:00:00 2001 From: Indrajit Raychaudhuri Date: Sun, 23 Apr 2023 18:25:21 -0500 Subject: dns: T5144: Modernize dynamic dns operation Apply next round of configuration tree updates to 'service dns dynamic' with the following changes: - Migrate `service dns dynamic interface [use-web]` to `service dns dynamic address ` or `service dns dynamic address web [web-options]` This communicates the intent that dynamic dns IP address is detected in only one way - using the `` or using an external web request, not both. - When using external web request, (`service dns dynamic address web`), external url is optional (`web-options url`). Ddclient defaults are used when unspecified, - Rename all config `login` to `username` for consistency and also to align better with alternative ddclient backends in consideration. - Apply global 'ipv6-enable' to per service 'ip-version: ipv6'. Selecting usage of IPv4 or IPv6 (or both simultaneously) is now at per service (protocol) level instead of global level. This allows more control on the ability to select IPv4 in some cases and IPv6 in some other cases wherever supported by the underlying ddclient protocol. - While the IP address (and by extension, the detection mechanism) is global, the way it is applied to a particular ddclient protocol depends on whether it supports IPv4 or IPv6 or both. - Related to the above, this also prevents generating incorrect config file (`ddclient.conf`) with multiple global sections leading to an unpredictable behavior of ddclient. - Implement provider (protocol) specific custom tweaks whenever possible (e.g., `zone`, `username`, `server` are not necessary in all cases). - Move service name from a combination of 'protocol' (with protocol config autodetected) and custom (with protocol config specified) to a single 'service' key. This allows for consisent setup of multiple config for the same ddclient protocol (with different options and credentials). This also avoid ambiguity with usual networking term 'protocol' and ddclient specific term 'protocol' (and can change with a move to a different backend). - Apply upfront XML constraints and validations consistently wherever applicable. - RFC2136 specific change: Rename rfc2136 config `record` to `host-name` for consistency. - Cloudflare specific change: While ddclient still supports authenticating with email and global auth key, skipping `username` in config will indicate the intent to use API token authentication (with special 'token' literal as `username`). --- src/conf_mode/dynamic_dns.py | 118 +++++++++++++++++-------------------------- 1 file changed, 45 insertions(+), 73 deletions(-) (limited to 'src') diff --git a/src/conf_mode/dynamic_dns.py b/src/conf_mode/dynamic_dns.py index 426e3d693..879086cc4 100755 --- a/src/conf_mode/dynamic_dns.py +++ b/src/conf_mode/dynamic_dns.py @@ -29,20 +29,14 @@ 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' -} +# 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: @@ -56,24 +50,13 @@ def get_config(config=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]) + 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]) return dyndns @@ -82,48 +65,37 @@ def verify(dyndns): if not dyndns: return None - # A 'node' corresponds to an interface - if 'interface' not in dyndns: - return None - - for interface in dyndns['interface']: + for address in dyndns['address']: # 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}') + 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['protocol'] not in dualstack_supported + and config['ip_version'] == 'both'): + raise ConfigError(f'"{config["protocol"]}" does not support IPv4 and IPv6 at the same time') return None -- cgit v1.2.3 From 604a12d65ddc5ca81bc1a8a7bbdcb7cf40a7897d Mon Sep 17 00:00:00 2001 From: Indrajit Raychaudhuri Date: Sun, 4 Jun 2023 00:17:59 -0500 Subject: dns: T5144: Restrict dualstack for dyndns2 protocol to dyn.com ddclient implementation of dualstack for dyndns2 protocol is targeted for dyn.com (dyndns.org) only. Dualstack won't work for other servers supporting dyndns2 protocol (for example, dyn.dns.he.net). --- src/conf_mode/dynamic_dns.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/conf_mode/dynamic_dns.py b/src/conf_mode/dynamic_dns.py index 879086cc4..fe72a3180 100755 --- a/src/conf_mode/dynamic_dns.py +++ b/src/conf_mode/dynamic_dns.py @@ -93,9 +93,12 @@ def verify(dyndns): if 'username' not in config: raise ConfigError(f'"username" {error_msg}') - if (config['protocol'] not in dualstack_supported - and config['ip_version'] == 'both'): - raise ConfigError(f'"{config["protocol"]}" does not support IPv4 and IPv6 at the same time') + 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 -- cgit v1.2.3 From 79040279b0400cd819c165e5ed38c24bf3c387e3 Mon Sep 17 00:00:00 2001 From: Indrajit Raychaudhuri Date: Thu, 11 May 2023 19:56:34 -0500 Subject: dns: T5144: Apply migration for dynamic dns path update Create migration and bump package version from 0 -> 1 for dynamic dns --- .../include/version/dns-dynamic-version.xml.i | 3 + interface-definitions/xml-component-version.xml.in | 1 + src/migration-scripts/dns-dynamic/0-to-1 | 104 +++++++++++++++++++++ 3 files changed, 108 insertions(+) create mode 100644 interface-definitions/include/version/dns-dynamic-version.xml.i create mode 100755 src/migration-scripts/dns-dynamic/0-to-1 (limited to 'src') 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 @@ + + + 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 @@ -10,6 +10,7 @@ #include #include #include + #include #include #include #include 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 . + +# T5144: +# - migrate "service dns dynamic interface ..." +# to "service dns dynamic address ..." +# - migrate "service dns dynamic interface use-web ..." +# to "service dns dynamic address
web-options ..." +# - migrate "service dns dynamic interface rfc2136 record ..." +# to "service dns dynamic address
rfc2136 host-name ..." +# - migrate "service dns dynamic interface service login ..." +# to "service dns dynamic address
service username ..." +# - apply global 'ipv6-enable' to per '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 rfc2136 record" + # to "service dns dynamic address
rfc2136 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 service login" + # to "service dns dynamic address
service 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 '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 use-web" + # to "service dns dynamic address
web-options" + # Also, rename
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) -- cgit v1.2.3 From f4ea61dd5bf0ef2baa7d545b12e168b652412509 Mon Sep 17 00:00:00 2001 From: Indrajit Raychaudhuri Date: Sat, 13 May 2023 00:55:33 -0500 Subject: dns: T5144: Relocate ddclient template path for consistency with config path --- data/templates/dns-dynamic/ddclient.conf.j2 | 70 +++++++++++++++ data/templates/dynamic-dns/ddclient.conf.j2 | 70 --------------- interface-definitions/dns-dynamic.xml.in | 2 +- src/conf_mode/dns_dynamic.py | 131 ++++++++++++++++++++++++++++ src/conf_mode/dynamic_dns.py | 131 ---------------------------- 5 files changed, 202 insertions(+), 202 deletions(-) create mode 100644 data/templates/dns-dynamic/ddclient.conf.j2 delete mode 100644 data/templates/dynamic-dns/ddclient.conf.j2 create mode 100755 src/conf_mode/dns_dynamic.py delete mode 100755 src/conf_mode/dynamic_dns.py (limited to 'src') diff --git a/data/templates/dns-dynamic/ddclient.conf.j2 b/data/templates/dns-dynamic/ddclient.conf.j2 new file mode 100644 index 000000000..945191bb7 --- /dev/null +++ b/data/templates/dns-dynamic/ddclient.conf.j2 @@ -0,0 +1,70 @@ +{% 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 + +{% 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/dynamic-dns/ddclient.conf.j2 b/data/templates/dynamic-dns/ddclient.conf.j2 deleted file mode 100644 index 6d1eb03a7..000000000 --- a/data/templates/dynamic-dns/ddclient.conf.j2 +++ /dev/null @@ -1,70 +0,0 @@ -{% 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 dynamic_dns.py ### -daemon=1m -syslog=yes -ssl=yes - -{% 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/interface-definitions/dns-dynamic.xml.in b/interface-definitions/dns-dynamic.xml.in index 308dc5de0..292c50603 100644 --- a/interface-definitions/dns-dynamic.xml.in +++ b/interface-definitions/dns-dynamic.xml.in @@ -7,7 +7,7 @@ Domain Name System related services - + Dynamic DNS diff --git a/src/conf_mode/dns_dynamic.py b/src/conf_mode/dns_dynamic.py new file mode 100755 index 000000000..e4e2cf30e --- /dev/null +++ b/src/conf_mode/dns_dynamic.py @@ -0,0 +1,131 @@ +#!/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 . + +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' + +# 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]) + + 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) + 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 fe72a3180..000000000 --- a/src/conf_mode/dynamic_dns.py +++ /dev/null @@ -1,131 +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 . - -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' - -# 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]) - - 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, '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) -- cgit v1.2.3 From a79a61b8e3afd39b7faeaca0750e68f3d693c5a9 Mon Sep 17 00:00:00 2001 From: Indrajit Raychaudhuri Date: Sun, 4 Jun 2023 00:28:40 -0500 Subject: dns: T5144: Relocate ddclient op-mode config files for consistency with config path --- op-mode-definitions/dns-dynamic.xml.in | 6 +- src/op_mode/dns_dynamic.py | 113 +++++++++++++++++++++++++++++++++ src/op_mode/dynamic_dns.py | 113 --------------------------------- 3 files changed, 116 insertions(+), 116 deletions(-) create mode 100755 src/op_mode/dns_dynamic.py delete mode 100755 src/op_mode/dynamic_dns.py (limited to 'src') 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 @@ -30,7 +30,7 @@ Show Dynamic DNS status - sudo ${vyos_op_scripts_dir}/dynamic_dns.py --status + sudo ${vyos_op_scripts_dir}/dns_dynamic.py --status @@ -46,7 +46,7 @@ Restart Dynamic DNS service - sudo ${vyos_op_scripts_dir}/dynamic_dns.py --update + sudo ${vyos_op_scripts_dir}/dns_dynamic.py --update @@ -66,7 +66,7 @@ Update Dynamic DNS information - sudo ${vyos_op_scripts_dir}/dynamic_dns.py --update + sudo ${vyos_op_scripts_dir}/dns_dynamic.py --update diff --git a/src/op_mode/dns_dynamic.py b/src/op_mode/dns_dynamic.py new file mode 100755 index 000000000..d41a74db3 --- /dev/null +++ b/src/op_mode/dns_dynamic.py @@ -0,0 +1,113 @@ +#!/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 . + +import os +import argparse +import sys +import time +from tabulate import tabulate + +from vyos.config import Config +from vyos.template import is_ipv4, is_ipv6 +from vyos.util import call + +cache_file = r'/run/ddclient/ddclient.cache' + +columns = { + 'host': 'Hostname', + 'ipv4': 'IPv4 address', + 'status-ipv4': 'IPv4 status', + 'ipv6': 'IPv6 address', + 'status-ipv6': 'IPv6 status', + 'mtime': 'Last update', +} + + +def _get_formatted_host_records(host_data): + data_entries = [] + for entry in host_data: + data_entries.append([entry.get(key) for key in columns.keys()]) + + header = columns.values() + output = tabulate(data_entries, header, numalign='left') + return output + + +def show_status(): + # A ddclient status file might not always exist + if not os.path.exists(cache_file): + sys.exit(0) + + data = [] + + with open(cache_file, 'r') as f: + for line in f: + if line.startswith('#'): + continue + + props = {} + # ddclient cache rows have properties in 'key=value' format separated by comma + # we pick up the ones we are interested in + for kvraw in line.split(' ')[0].split(','): + k, v = kvraw.split('=') + if k in list(columns.keys()) + ['ip', 'status']: # ip and status are legacy 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( + "%Y-%m-%d %H:%M:%S", time.localtime(int(props['mtime'], base=10))) + + data.append(props) + + print(_get_formatted_host_records(data)) + + +def update_ddns(): + call('systemctl stop ddclient.service') + if os.path.exists(cache_file): + os.remove(cache_file) + call('systemctl start ddclient.service') + + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + group = parser.add_mutually_exclusive_group() + group.add_argument("--status", help="Show DDNS status", action="store_true") + group.add_argument("--update", help="Update DDNS on a given interface", action="store_true") + args = parser.parse_args() + + # Do nothing if service is not configured + c = Config() + if not c.exists_effective('service dns dynamic'): + print("Dynamic DNS not configured") + sys.exit(1) + + if args.status: + show_status() + elif args.update: + update_ddns() diff --git a/src/op_mode/dynamic_dns.py b/src/op_mode/dynamic_dns.py deleted file mode 100755 index d41a74db3..000000000 --- a/src/op_mode/dynamic_dns.py +++ /dev/null @@ -1,113 +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 . - -import os -import argparse -import sys -import time -from tabulate import tabulate - -from vyos.config import Config -from vyos.template import is_ipv4, is_ipv6 -from vyos.util import call - -cache_file = r'/run/ddclient/ddclient.cache' - -columns = { - 'host': 'Hostname', - 'ipv4': 'IPv4 address', - 'status-ipv4': 'IPv4 status', - 'ipv6': 'IPv6 address', - 'status-ipv6': 'IPv6 status', - 'mtime': 'Last update', -} - - -def _get_formatted_host_records(host_data): - data_entries = [] - for entry in host_data: - data_entries.append([entry.get(key) for key in columns.keys()]) - - header = columns.values() - output = tabulate(data_entries, header, numalign='left') - return output - - -def show_status(): - # A ddclient status file might not always exist - if not os.path.exists(cache_file): - sys.exit(0) - - data = [] - - with open(cache_file, 'r') as f: - for line in f: - if line.startswith('#'): - continue - - props = {} - # ddclient cache rows have properties in 'key=value' format separated by comma - # we pick up the ones we are interested in - for kvraw in line.split(' ')[0].split(','): - k, v = kvraw.split('=') - if k in list(columns.keys()) + ['ip', 'status']: # ip and status are legacy 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( - "%Y-%m-%d %H:%M:%S", time.localtime(int(props['mtime'], base=10))) - - data.append(props) - - print(_get_formatted_host_records(data)) - - -def update_ddns(): - call('systemctl stop ddclient.service') - if os.path.exists(cache_file): - os.remove(cache_file) - call('systemctl start ddclient.service') - - -if __name__ == '__main__': - parser = argparse.ArgumentParser() - group = parser.add_mutually_exclusive_group() - group.add_argument("--status", help="Show DDNS status", action="store_true") - group.add_argument("--update", help="Update DDNS on a given interface", action="store_true") - args = parser.parse_args() - - # Do nothing if service is not configured - c = Config() - if not c.exists_effective('service dns dynamic'): - print("Dynamic DNS not configured") - sys.exit(1) - - if args.status: - show_status() - elif args.update: - update_ddns() -- cgit v1.2.3 From b98f38a604954b87c1505bdb6c500a7d6fab983f Mon Sep 17 00:00:00 2001 From: Indrajit Raychaudhuri Date: Sun, 4 Jun 2023 02:56:20 -0500 Subject: dns: T5144: Add pid and cache config as ddclient global --- data/templates/dns-dynamic/ddclient.conf.j2 | 2 ++ src/conf_mode/dns_dynamic.py | 1 + 2 files changed, 3 insertions(+) (limited to 'src') diff --git a/data/templates/dns-dynamic/ddclient.conf.j2 b/data/templates/dns-dynamic/ddclient.conf.j2 index 945191bb7..a19b79c00 100644 --- a/data/templates/dns-dynamic/ddclient.conf.j2 +++ b/data/templates/dns-dynamic/ddclient.conf.j2 @@ -26,6 +26,8 @@ if{{ ipv }}={{ address }}, \ 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() %} diff --git a/src/conf_mode/dns_dynamic.py b/src/conf_mode/dns_dynamic.py index e4e2cf30e..d1ac2a08f 100755 --- a/src/conf_mode/dns_dynamic.py +++ b/src/conf_mode/dns_dynamic.py @@ -58,6 +58,7 @@ def get_config(config=None): 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): -- cgit v1.2.3 From c14825f55d286d54ca3c04703ecbded1cb4c2cca Mon Sep 17 00:00:00 2001 From: Indrajit Raychaudhuri Date: Sun, 4 Jun 2023 02:27:00 -0500 Subject: dns: T5144: Streamline ddclient systemd service override Templatize systemd override for ddclient service and move the generated override files in /run. This ensures that the override files are always generated afresh after boot. Additionally, simplify the systemd override file by removing the redundant/superfluous overrides. --- data/templates/dns-dynamic/override.conf.j2 | 11 +++++++++++ src/conf_mode/dns_dynamic.py | 2 ++ src/etc/systemd/system/ddclient.service.d/override.conf | 11 ----------- 3 files changed, 13 insertions(+), 11 deletions(-) create mode 100644 data/templates/dns-dynamic/override.conf.j2 delete mode 100644 src/etc/systemd/system/ddclient.service.d/override.conf (limited to 'src') 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/src/conf_mode/dns_dynamic.py b/src/conf_mode/dns_dynamic.py index d1ac2a08f..f97225370 100755 --- a/src/conf_mode/dns_dynamic.py +++ b/src/conf_mode/dns_dynamic.py @@ -28,6 +28,7 @@ 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'] @@ -109,6 +110,7 @@ def generate(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): 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 -- cgit v1.2.3