summaryrefslogtreecommitdiff
path: root/src/conf_mode/dynamic_dns.py
diff options
context:
space:
mode:
Diffstat (limited to 'src/conf_mode/dynamic_dns.py')
-rwxr-xr-xsrc/conf_mode/dynamic_dns.py249
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)