From 1df3827994bfef6c166bdb0f51cc51c212507361 Mon Sep 17 00:00:00 2001
From: Christian Poessinger <christian@poessinger.com>
Date: Fri, 16 Mar 2018 20:09:59 +0100
Subject: Initial CLI interface support for XML DNS forwarder

---
 src/conf-mode/vyos-config-dns-forwarding.py | 45 +++++++++++++++++++++++++++++
 1 file changed, 45 insertions(+)
 create mode 100755 src/conf-mode/vyos-config-dns-forwarding.py

(limited to 'src')

diff --git a/src/conf-mode/vyos-config-dns-forwarding.py b/src/conf-mode/vyos-config-dns-forwarding.py
new file mode 100755
index 000000000..27e7a5ef5
--- /dev/null
+++ b/src/conf-mode/vyos-config-dns-forwarding.py
@@ -0,0 +1,45 @@
+#!/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 sys
+import os
+
+from vyos.config import Config
+from vyos.util import ConfigError
+
+def get_config():
+    return None
+
+def verify(dns):
+    return None
+
+def generate(dns):
+    return None
+
+def apply(dns):
+    return None
+
+if __name__ == '__main__':
+    try:
+        c = get_config()
+        verify(c)
+        generate(c)
+        apply(c)
+    except ConfigError as e:
+        print(e)
+        sys.exit(1)
-- 
cgit v1.2.3


From 28c3a6385143c97c3255f2a476523b103d3b830d Mon Sep 17 00:00:00 2001
From: Christian Poessinger <christian@poessinger.com>
Date: Fri, 16 Mar 2018 21:00:43 +0100
Subject: Initial working version for XML interface to configure dnsmasq

---
 Makefile                                    |   1 +
 src/conf-mode/vyos-config-dns-forwarding.py | 119 +++++++++++++++++++++++++++-
 2 files changed, 119 insertions(+), 1 deletion(-)

(limited to 'src')

diff --git a/Makefile b/Makefile
index f5800da43..de7c136eb 100644
--- a/Makefile
+++ b/Makefile
@@ -10,6 +10,7 @@ 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)/service/node.def
+	rm -f $(TMPL_DIR)/service/dns/node.def
 	rm -f $(TMPL_DIR)/protocols/node.def
 
 .PHONY: all
diff --git a/src/conf-mode/vyos-config-dns-forwarding.py b/src/conf-mode/vyos-config-dns-forwarding.py
index 27e7a5ef5..cdbd89eba 100755
--- a/src/conf-mode/vyos-config-dns-forwarding.py
+++ b/src/conf-mode/vyos-config-dns-forwarding.py
@@ -18,20 +18,137 @@
 
 import sys
 import os
+import time
 
 from vyos.config import Config
 from vyos.util import ConfigError
 
+config_file = r'/etc/dnsmasq.d/vyos.conf'
+
+# borrowed from: https://github.com/donjajo/py-world/blob/master/resolvconfReader.py, THX!
+def get_resolvers(file):
+    resolvers = []
+    try:
+        with open(file, 'r') as resolvconf:
+            for line in resolvconf.readlines():
+                line = line.split('#',1)[0];
+                line = line.rstrip();
+                if 'nameserver' in line:
+                    resolvers.append(line.split()[1])
+        return resolvers
+    except IOError as error:
+        return error.strerror
+
 def get_config():
-    return None
+    dns = {}
+    conf = Config()
+    conf.set_level('service dns forwarding')
+    if not conf.exists(''):
+        return dns
+
+    if conf.exists('cache-size'):
+        cache = conf.return_value('cache-size')
+        dns.setdefault('cache-size', cache)
+
+    if conf.exists('dhcp'):
+        dns.setdefault('dhcp', [])
+        interfaces = []
+        interfaces = conf.return_values('dhcp')
+        for interface in interfaces:
+            resolvers = get_resolvers("/etc/resolv.conf.dhclient-new-{0}".format(interface))
+            dhcp = {
+                "interface": interface,
+                "resolvers": resolvers
+            }
+            dns['dhcp'].append(dhcp)
+
+    if conf.exists('domain'):
+        dns.setdefault('domain', [])
+        for node in conf.list_nodes('domain'):
+            server = conf.return_value("domain {0} server".format(node))
+            domain = {
+                "name": node,
+                "server": server
+            }
+            dns['domain'].append(domain)
+
+    if conf.exists('ignore-hosts-file'):
+        dns.setdefault('ignore-hosts-file', True)
+
+    if conf.exists('listen-on'):
+        interfaces = []
+        interfaces = conf.return_values('listen-on')
+        dns.setdefault('listen-on', interfaces)
+
+    if conf.exists('name-server'):
+        nameservers = []
+        nameservers = conf.return_values('name-server')
+        dns.setdefault('name-server', nameservers)
+
+    if conf.exists('system'):
+        conf.set_level('system')
+        nameservers = []
+        nameservers = conf.return_values('name-server')
+        if len(nameservers) == 0:
+            print("DNS forwarding warning: No name-servers set under 'system name-server'\n")
+        dns.setdefault('system-name-server', True)
+
+    return dns
 
 def verify(dns):
