#!/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)