summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorChristian Breunig <christian@breunig.cc>2023-06-05 22:04:21 +0200
committerGitHub <noreply@github.com>2023-06-05 22:04:21 +0200
commit47cce6805562c11b75ef9d5d761881e275d1a27d (patch)
treee75bac5474b8f47cf0a37c47fa6acf78e2116e92 /src
parent3cfeddebb73e12de15d46d13c44003ede2d72c19 (diff)
parentc14825f55d286d54ca3c04703ecbded1cb4c2cca (diff)
downloadvyos-1x-47cce6805562c11b75ef9d5d761881e275d1a27d.tar.gz
vyos-1x-47cce6805562c11b75ef9d5d761881e275d1a27d.zip
Merge pull request #2005 from indrajitr/ddclient-improvement-round-2
dns: T5144: Modernize dynamic dns operation (round 2)
Diffstat (limited to 'src')
-rwxr-xr-xsrc/conf_mode/dns_dynamic.py134
-rwxr-xr-xsrc/conf_mode/dynamic_dns.py156
-rw-r--r--src/etc/systemd/system/ddclient.service.d/override.conf11
-rwxr-xr-xsrc/migration-scripts/dns-dynamic/0-to-1104
-rwxr-xr-xsrc/op_mode/dns_dynamic.py (renamed from src/op_mode/dynamic_dns.py)0
5 files changed, 238 insertions, 167 deletions
diff --git a/src/conf_mode/dns_dynamic.py b/src/conf_mode/dns_dynamic.py
new file mode 100755
index 000000000..f97225370
--- /dev/null
+++ b/src/conf_mode/dns_dynamic.py
@@ -0,0 +1,134 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2018-2020 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+import os
+
+from sys import exit
+
+from vyos.config import Config
+from vyos.configdict import dict_merge
+from vyos.template import render
+from vyos.util import call
+from vyos.xml import defaults
+from vyos import ConfigError
+from vyos import airbag
+airbag.enable()
+
+config_file = r'/run/ddclient/ddclient.conf'
+systemd_override = r'/run/systemd/system/ddclient.service.d/override.conf'
+
+# Protocols that require zone
+zone_allowed = ['cloudflare', 'godaddy', 'hetzner', 'gandi', 'nfsn']
+
+# Protocols that do not require username
+username_unnecessary = ['1984', 'cloudflare', 'cloudns', 'duckdns', 'freemyip', 'hetzner', 'keysystems', 'njalla']
+
+# Protocols that support both IPv4 and IPv6
+dualstack_supported = ['cloudflare', 'dyndns2', 'freedns', 'njalla']
+
+def get_config(config=None):
+ if config:
+ conf = config
+ else:
+ conf = Config()
+
+ base_level = ['service', 'dns', 'dynamic']
+ if not conf.exists(base_level):
+ return None
+
+ dyndns = conf.get_config_dict(base_level, key_mangling=('-', '_'), get_first_key=True)
+
+ for address in dyndns['address']:
+ # Apply service specific defaults (stype = ['rfc2136', 'service'])
+ for svc_type in dyndns['address'][address]:
+ default_values = defaults(base_level + ['address', svc_type])
+ for svc_cfg in dyndns['address'][address][svc_type]:
+ dyndns['address'][address][svc_type][svc_cfg] = dict_merge(
+ default_values, dyndns['address'][address][svc_type][svc_cfg])
+
+ dyndns['config_file'] = config_file
+ return dyndns
+
+def verify(dyndns):
+ # bail out early - looks like removal from running config
+ if not dyndns:
+ return None
+
+ for address in dyndns['address']:
+ # RFC2136 - configuration validation
+ if 'rfc2136' in dyndns['address'][address]:
+ for config in dyndns['address'][address]['rfc2136'].values():
+ for field in ['host_name', 'zone', 'server', 'key']:
+ if field not in config:
+ raise ConfigError(f'"{field.replace("_", "-")}" is required for RFC2136 '
+ f'based Dynamic DNS service on "{address}"')
+
+ # Dynamic DNS service provider - configuration validation
+ if 'service' in dyndns['address'][address]:
+ for service, config in dyndns['address'][address]['service'].items():
+ error_msg = f'is required for Dynamic DNS service "{service}" on "{address}"'
+
+ for field in ['host_name', 'password', 'protocol']:
+ if field not in config:
+ raise ConfigError(f'"{field.replace("_", "-")}" {error_msg}')
+
+ if config['protocol'] in zone_allowed and 'zone' not in config:
+ raise ConfigError(f'"zone" {error_msg}')
+
+ if config['protocol'] not in zone_allowed and 'zone' in config:
+ raise ConfigError(f'"{config["protocol"]}" does not support "zone"')
+
+ if config['protocol'] not in username_unnecessary:
+ if 'username' not in config:
+ raise ConfigError(f'"username" {error_msg}')
+
+ if config['ip_version'] == 'both':
+ if config['protocol'] not in dualstack_supported:
+ raise ConfigError(f'"{config["protocol"]}" does not support IPv4 and IPv6 at the same time')
+ # dyndns2 protocol in ddclient honors dual stack only for dyn.com (dyndns.org)
+ if config['protocol'] == 'dyndns2' and 'server' in config and config['server'] != 'members.dyndns.org':
+ raise ConfigError(f'"{config["protocol"]}" for "{config["server"]}" does not support IPv4 and IPv6 at the same time')
+
+ return None
+
+def generate(dyndns):
+ # bail out early - looks like removal from running config
+ if not dyndns:
+ return None
+
+ render(config_file, 'dns-dynamic/ddclient.conf.j2', dyndns)
+ render(systemd_override, 'dns-dynamic/override.conf.j2', dyndns)
+ return None
+
+def apply(dyndns):
+ if not dyndns:
+ call('systemctl stop ddclient.service')
+ if os.path.exists(config_file):
+ os.unlink(config_file)
+ else:
+ call('systemctl restart ddclient.service')
+
+ return None
+
+if __name__ == '__main__':
+ try:
+ c = get_config()
+ verify(c)
+ generate(c)
+ apply(c)
+ except ConfigError as e:
+ print(e)
+ exit(1)
diff --git a/src/conf_mode/dynamic_dns.py b/src/conf_mode/dynamic_dns.py
deleted file mode 100755
index 426e3d693..000000000
--- a/src/conf_mode/dynamic_dns.py
+++ /dev/null
@@ -1,156 +0,0 @@
-#!/usr/bin/env python3
-#
-# Copyright (C) 2018-2020 VyOS maintainers and contributors
-#
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the GNU General Public License version 2 or later as
-# published by the Free Software Foundation.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with this program. If not, see <http://www.gnu.org/licenses/>.
-
-import os
-
-from sys import exit
-
-from vyos.config import Config
-from vyos.configdict import dict_merge
-from vyos.template import render
-from vyos.util import call
-from vyos.xml import defaults
-from vyos import ConfigError
-from vyos import airbag
-airbag.enable()
-
-config_file = r'/run/ddclient/ddclient.conf'
-
-# Mapping of service name to service protocol
-default_service_protocol = {
- 'afraid': 'freedns',
- 'changeip': 'changeip',
- 'cloudflare': 'cloudflare',
- 'dnspark': 'dnspark',
- 'dslreports': 'dslreports1',
- 'dyndns': 'dyndns2',
- 'easydns': 'easydns',
- 'namecheap': 'namecheap',
- 'noip': 'noip',
- 'sitelutions': 'sitelutions',
- 'zoneedit': 'zoneedit1'
-}
-
-def get_config(config=None):
- if config:
- conf = config
- else:
- conf = Config()
-
- base_level = ['service', 'dns', 'dynamic']
- if not conf.exists(base_level):
- return None
-
- dyndns = conf.get_config_dict(base_level, key_mangling=('-', '_'), get_first_key=True)
-
- # We have gathered the dict representation of the CLI, but there are default
- # options which we need to update into the dictionary retrived.
- for interface in dyndns['interface']:
- if 'service' in dyndns['interface'][interface]:
- # 'Autodetect' protocol used by DynDNS service
- for service in dyndns['interface'][interface]['service']:
- if service in default_service_protocol:
- dyndns['interface'][interface]['service'][service].update(
- {'protocol' : default_service_protocol.get(service)})
- else:
- dyndns['interface'][interface]['service'][service].update(
- {'custom': ''})
-
- if 'rfc2136' in dyndns['interface'][interface]:
- default_values = defaults(base_level + ['interface', 'rfc2136'])
- for rfc2136 in dyndns['interface'][interface]['rfc2136']:
- dyndns['interface'][interface]['rfc2136'][rfc2136] = dict_merge(
- default_values, dyndns['interface'][interface]['rfc2136'][rfc2136])
-
- return dyndns
-
-def verify(dyndns):
- # bail out early - looks like removal from running config
- if not dyndns:
- return None
-
- # A 'node' corresponds to an interface
- if 'interface' not in dyndns:
- return None
-
- for interface in dyndns['interface']:
- # RFC2136 - configuration validation
- if 'rfc2136' in dyndns['interface'][interface]:
- for rfc2136, config in dyndns['interface'][interface]['rfc2136'].items():
-
- for tmp in ['record', 'zone', 'server', 'key']:
- if tmp not in config:
- raise ConfigError(f'"{tmp}" required for rfc2136 based '
- f'DynDNS service on "{interface}"')
-
- if not os.path.isfile(config['key']):
- raise ConfigError(f'"key"-file not found for rfc2136 based '
- f'DynDNS service on "{interface}"')
-
- # DynDNS service provider - configuration validation
- if 'service' in dyndns['interface'][interface]:
- for service, config in dyndns['interface'][interface]['service'].items():
- error_msg = f'required for DynDNS service "{service}" on "{interface}"'
- if 'host_name' not in config:
- raise ConfigError(f'"host-name" {error_msg}')
-
- if 'login' not in config:
- if service != 'cloudflare' and ('protocol' not in config or config['protocol'] != 'cloudflare'):
- raise ConfigError(f'"login" (username) {error_msg}, unless using CloudFlare')
-
- if 'password' not in config:
- raise ConfigError(f'"password" {error_msg}')
-
- if 'zone' in config:
- if service != 'cloudflare' and ('protocol' not in config or config['protocol'] != 'cloudflare'):
- raise ConfigError(f'"zone" option only supported with CloudFlare')
-
- if 'custom' in config:
- if 'protocol' not in config:
- raise ConfigError(f'"protocol" {error_msg}')
-
- if 'server' not in config:
- raise ConfigError(f'"server" {error_msg}')
-
- return None
-
-def generate(dyndns):
- # bail out early - looks like removal from running config
- if not dyndns:
- return None
-
- render(config_file, 'dynamic-dns/ddclient.conf.j2', dyndns)
- return None
-
-def apply(dyndns):
- if not dyndns:
- call('systemctl stop ddclient.service')
- if os.path.exists(config_file):
- os.unlink(config_file)
- else:
- call('systemctl restart ddclient.service')
-
- return None
-
-if __name__ == '__main__':
- try:
- c = get_config()
- verify(c)
- generate(c)
- apply(c)
- except ConfigError as e:
- print(e)
- exit(1)
diff --git a/src/etc/systemd/system/ddclient.service.d/override.conf b/src/etc/systemd/system/ddclient.service.d/override.conf
deleted file mode 100644
index 09d929d39..000000000
--- a/src/etc/systemd/system/ddclient.service.d/override.conf
+++ /dev/null
@@ -1,11 +0,0 @@
-[Unit]
-After=
-After=vyos-router.service
-
-[Service]
-WorkingDirectory=
-WorkingDirectory=/run/ddclient
-PIDFile=
-PIDFile=/run/ddclient/ddclient.pid
-ExecStart=
-ExecStart=/usr/bin/ddclient -cache /run/ddclient/ddclient.cache -pid /run/ddclient/ddclient.pid -file /run/ddclient/ddclient.conf
diff --git a/src/migration-scripts/dns-dynamic/0-to-1 b/src/migration-scripts/dns-dynamic/0-to-1
new file mode 100755
index 000000000..cf0983b01
--- /dev/null
+++ b/src/migration-scripts/dns-dynamic/0-to-1
@@ -0,0 +1,104 @@
+#!/usr/bin/env python3
+
+# Copyright (C) 2023 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+# T5144:
+# - migrate "service dns dynamic interface ..."
+# to "service dns dynamic address ..."
+# - migrate "service dns dynamic interface <interface> use-web ..."
+# to "service dns dynamic address <address> web-options ..."
+# - migrate "service dns dynamic interface <interface> rfc2136 <config> record ..."
+# to "service dns dynamic address <address> rfc2136 <config> host-name ..."
+# - migrate "service dns dynamic interface <interface> service <config> login ..."
+# to "service dns dynamic address <address> service <config> username ..."
+# - apply global 'ipv6-enable' to per <config> 'ip-version: ipv6'
+# - apply service protocol mapping upfront, they are not 'auto-detected' anymore
+
+import sys
+from vyos.configtree import ConfigTree
+
+service_protocol_mapping = {
+ 'afraid': 'freedns',
+ 'changeip': 'changeip',
+ 'cloudflare': 'cloudflare',
+ 'dnspark': 'dnspark',
+ 'dslreports': 'dslreports1',
+ 'dyndns': 'dyndns2',
+ 'easydns': 'easydns',
+ 'namecheap': 'namecheap',
+ 'noip': 'noip',
+ 'sitelutions': 'sitelutions',
+ 'zoneedit': 'zoneedit1'
+}
+
+if (len(sys.argv) < 1):
+ print("Must specify file name!")
+ sys.exit(1)
+
+file_name = sys.argv[1]
+
+with open(file_name, 'r') as f:
+ config_file = f.read()
+
+config = ConfigTree(config_file)
+
+old_base_path = ['service', 'dns', 'dynamic', 'interface']
+new_base_path = ['service', 'dns', 'dynamic', 'address']
+
+if not config.exists(old_base_path):
+ # Nothing to do
+ sys.exit(0)
+
+# Migrate "service dns dynamic interface"
+# to "service dns dynamic address"
+config.rename(old_base_path, new_base_path[-1])
+
+for address in config.list_nodes(new_base_path):
+ # Migrate "service dns dynamic interface <interface> rfc2136 <config> record"
+ # to "service dns dynamic address <address> rfc2136 <config> host-name"
+ if config.exists(new_base_path + [address, 'rfc2136']):
+ for rfc_cfg in config.list_nodes(new_base_path + [address, 'rfc2136']):
+ if config.exists(new_base_path + [address, 'rfc2136', rfc_cfg, 'record']):
+ config.rename(new_base_path + [address, 'rfc2136', rfc_cfg, 'record'], 'host-name')
+
+ # Migrate "service dns dynamic interface <interface> service <config> login"
+ # to "service dns dynamic address <address> service <config> username"
+ if config.exists(new_base_path + [address, 'service']):
+ for svc_cfg in config.list_nodes(new_base_path + [address, 'service']):
+ if config.exists(new_base_path + [address, 'service', svc_cfg, 'login']):
+ config.rename(new_base_path + [address, 'service', svc_cfg, 'login'], 'username')
+ # Apply global 'ipv6-enable' to per <config> 'ip-version: ipv6'
+ if config.exists(new_base_path + [address, 'ipv6-enable']):
+ config.set(new_base_path + [address, 'service', svc_cfg, 'ip-version'],
+ value='ipv6', replace=False)
+ config.delete(new_base_path + [address, 'ipv6-enable'])
+ # Apply service protocol mapping upfront, they are not 'auto-detected' anymore
+ if svc_cfg in service_protocol_mapping:
+ config.set(new_base_path + [address, 'service', svc_cfg, 'protocol'],
+ value=service_protocol_mapping.get(svc_cfg), replace=False)
+
+ # Migrate "service dns dynamic interface <interface> use-web"
+ # to "service dns dynamic address <address> web-options"
+ # Also, rename <address> to 'web' literal for backward compatibility
+ if config.exists(new_base_path + [address, 'use-web']):
+ config.rename(new_base_path + [address], 'web')
+ config.rename(new_base_path + ['web', 'use-web'], 'web-options')
+
+try:
+ with open(file_name, 'w') as f:
+ f.write(config.to_string())
+except OSError as e:
+ print("Failed to save the modified config: {}".format(e))
+ sys.exit(1)
diff --git a/src/op_mode/dynamic_dns.py b/src/op_mode/dns_dynamic.py
index d41a74db3..d41a74db3 100755
--- a/src/op_mode/dynamic_dns.py
+++ b/src/op_mode/dns_dynamic.py