+    if 'listen-on' not in dns.keys():
+        raise ConfigError("Error: DNS forwarding requires a configured listen interface!")
+
     return None
 
 def generate(dns):
+    config_header = '### Autogenerated by vyos-config-dns-forwarding.py on {tm} ###\n'.format(tm=time.strftime("%a, %d %b %Y %H:%M:%S", time.localtime()))
+
+    # write new configuration file
+    f = open(config_file, 'w')
+    f.write(config_header)
+    f.write("log-facility=/var/log/dnsmasq.log\n")
+    f.write("no-poll\n")
+    f.write("edns-packet-max=4096\n")
+    f.write("bind-interfaces\n")
+
+    if 'listen-on' in dns.keys():
+        for interface in dns['listen-on']:
+            f.write("interface={0}\n".format(interface))
+
+    if 'dhcp' in dns.keys():
+        for dhcp in dns['dhcp']:
+            for resolver in dhcp['resolvers']:
+                f.write("server={0}\t# dhcp {1}\n".format(resolver, dhcp['interface']))
+
+    if 'domain' in dns.keys():
+        for domain in dns['domain']:
+            f.write("server=/{0}/{1}\t# domain-override\n".format(domain['name'], domain['server']))
+
+    if 'cache-size' in dns.keys():
+        f.write("cache-size={0}\n".format(dns['cache-size']))
+
+    if 'ignore-hosts-file' in dns.keys():
+        f.write("no-hosts\n")
+
+    if 'name-server' in dns.keys():
+        for nameserver in dns['name-server']:
+            f.write("server={0}\t# statically configured\n".format(nameserver))
+
+    if 'system-name-server' in dns.keys():
+        # Read the IP addresses of the upstream nameservers from /etc/resolv.conf
+        f.write("resolv-file=/etc/resolv.conf\n")
+
+    f.close()
     return None
 
 def apply(dns):
+    if len(dns) == 0:
+        cmd = "sudo systemctl stop dnsmasq"
+    else:
+        cmd = "sudo systemctl restart dnsmasq"
+
+    os.system(cmd)
     return None
 
 if __name__ == '__main__':
-- 
cgit v1.2.3


From d08fbff714dbff0ae7b914a55e58ee141042c4f0 Mon Sep 17 00:00:00 2001
From: Christian Poessinger <christian@poessinger.com>
Date: Fri, 16 Mar 2018 21:40:41 +0100
Subject: dns-forwarding.py: add missing 'query-all-servers' path

---
 src/conf-mode/vyos-config-dns-forwarding.py | 11 +++++++++--
 1 file changed, 9 insertions(+), 2 deletions(-)

(limited to 'src')

diff --git a/src/conf-mode/vyos-config-dns-forwarding.py b/src/conf-mode/vyos-config-dns-forwarding.py
index cdbd89eba..742e111cb 100755
--- a/src/conf-mode/vyos-config-dns-forwarding.py
+++ b/src/conf-mode/vyos-config-dns-forwarding.py
@@ -85,6 +85,9 @@ def get_config():
         nameservers = conf.return_values('name-server')
         dns.setdefault('name-server', nameservers)
 
+    if conf.exists('query-all-servers'):
+        dns.setdefault('query-all-servers', True)
+
     if conf.exists('system'):
         conf.set_level('system')
         nameservers = []
@@ -96,8 +99,9 @@ def get_config():
     return dns
 
 def verify(dns):
-    if 'listen-on' not in dns.keys():
-        raise ConfigError("Error: DNS forwarding requires a configured listen interface!")
+    if len(dns) > 0:
+        if 'listen-on' not in dns.keys():
+            raise ConfigError("Error: DNS forwarding requires a configured listen interface!")
 
     return None
 
