diff options
Diffstat (limited to 'src/conf_mode/dynamic_dns.py')
-rwxr-xr-x | src/conf_mode/dynamic_dns.py | 249 |
1 files changed, 249 insertions, 0 deletions
diff --git a/src/conf_mode/dynamic_dns.py b/src/conf_mode/dynamic_dns.py new file mode 100755 index 000000000..5b1883c03 --- /dev/null +++ b/src/conf_mode/dynamic_dns.py @@ -0,0 +1,249 @@ +#!/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 copy import deepcopy +from stat import S_IRUSR, S_IWUSR + +from vyos.config import Config +from vyos import ConfigError +from vyos.util import call +from vyos.template import render + +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' +} + +default_config_data = { + 'interfaces': [], + 'deleted': False +} + +def get_config(): + dyndns = deepcopy(default_config_data) + conf = Config() + base_level = ['service', 'dns', 'dynamic'] + + if not conf.exists(base_level): + dyndns['deleted'] = True + return dyndns + + for interface in conf.list_nodes(base_level + ['interface']): + node = { + 'interface': interface, + 'rfc2136': [], + 'service': [], + 'web_skip': '', + 'web_url': '' + } + + # set config level to e.g. "service dns dynamic interface eth0" + conf.set_level(base_level + ['interface', interface]) + # Handle RFC2136 - Dynamic Updates in the Domain Name System + for rfc2136 in conf.list_nodes(['rfc2136']): + rfc = { + 'name': rfc2136, + 'keyfile': '', + 'record': [], + 'server': '', + 'ttl': '600', + 'zone': '' + } + + # set config level + conf.set_level(base_level + ['interface', interface, 'rfc2136', rfc2136]) + + if conf.exists(['key']): + rfc['keyfile'] = conf.return_value(['key']) + + if conf.exists(['record']): + rfc['record'] = conf.return_values(['record']) + + if conf.exists(['server']): + rfc['server'] = conf.return_value(['server']) + + if conf.exists(['ttl']): + rfc['ttl'] = conf.return_value(['ttl']) + + if conf.exists(['zone']): + rfc['zone'] = conf.return_value(['zone']) + + node['rfc2136'].append(rfc) + + # set config level to e.g. "service dns dynamic interface eth0" + conf.set_level(base_level + ['interface', interface]) + # Handle DynDNS service providers + for service in conf.list_nodes(['service']): + srv = { + 'provider': service, + 'host': [], + 'login': '', + 'password': '', + 'protocol': '', + 'server': '', + 'custom' : False, + 'zone' : '' + } + + # set config level + conf.set_level(base_level + ['interface', interface, 'service', service]) + + # preload protocol from default service mapping + if service in default_service_protocol.keys(): + srv['protocol'] = default_service_protocol[service] + else: + srv['custom'] = True + + if conf.exists(['login']): + srv['login'] = conf.return_value(['login']) + + if conf.exists(['host-name']): + srv['host'] = conf.return_values(['host-name']) + + if conf.exists(['protocol']): + srv['protocol'] = conf.return_value(['protocol']) + + if conf.exists(['password']): + srv['password'] = conf.return_value(['password']) + + if conf.exists(['server']): + srv['server'] = conf.return_value(['server']) + + if conf.exists(['zone']): + srv['zone'] = conf.return_value(['zone']) + elif srv['provider'] == 'cloudflare': + # default populate zone entry with bar.tld if + # host-name is foo.bar.tld + srv['zone'] = srv['host'][0].split('.',1)[1] + + node['service'].append(srv) + + # Set config back to appropriate level for these options + conf.set_level(base_level + ['interface', interface]) + + # Additional settings in CLI + if conf.exists(['use-web', 'skip']): + node['web_skip'] = conf.return_value(['use-web', 'skip']) + + if conf.exists(['use-web', 'url']): + node['web_url'] = conf.return_value(['use-web', 'url']) + + # set config level back to top level + conf.set_level(base_level) + + dyndns['interfaces'].append(node) + + return dyndns + +def verify(dyndns): + # bail out early - looks like removal from running config + if dyndns['deleted']: + return None + + # A 'node' corresponds to an interface + for node in dyndns['interfaces']: + + # RFC2136 - configuration validation + for rfc2136 in node['rfc2136']: + if not rfc2136['record']: + raise ConfigError('Set key for service "{0}" to send DDNS updates for interface "{1}"'.format(rfc2136['name'], node['interface'])) + + if not rfc2136['zone']: + raise ConfigError('Set zone for service "{0}" to send DDNS updates for interface "{1}"'.format(rfc2136['name'], node['interface'])) + + if not rfc2136['keyfile']: + raise ConfigError('Set keyfile for service "{0}" to send DDNS updates for interface "{1}"'.format(rfc2136['name'], node['interface'])) + else: + if not os.path.isfile(rfc2136['keyfile']): + raise ConfigError('Keyfile for service "{0}" to send DDNS updates for interface "{1}" does not exist'.format(rfc2136['name'], node['interface'])) + + if not rfc2136['server']: + raise ConfigError('Set server for service "{0}" to send DDNS updates for interface "{1}"'.format(rfc2136['name'], node['interface'])) + + # DynDNS service provider - configuration validation + for service in node['service']: + if not service['host']: + raise ConfigError('Set host-name for service "{0}" to send DDNS updates for interface "{1}"'.format(service['provider'], node['interface'])) + + if not service['login']: + raise ConfigError('Set login for service "{0}" to send DDNS updates for interface "{1}"'.format(service['provider'], node['interface'])) + + if not service['password']: + raise ConfigError('Set password for service "{0}" to send DDNS updates for interface "{1}"'.format(service['provider'], node['interface'])) + + if service['custom'] is True: + if not service['protocol']: + raise ConfigError('Set protocol for service "{0}" to send DDNS updates for interface "{1}"'.format(service['provider'], node['interface'])) + + if not service['server']: + raise ConfigError('Set server for service "{0}" to send DDNS updates for interface "{1}"'.format(service['provider'], node['interface'])) + + if service['zone']: + if service['provider'] != 'cloudflare': + raise ConfigError('Zone option not allowed for "{0}", it can only be used for CloudFlare'.format(service['provider'])) + + return None + +def generate(dyndns): + # bail out early - looks like removal from running config + if dyndns['deleted']: + return None + + render(config_file, 'dynamic-dns/ddclient.conf.tmpl', dyndns) + + # Config file must be accessible only by its owner + os.chmod(config_file, S_IRUSR | S_IWUSR) + + return None + +def apply(dyndns): + if dyndns['deleted']: + 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) |