summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Makefile2
-rw-r--r--interface-definitions/dynamic-dns.xml224
-rwxr-xr-xsrc/conf_mode/dynamic_dns.py260
3 files changed, 484 insertions, 2 deletions
diff --git a/Makefile b/Makefile
index 7447275ca..5e0cbc910 100644
--- a/Makefile
+++ b/Makefile
@@ -11,8 +11,6 @@ interface_definitions:
# XXX: delete top level node.def's that now live in other packages
rm -f $(TMPL_DIR)/system/node.def
rm -f $(TMPL_DIR)/system/options/node.def
- rm -f $(TMPL_DIR)/service/node.def
- rm -f $(TMPL_DIR)/service/dns/node.def
rm -f $(TMPL_DIR)/protocols/node.def
.PHONY: op_mode_definitions
diff --git a/interface-definitions/dynamic-dns.xml b/interface-definitions/dynamic-dns.xml
new file mode 100644
index 000000000..e0b2cf764
--- /dev/null
+++ b/interface-definitions/dynamic-dns.xml
@@ -0,0 +1,224 @@
+<?xml version="1.0"?>
+<!-- Dynamic DNS configuration -->
+<interfaceDefinition>
+ <node name="service">
+ <children>
+ <node name="dns">
+ <children>
+ <node name="dynamic" owner="${vyos_conf_scripts_dir}/dynamic_dns.py">
+ <properties>
+ <help>Dynamic DNS</help>
+ <priority>919</priority>
+ </properties>
+ <children>
+ <tagNode name="interface">
+ <properties>
+ <help>Interface to send DDNS updates for [REQUIRED]</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_interfaces.py</script>
+ </completionHelp>
+ </properties>
+ <children>
+ <tagNode name="rfc2136">
+ <properties>
+ <help>RFC2136 Update name</help>
+ </properties>
+ <children>
+ <leafNode name="key">
+ <properties>
+ <help>File containing the secret key shared with remote DNS server [REQUIRED]</help>
+ <valueHelp>
+ <format>file</format>
+ <description>File in /config/auth directory</description>
+ </valueHelp>
+ </properties>
+ </leafNode>
+ <leafNode name="record">
+ <properties>
+ <help>Record to be updated [REQUIRED]</help>
+ <multi/>
+ </properties>
+ </leafNode>
+ <leafNode name="server">
+ <properties>
+ <help>Server to be updated [REQUIRED]</help>
+ </properties>
+ </leafNode>
+ <leafNode name="ttl">
+ <properties>
+ <help>Time To Live (default: 600)</help>
+ <valueHelp>
+ <format>1-86400</format>
+ <description>DNS forwarding cache size</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-86400"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="zone">
+ <properties>
+ <help>Zone to be updated [REQUIRED]</help>
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
+ <tagNode name="service">
+ <properties>
+ <help>Service being used for Dynamic DNS [REQUIRED]</help>
+ <valueHelp>
+ <format>custom</format>
+ <description>Custom or predefined service</description>
+ </valueHelp>
+ <valueHelp>
+ <format>afraid</format>
+ <description></description>
+ </valueHelp>
+ <valueHelp>
+ <format>changeip</format>
+ <description></description>
+ </valueHelp>
+ <valueHelp>
+ <format>cloudflare</format>
+ <description></description>
+ </valueHelp>
+ <valueHelp>
+ <format>dnspark</format>
+ <description></description>
+ </valueHelp>
+ <valueHelp>
+ <format>dslreports</format>
+ <description></description>
+ </valueHelp>
+ <valueHelp>
+ <format>dyndns</format>
+ <description></description>
+ </valueHelp>
+ <valueHelp>
+ <format>easydns</format>
+ <description></description>
+ </valueHelp>
+ <valueHelp>
+ <format>namecheap</format>
+ <description></description>
+ </valueHelp>
+ <valueHelp>
+ <format>noip</format>
+ <description></description>
+ </valueHelp>
+ <valueHelp>
+ <format>sitelutions</format>
+ <description></description>
+ </valueHelp>
+ <valueHelp>
+ <format>zoneedit</format>
+ <description></description>
+ </valueHelp>
+ </properties>
+ <children>
+ <leafNode name="host-name">
+ <properties>
+ <help>Hostname registered with DDNS service [REQUIRED]</help>
+ <multi/>
+ </properties>
+ </leafNode>
+ <leafNode name="login">
+ <properties>
+ <help>Login for DDNS service [REQUIRED]</help>
+ </properties>
+ </leafNode>
+ <leafNode name="password">
+ <properties>
+ <help>Password for DDNS service [REQUIRED]</help>
+ </properties>
+ </leafNode>
+ <leafNode name="protocol">
+ <properties>
+ <help>ddclient protocol used for DDNS service [REQUIRED FOR CUSTOM]</help>
+ <valueHelp>
+ <format>protocol</format>
+ <description>ddclient protocol</description>
+ </valueHelp>
+ <valueHelp>
+ <format>changeip</format>
+ <description></description>
+ </valueHelp>
+ <valueHelp>
+ <format>cloudflare</format>
+ <description></description>
+ </valueHelp>
+ <valueHelp>
+ <format>dnspark</format>
+ <description></description>
+ </valueHelp>
+ <valueHelp>
+ <format>dslreports1</format>
+ <description></description>
+ </valueHelp>
+ <valueHelp>
+ <format>dyndns2</format>
+ <description></description>
+ </valueHelp>
+ <valueHelp>
+ <format>easydns</format>
+ <description></description>
+ </valueHelp>
+ <valueHelp>
+ <format>namecheap</format>
+ <description></description>
+ </valueHelp>
+ <valueHelp>
+ <format>noip</format>
+ <description></description>
+ </valueHelp>
+ <valueHelp>
+ <format>sitelutions</format>
+ <description></description>
+ </valueHelp>
+ <valueHelp>
+ <format>zoneedit1</format>
+ <description></description>
+ </valueHelp>
+ </properties>
+ </leafNode>
+ <leafNode name="server">
+ <properties>
+ <help>Server to send DDNS update to [REQUIRED FOR CUSTOM]</help>
+ <valueHelp>
+ <format>IPv4</format>
+ <description>IP address of DDNS server</description>
+ </valueHelp>
+ <valueHelp>
+ <format>FQDN</format>
+ <description>Hostname of DDNS server</description>
+ </valueHelp>
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
+ <node name="use-web">
+ <properties>
+ <help>Web check used for obtaining the external IP address</help>
+ </properties>
+ <children>
+ <leafNode name="skip">
+ <properties>
+ <help>Skip everything before this on the given URL</help>
+ </properties>
+ </leafNode>
+ <leafNode name="url">
+ <properties>
+ <help>URL to obtain the current external IP address</help>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </tagNode>
+ </children>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition>
diff --git a/src/conf_mode/dynamic_dns.py b/src/conf_mode/dynamic_dns.py
new file mode 100755
index 000000000..90d4ff567
--- /dev/null
+++ b/src/conf_mode/dynamic_dns.py
@@ -0,0 +1,260 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2018 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
+import sys
+import jinja2
+
+from vyos.config import Config
+from vyos import ConfigError
+
+config_file = r'/etc/ddclient.conf'
+
+config_tmpl = """
+### Autogenerated by dynamic_dns.py ###
+daemon=1m
+syslog=yes
+ssl=yes
+pid=/var/run/ddclient/ddclient.pid
+cache=/var/cache/ddclient/ddclient.cache
+
+{% for interface in interfaces -%}
+
+#
+# ddclient configuration for interface "{{ interface.interface }}":
+#
+{% if interface.web_url and interface.web_skip -%}
+use=web, web={{ interface.web_url}}, web-skip={{ interface.web_skip }}
+{% else -%}
+use=if, if={{ interface.interface }}
+{% endif -%}
+
+{% for rfc in interface.rfc2136 -%}
+{% for record in rfc.record %}
+# RFC2136 dynamic DNS configuration for {{ record }}.{{ rfc.zone }}
+server={{ rfc.server }}
+protocol=nsupdate
+password={{ rfc.keyfile }}
+ttl={{ rfc.ttl }}
+zone={{ rfc.zone }}
+{{ record }}
+{% endfor -%}
+{% endfor -%}
+
+{% for srv in interface.service %}
+{% for host in srv.host %}
+# DynDNS provider configuration for {{ host }}
+protocol={{ srv.protocol }}
+max-interval=28d
+login={{ srv.login }}
+password='{{ srv.password }}'
+{{ host }}
+{% endfor %}
+{% endfor %}
+
+{% endfor %}
+"""
+
+# 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': [],
+}
+
+def get_config():
+ dyndns = default_config_data
+ conf = Config()
+ if not conf.exists('service dns dynamic'):
+ return None
+ else:
+ conf.set_level('service dns dynamic')
+
+ for interface in conf.list_nodes('interface'):
+ node = {
+ 'interface': interface,
+ 'rfc2136': [],
+ 'service': [],
+ 'web_skip': '',
+ 'web_url': ''
+ }
+
+ # set config level to e.g. "service dns dynamic interface eth0"
+ conf.set_level('service dns dynamic interface {0}'.format(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': ''
+ }
+
+ if conf.exists('rfc2136 {0} key'.format(rfc2136)):
+ rfc['keyfile'] = conf.return_value('rfc2136 {0} key'.format(rfc2136))
+
+ if conf.exists('rfc2136 {0} record'.format(rfc2136)):
+ rfc['record'] = conf.return_values('rfc2136 {0} record'.format(rfc2136))
+
+ if conf.exists('rfc2136 {0} server'.format(rfc2136)):
+ rfc['server'] = conf.return_value('rfc2136 {0} server'.format(rfc2136))
+
+ if conf.exists('rfc2136 {0} ttl'.format(rfc2136)):
+ rfc['ttl'] = conf.return_value('rfc2136 {0} ttl'.format(rfc2136))
+
+ if conf.exists('rfc2136 {0} zone'.format(rfc2136)):
+ rfc['zone'] = conf.return_value('rfc2136 {0} zone'.format(rfc2136))
+
+ node['rfc2136'].append(rfc)
+
+ # Handle DynDNS service providers
+ for service in conf.list_nodes('service'):
+ srv = {
+ 'provider': service,
+ 'host': [],
+ 'login': '',
+ 'password': '',
+ 'protocol': '',
+ 'server': '',
+ 'custom' : False
+ }
+
+ # 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('service {0} login'.format(service)):
+ srv['login'] = conf.return_value('service {0} login'.format(service))
+
+ if conf.exists('service {0} host-name'.format(service)):
+ srv['host'] = conf.return_values('service {0} host-name'.format(service))
+
+ if conf.exists('service {0} protocol'.format(service)):
+ srv['protocol'] = conf.return_value('service {0} protocol'.format(service))
+
+ if conf.exists('service {0} password'.format(service)):
+ srv['password'] = conf.return_value('service {0} password'.format(service))
+
+ if conf.exists('service {0} server'.format(service)):
+ srv['server'] = conf.return_value('service {0} server'.format(service))
+
+ node['service'].append(srv)
+
+ # 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')
+
+ dyndns['interfaces'].append(node)
+
+ return dyndns
+
+def verify(dyndns):
+ # bail out early - looks like removal from running config
+ if dyndns is None:
+ 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']))
+
+ return None
+
+def generate(dyndns):
+ # bail out early - looks like removal from running config
+ if dyndns is None:
+ return None
+
+ tmpl = jinja2.Template(config_tmpl)
+
+ config_text = tmpl.render(dyndns)
+ with open(config_file, 'w') as f:
+ f.write(config_text)
+
+ return None
+
+def apply(dyndns):
+ if dyndns is None:
+ os.system('/etc/init.d/ddclient stop')
+ else:
+ os.system('/etc/init.d/ddclient restart')
+
+ return None
+
+if __name__ == '__main__':
+ try:
+ c = get_config()
+ verify(c)
+ generate(c)
+ apply(c)
+ except ConfigError as e:
+ print(e)
+ sys.exit(1)