@@ -135,6 +139,9 @@ def generate(dns):
         for nameserver in dns['name-server']:
             f.write("server={0}\t# statically configured\n".format(nameserver))
 
+    if 'query-all-servers' in dns.keys():
+        f.write("all-servers\n")
+
     if 'system-name-server' in dns.keys():
         # Read the IP addresses of the upstream nameservers from /etc/resolv.conf
         f.write("resolv-file=/etc/resolv.conf\n")
-- 
cgit v1.2.3


From 25a57a9f727f908e0027a598d56eaa6375ac5c25 Mon Sep 17 00:00:00 2001
From: Christian Poessinger <christian@poessinger.com>
Date: Fri, 13 Apr 2018 20:38:03 +0200
Subject: T560: dns-forwarding: replace dnsmasq with pdns-recursor

---
 interface-definitions/dns-forwarding.xml    |  9 +---
 src/conf-mode/vyos-config-dns-forwarding.py | 84 +++++++++++++++++++----------
 2 files changed, 58 insertions(+), 35 deletions(-)

(limited to 'src')

diff --git a/interface-definitions/dns-forwarding.xml b/interface-definitions/dns-forwarding.xml
index f1edec414..d56911c60 100644
--- a/interface-definitions/dns-forwarding.xml
+++ b/interface-definitions/dns-forwarding.xml
@@ -32,7 +32,6 @@
               <tagNode name="domain">
                 <properties>
                   <help>DNS domain to forward to a local server</help>
-                  <type>txt</type>
                 </properties>
                 <children>
                   <leafNode name="server">
@@ -77,12 +76,8 @@
                     <format>ipv6</format>
                     <description>Domain Name Server (DNS) IPv6 address</description>
                   </valueHelp>
-                <multi/>
-                </properties>
-              </leafNode>
-              <leafNode name="query-all-servers">
-                <properties>
-                  <help>Query all DNS servers, respond and cache fastest result</help>
+                  <multi/>
+                  <type>ipv4,ipv6</type>
                 </properties>
               </leafNode>
               <leafNode name="system">
diff --git a/src/conf-mode/vyos-config-dns-forwarding.py b/src/conf-mode/vyos-config-dns-forwarding.py
index 742e111cb..7a8aed75d 100755
--- a/src/conf-mode/vyos-config-dns-forwarding.py
+++ b/src/conf-mode/vyos-config-dns-forwarding.py
@@ -19,11 +19,12 @@
 import sys
 import os
 import time
+import netifaces
 
 from vyos.config import Config
 from vyos.util import ConfigError
 
-config_file = r'/etc/dnsmasq.d/vyos.conf'
+config_file = r'/etc/powerdns/recursor.conf'
 
 # borrowed from: https://github.com/donjajo/py-world/blob/master/resolvconfReader.py, THX!
 def get_resolvers(file):
@@ -65,7 +66,7 @@ def get_config():
     if conf.exists('domain'):
         dns.setdefault('domain', [])
         for node in conf.list_nodes('domain'):
