diff options
| author | Christian Breunig <christian@breunig.cc> | 2023-06-05 22:04:21 +0200 | 
|---|---|---|
| committer | GitHub <noreply@github.com> | 2023-06-05 22:04:21 +0200 | 
| commit | 47cce6805562c11b75ef9d5d761881e275d1a27d (patch) | |
| tree | e75bac5474b8f47cf0a37c47fa6acf78e2116e92 /src | |
| parent | 3cfeddebb73e12de15d46d13c44003ede2d72c19 (diff) | |
| parent | c14825f55d286d54ca3c04703ecbded1cb4c2cca (diff) | |
| download | vyos-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-x | src/conf_mode/dns_dynamic.py | 134 | ||||
| -rwxr-xr-x | src/conf_mode/dynamic_dns.py | 156 | ||||
| -rw-r--r-- | src/etc/systemd/system/ddclient.service.d/override.conf | 11 | ||||
| -rwxr-xr-x | src/migration-scripts/dns-dynamic/0-to-1 | 104 | ||||
| -rwxr-xr-x | src/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 | 
