diff options
-rw-r--r-- | data/templates/dns-dynamic/ddclient.conf.j2 | 48 | ||||
-rw-r--r-- | interface-definitions/dns-dynamic.xml.in | 226 | ||||
-rwxr-xr-x | src/completion/list_ddclient_protocols.sh | 2 | ||||
-rwxr-xr-x | src/conf_mode/dns_dynamic.py | 92 | ||||
-rwxr-xr-x | src/validators/ddclient-protocol | 2 |
5 files changed, 204 insertions, 166 deletions
diff --git a/data/templates/dns-dynamic/ddclient.conf.j2 b/data/templates/dns-dynamic/ddclient.conf.j2 index 356b8d0d0..30afb9e64 100644 --- a/data/templates/dns-dynamic/ddclient.conf.j2 +++ b/data/templates/dns-dynamic/ddclient.conf.j2 @@ -29,44 +29,28 @@ cache={{ config_file | replace('.conf', '.cache') }} {# 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=disabled +use=no -{% 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 %} +{% if name is vyos_defined %} +{% for service, config in name.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 [config.ip_version[2:]] %} +{% 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 #} +{% 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 %} -# Web service dynamic DNS configuration for {{ name }}: [{{ config.protocol }}, {{ host }}] -{{ render_config(host, address, service_cfg.web_options, ip_suffixes, +# Web service dynamic DNS configuration for {{ service }}: [{{ config.protocol }}, {{ host }}] +{{ render_config(host, config.address, config.web_options, ip_suffixes, protocol=config.protocol, server=config.server, zone=config.zone, - login=config.username, password=config.password, ttl=config.ttl, + login=config.username, password=password, ttl=config.ttl, min_interval=config.wait_time, max_interval=config.expiry_time) }} -{% endfor %} -{% endfor %} -{% endif %} +{% endfor %} {% endfor %} {% endif %} diff --git a/interface-definitions/dns-dynamic.xml.in b/interface-definitions/dns-dynamic.xml.in index 32c5af9b6..f089f0e52 100644 --- a/interface-definitions/dns-dynamic.xml.in +++ b/interface-definitions/dns-dynamic.xml.in @@ -12,27 +12,48 @@ <help>Dynamic DNS</help> </properties> <children> - <tagNode name="address"> + <tagNode name="name"> <properties> - <help>Obtain IP address to send Dynamic DNS update for</help> + <help>Dynamic DNS configuration</help> <valueHelp> <format>txt</format> - <description>Use interface to obtain the IP address</description> + <description>Dynamic DNS service name</description> </valueHelp> - <valueHelp> - <format>web</format> - <description>Use HTTP(S) web request to obtain the IP address</description> - </valueHelp> - <completionHelp> - <script>${vyos_completion_dir}/list_interfaces</script> - <list>web</list> - </completionHelp> - <constraint> - #include <include/constraint/interface-name.xml.i> - <regex>web</regex> - </constraint> </properties> <children> + #include <include/generic-description.xml.i> + <leafNode name="protocol"> + <properties> + <help>ddclient protocol used for Dynamic DNS service</help> + <completionHelp> + <script>${vyos_completion_dir}/list_ddclient_protocols.sh</script> + </completionHelp> + <constraint> + <validator name="ddclient-protocol"/> + </constraint> + </properties> + </leafNode> + <leafNode name="address"> + <properties> + <help>Obtain IP address to send Dynamic DNS update for</help> + <valueHelp> + <format>txt</format> + <description>Use interface to obtain the IP address</description> + </valueHelp> + <valueHelp> + <format>web</format> + <description>Use HTTP(S) web request to obtain the IP address</description> + </valueHelp> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces</script> + <list>web</list> + </completionHelp> + <constraint> + #include <include/constraint/interface-name.xml.i> + <regex>web</regex> + </constraint> + </properties> + </leafNode> <node name="web-options"> <properties> <help>Options when using HTTP(S) web request to obtain the IP address</help> @@ -50,88 +71,117 @@ </leafNode> </children> </node> - <tagNode name="rfc2136"> + <leafNode name="ip-version"> <properties> - <help>RFC2136 nsupdate configuration</help> + <help>IP address version to use</help> <valueHelp> - <format>txt</format> - <description>RFC2136 nsupdate service name</description> + <format>_ipv4</format> + <description>Use only IPv4 address</description> + </valueHelp> + <valueHelp> + <format>_ipv6</format> + <description>Use only IPv6 address</description> </valueHelp> + <valueHelp> + <format>both</format> + <description>Use both IPv4 and IPv6 address</description> + </valueHelp> + <completionHelp> + <list>ipv4 ipv6 both</list> + </completionHelp> + <constraint> + <regex>(ipv[46]|both)</regex> + </constraint> + <constraintErrorMessage>IP Version must be literal 'ipv4', 'ipv6' or 'both'</constraintErrorMessage> </properties> - <children> - #include <include/generic-description.xml.i> - #include <include/dns/dynamic-service-host-name-server.xml.i> - #include <include/dns/dynamic-service-wait-expiry-time.xml.i> - <leafNode name="key"> - <properties> - <help>File containing the TSIG secret key shared with remote DNS server</help> - <valueHelp> - <format>filename</format> - <description>File in /config/auth directory</description> - </valueHelp> - <constraint> - <validator name="file-path" argument="--strict --parent-dir /config/auth"/> - </constraint> - </properties> - </leafNode> - #include <include/dns/time-to-live.xml.i> - #include <include/dns/dynamic-service-zone.xml.i> - </children> - </tagNode> - <tagNode name="service"> + <defaultValue>ipv4</defaultValue> + </leafNode> + <leafNode name="host-name"> + <properties> + <help>Hostname to register with Dynamic DNS service</help> + <constraint> + #include <include/constraint/host-name.xml.i> + <regex>(\@|\*)[-.A-Za-z0-9]*</regex> + </constraint> + <constraintErrorMessage>Host-name must be alphanumeric, can contain hyphens and can be prefixed with '@' or '*'</constraintErrorMessage> + <multi/> + </properties> + </leafNode> + <leafNode name="server"> <properties> - <help>Dynamic DNS configuration</help> + <help>Remote Dynamic DNS server to send updates to</help> + <valueHelp> + <format>ipv4</format> + <description>IPv4 address of the remote server</description> + </valueHelp> + <valueHelp> + <format>ipv6</format> + <description>IPv6 address of the remote server</description> + </valueHelp> + <valueHelp> + <format>hostname</format> + <description>Fully qualified domain name of the remote server</description> + </valueHelp> + <constraint> + <validator name="ip-address"/> + <validator name="fqdn"/> + </constraint> + <constraintErrorMessage>Remote server must be IP address or fully qualified domain name</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="zone"> + <properties> + <help>DNS zone to be updated</help> <valueHelp> <format>txt</format> - <description>Dynamic DNS service name</description> + <description>Name of DNS zone</description> </valueHelp> + <constraint> + <validator name="fqdn"/> + </constraint> </properties> - <children> - #include <include/generic-description.xml.i> - #include <include/dns/dynamic-service-host-name-server.xml.i> - #include <include/dns/dynamic-service-wait-expiry-time.xml.i> - #include <include/generic-username.xml.i> - #include <include/generic-password.xml.i> - #include <include/dns/time-to-live.xml.i> - <leafNode name="protocol"> - <properties> - <help>ddclient protocol used for Dynamic DNS service</help> - <completionHelp> - <script>${vyos_completion_dir}/list_ddclient_protocols.sh</script> - </completionHelp> - <constraint> - <validator name="ddclient-protocol"/> - </constraint> - </properties> - </leafNode> - #include <include/dns/dynamic-service-zone.xml.i> - <leafNode name="ip-version"> - <properties> - <help>IP address version to use</help> - <valueHelp> - <format>_ipv4</format> - <description>Use only IPv4 address</description> - </valueHelp> - <valueHelp> - <format>_ipv6</format> - <description>Use only IPv6 address</description> - </valueHelp> - <valueHelp> - <format>both</format> - <description>Use both IPv4 and IPv6 address</description> - </valueHelp> - <completionHelp> - <list>ipv4 ipv6 both</list> - </completionHelp> - <constraint> - <regex>(ipv[46]|both)</regex> - </constraint> - <constraintErrorMessage>IP Version must be literal 'ipv4', 'ipv6' or 'both'</constraintErrorMessage> - </properties> - <defaultValue>ipv4</defaultValue> - </leafNode> - </children> - </tagNode> + </leafNode> + #include <include/generic-username.xml.i> + #include <include/generic-password.xml.i> + <leafNode name="key"> + <properties> + <help>File containing TSIG authentication key for RFC2136 nsupdate on remote DNS server</help> + <valueHelp> + <format>filename</format> + <description>File in /config/auth directory</description> + </valueHelp> + <constraint> + <validator name="file-path" argument="--strict --parent-dir /config/auth"/> + </constraint> + </properties> + </leafNode> + #include <include/dns/time-to-live.xml.i> + <leafNode name="wait-time"> + <properties> + <help>Time in seconds to wait between update attempts</help> + <valueHelp> + <format>u32:60-86400</format> + <description>Time in seconds</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 60-86400"/> + </constraint> + <constraintErrorMessage>Wait time must be between 60 and 86400 seconds</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="expiry-time"> + <properties> + <help>Time in seconds for the hostname to be marked expired in cache</help> + <valueHelp> + <format>u32:300-2160000</format> + <description>Time in seconds</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 300-2160000"/> + </constraint> + <constraintErrorMessage>Expiry time must be between 300 and 2160000 seconds</constraintErrorMessage> + </properties> + </leafNode> </children> </tagNode> <leafNode name="interval"> diff --git a/src/completion/list_ddclient_protocols.sh b/src/completion/list_ddclient_protocols.sh index c8855b5d1..634981660 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 <http://www.gnu.org/licenses/>. -echo -n $(ddclient -list-protocols | grep -vE 'nsupdate|cloudns|porkbun') +echo -n $(ddclient -list-protocols | grep -vE 'cloudns|porkbun') diff --git a/src/conf_mode/dns_dynamic.py b/src/conf_mode/dns_dynamic.py index 2bccaee0f..3ddc8e7fd 100755 --- a/src/conf_mode/dns_dynamic.py +++ b/src/conf_mode/dns_dynamic.py @@ -30,16 +30,18 @@ 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'] +zone_necessary = ['cloudflare', 'digitalocean', 'godaddy', 'hetzner', 'gandi', + 'nfsn', 'nsupdate'] zone_supported = zone_necessary + ['dnsexit2', 'zoneedit1'] # Protocols that do not require username username_unnecessary = ['1984', 'cloudflare', 'cloudns', 'digitalocean', 'dnsexit2', 'duckdns', 'freemyip', 'hetzner', 'keysystems', 'njalla', - 'regfishde'] + 'nsupdate', 'regfishde'] # Protocols that support TTL -ttl_supported = ['cloudflare', 'dnsexit2', 'gandi', 'hetzner', 'godaddy', 'nfsn'] +ttl_supported = ['cloudflare', 'dnsexit2', 'gandi', 'hetzner', 'godaddy', 'nfsn', + 'nsupdate'] # Protocols that support both IPv4 and IPv6 dualstack_supported = ['cloudflare', 'digitalocean', 'dnsexit2', 'duckdns', @@ -70,63 +72,65 @@ def get_config(config=None): def verify(dyndns): # bail out early - looks like removal from running config - if not dyndns or 'address' not in dyndns: + if not dyndns or 'name' not in dyndns: return None - for address in dyndns['address']: - # If dyndns address is an interface, ensure it exists - if address != 'web': - verify_interface_exists(address) + # Dynamic DNS service provider - configuration validation + for service, config in dyndns['name'].items(): - # 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}"') + error_msg_req = f'is required for Dynamic DNS service "{service}"' + error_msg_uns = f'is not supported for Dynamic DNS service "{service}"' - # Dynamic DNS service provider - configuration validation - if 'web_options' in dyndns['address'][address] and address != 'web': - raise ConfigError(f'"web-options" is applicable only when using HTTP(S) web request to obtain the IP address') + for field in ['protocol', 'address', 'host_name']: + if field not in config: + raise ConfigError(f'"{field.replace("_", "-")}" {error_msg_req}') - # Dynamic DNS service provider - configuration validation - if 'service' in dyndns['address'][address]: - for service, config in dyndns['address'][address]['service'].items(): - error_msg_req = f'is required for Dynamic DNS service "{service}" on "{address}"' - error_msg_uns = f'is not supported for Dynamic DNS service "{service}" on "{address}" with protocol "{config["protocol"]}"' + # If dyndns address is an interface, ensure that it exists + # and that web-options are not set + if config['address'] != 'web': + verify_interface_exists(config['address']) + if 'web_options' in config: + raise ConfigError(f'"web-options" is applicable only when using HTTP(S) web request to obtain the IP address') - for field in ['host_name', 'password', 'protocol']: - if field not in config: - raise ConfigError(f'"{field.replace("_", "-")}" {error_msg_req}') + # RFC2136 uses 'key' instead of 'password' + if config['protocol'] != 'nsupdate' and 'password' not in config: + raise ConfigError(f'"password" {error_msg_req}') - if config['protocol'] in zone_necessary and 'zone' not in config: - raise ConfigError(f'"zone" {error_msg_req} with protocol "{config["protocol"]}"') + # Other RFC2136 specific configuration validation + if config['protocol'] == 'nsupdate': + if 'password' in config: + 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"]}"') - if config['protocol'] not in zone_supported and 'zone' in config: - raise ConfigError(f'"zone" {error_msg_uns}') + if config['protocol'] in zone_necessary and 'zone' not in config: + raise ConfigError(f'"zone" {error_msg_req} 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"]}"') + if config['protocol'] not in zone_supported and 'zone' in config: + raise ConfigError(f'"zone" {error_msg_uns} with protocol "{config["protocol"]}"') - if config['protocol'] not in ttl_supported and 'ttl' in config: - raise ConfigError(f'"ttl" {error_msg_uns}') + if config['protocol'] not in username_unnecessary and 'username' not in config: + raise ConfigError(f'"username" {error_msg_req} 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}') - # 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} for "{config["server"]}"') + if config['protocol'] not in ttl_supported and 'ttl' in config: + raise ConfigError(f'"ttl" {error_msg_uns} 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"') + 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} 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} 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 Dynamic DNS service "{service}"') return None def generate(dyndns): # bail out early - looks like removal from running config - if not dyndns or 'address' not in dyndns: + if not dyndns or 'name' not in dyndns: return None render(config_file, 'dns-dynamic/ddclient.conf.j2', dyndns, permission=0o600) @@ -139,7 +143,7 @@ def apply(dyndns): call('systemctl daemon-reload') # bail out early - looks like removal from running config - if not dyndns or 'address' not in dyndns: + if not dyndns or 'name' not in dyndns: call(f'systemctl stop {systemd_service}') if os.path.exists(config_file): os.unlink(config_file) diff --git a/src/validators/ddclient-protocol b/src/validators/ddclient-protocol index 8f455e12e..ce5efbd52 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 <http://www.gnu.org/licenses/>. -ddclient -list-protocols | grep -vE 'nsupdate|cloudns|porkbun' | grep -qw $1 +ddclient -list-protocols | grep -vE 'cloudns|porkbun' | grep -qw $1 if [ $? -gt 0 ]; then echo "Error: $1 is not a valid protocol, please choose from the supported list of protocols" |