-            server = conf.return_value("domain {0} server".format(node))
+            server = conf.return_values("domain {0} server".format(node))
             domain = {
                 "name": node,
                 "server": server
@@ -85,75 +86,102 @@ def get_config():
         nameservers = conf.return_values('name-server')
         dns.setdefault('name-server', nameservers)
 
-    if conf.exists('query-all-servers'):
-        dns.setdefault('query-all-servers', True)
-
     if conf.exists('system'):
         conf.set_level('system')
         nameservers = []
         nameservers = conf.return_values('name-server')
         if len(nameservers) == 0:
             print("DNS forwarding warning: No name-servers set under 'system name-server'\n")
-        dns.setdefault('system-name-server', True)
+        else:
+            dns.setdefault('system-name-server', nameservers)
 
     return dns
 
 def verify(dns):
     if len(dns) > 0:
         if 'listen-on' not in dns.keys():
-            raise ConfigError("Error: DNS forwarding requires a configured listen interface!")
+            raise ConfigError('Error: DNS forwarding requires a configured listen interface!')
+
+        for interface in dns['listen-on']:
+            try:
+                netifaces.ifaddresses(interface)[netifaces.AF_INET]
+            except KeyError as e:
+                raise ConfigError('Error: Interface {0} has no IP address assigned'.format(interface))
+
+        if 'domain' in dns.keys():
+            for domain in dns['domain']:
+                if len(domain['server']) == 0:
+                    raise ConfigError('Error: No server configured for domain {0}'.format(domain['name']))
 
     return None
 
 def generate(dns):
     config_header = '### Autogenerated by vyos-config-dns-forwarding.py on {tm} ###\n'.format(tm=time.strftime("%a, %d %b %Y %H:%M:%S", time.localtime()))
 
+    fwds = []
+    fwds_src = []
+
     # write new configuration file
     f = open(config_file, 'w')
     f.write(config_header)
-    f.write("log-facility=/var/log/dnsmasq.log\n")
-    f.write("no-poll\n")
-    f.write("edns-packet-max=4096\n")
-    f.write("bind-interfaces\n")
+    f.write('daemon=yes\n')
+    f.write('threads=1\n')
+    f.write('allow-from=0.0.0.0/0\n')
+    f.write('log-common-errors=yes\n')
 
     if 'listen-on' in dns.keys():
+        listen4 = []
+        listen6 = []
         for interface in dns['listen-on']:
-            f.write("interface={0}\n".format(interface))
+            addrs = netifaces.ifaddresses(interface)
+            for ip4 in addrs[netifaces.AF_INET]:
+                listen4.append(ip4['addr'])
+
+            for ip6 in addrs[netifaces.AF_INET6]:
+                listen6.append(ip6['addr'])
+
+        # build local-address string, example:
+        # local-address=172.16.37.240, 127.0.0.1, 172.16.254.35, 172.16.77.1, 2001:DB8::1:25, ::1
+        f.write('local-address=' + ', '.join(listen4) + ', ' + ', '.join(listen6) + '\n')
+
+    if 'cache-size' in dns.keys():
+        f.write("max-cache-entries={0}\n".format(dns['cache-size']))
 
     if 'dhcp' in dns.keys():
         for dhcp in dns['dhcp']:
-            for resolver in dhcp['resolvers']:
-                f.write("server={0}\t# dhcp {1}\n".format(resolver, dhcp['interface']))
+            fwds_src.append('DHCP: {0} ({1})'.format(dhcp['interface'], ', '.join(str(ns) for ns in dhcp['resolvers'])))
+            fwds.append(', '.join(str(ns) for ns in dhcp['resolvers']))
 
     if 'domain' in dns.keys():
+        zones = []
         for domain in dns['domain']:
-            f.write("server=/{0}/{1}\t# domain-override\n".format(domain['name'], domain['server']))
-
-    if 'cache-size' in dns.keys():
-        f.write("cache-size={0}\n".format(dns['cache-size']))
+            zones.append('{0}={1}'.format(domain['name'], ';'.join(str(ns) for ns in domain['server'])))
+        f.write('forward-zones=' + ', '.join(zones) + '\n')
 
     if 'ignore-hosts-file' in dns.keys():
-        f.write("no-hosts\n")
+        f.write("export-etc-hosts=no\n")
 
     if 'name-server' in dns.keys():
-        for nameserver in dns['name-server']:
-            f.write("server={0}\t# statically configured\n".format(nameserver))
-
-    if 'query-all-servers' in dns.keys():
-        f.write("all-servers\n")
+        fwds_src.append('statically configured: {0}'.format(', '.join(str(ns) for ns in dns['name-server'])))
+        fwds.append(', '.join(str(ns) for ns in dns['name-server']))
 
     if 'system-name-server' in dns.keys():
-        # Read the IP addresses of the upstream nameservers from /etc/resolv.conf
-        f.write("resolv-file=/etc/resolv.conf\n")
+        fwds_src.append('system: {0}'.format(', '.join(str(ns) for ns in dns['system-name-server'])))
+        fwds.append(', '.join(str(ns) for ns in dns['system-name-server']))
+
+    if len(fwds) > 0:
+        f.write('\n')
+        f.write('# ' + '\n# '.join(fwds_src) + '\n')
+        f.write('forward-zones-recurse=.=' + '; '.join(fwds) + '\n')
 
     f.close()
     return None
 
 def apply(dns):
     if len(dns) == 0:
-        cmd = "sudo systemctl stop dnsmasq"
+        cmd = "sudo systemctl stop pdns-recursor"
     else:
-        cmd = "sudo systemctl restart dnsmasq"
+        cmd = "sudo systemctl restart pdns-recursor"
 
     os.system(cmd)
     return None
-- 
cgit v1.2.3