Add support for forward/reverse DNS range split

- Allow for explicit reverse RFC1918 lookups with unbound_reverse_zones
- Add tests, similar to NSD ones
- Wait for network to be online before starting (fixes AETHER-1041)
- Multiplatform support

Change-Id: I385aec6f766b45a9db595d2f8af5ed8fe0dde2ca
diff --git a/README.md b/README.md
index 1e8d982..12cc7b0 100644
--- a/README.md
+++ b/README.md
@@ -14,7 +14,10 @@
 
 ## Configuration
 
-
+See the NSD role for descriptions of `dns_forward_zones` and
+`dns_reverse_zones` - if those are defined, then Unbound will forward queries
+for those zones to the NSD server - by default, it's assume to be running on
+  the same host, listening at 127.0.0.1:53.
 
 If you're using a network that already has DNS servers, or you want to use
 specific DNS servers external to the network you can specify the zones they
@@ -29,6 +32,9 @@
       - "8.8.4.4"
 ```
 
+Also set `unbound_reverse_zones` if you want to allow lookups against other
+RFC1918 ip ranges - by default Unbound will reply with NXDOMAIN for reverse
+lookups of addresses in those ranges.
 
 ## Example Playbook
 
diff --git a/defaults/main.yml b/defaults/main.yml
index bf988ec..725181c 100644
--- a/defaults/main.yml
+++ b/defaults/main.yml
@@ -20,5 +20,9 @@
 # Forwarders
 unbound_forward_zones: []
 
-# DNS zones
-dns_zones: {}
+# Reverse zones, used to allow queries against RFC1918 domains
+unbound_reverse_zones: []
+
+# DNS forward/reverse zones, see also NSD role
+dns_forward_zones: {}
+dns_reverse_zones: {}
diff --git a/files/network_online.conf b/files/network_online.conf
new file mode 100644
index 0000000..5d61ced
--- /dev/null
+++ b/files/network_online.conf
@@ -0,0 +1,5 @@
+# SPDX-FileCopyrightText: © 2020 Open Networking Foundation <support@opennetworking.org>
+# SPDX-License-Identifier: Apache-2.0
+[Unit]
+After=network-online.target
+Wants=network-online.target
diff --git a/molecule/default/molecule.yml b/molecule/default/molecule.yml
index 435ea97..d6f6823 100644
--- a/molecule/default/molecule.yml
+++ b/molecule/default/molecule.yml
@@ -22,10 +22,10 @@
     host_vars:
       ubuntu-18.04-priv:
         ansible_python_interpreter: /usr/bin/python3
-        dns_zones:
+        dns_forward_zones:
           example.com:
             serial: 20201102
-            ip_range: 192.168.1.1/24
+            ip_range: 192.168.1.0/24
             ns:
               - gw.example.com.
             a:
@@ -37,9 +37,18 @@
               lpr: printer.example.com.
             srv: {}
             txt: {}
+        dns_reverse_zones:
+          192.168.1.0/24:
+            ns:
+              - gw.example.com.
+            ptr:
+              192.168.1.1: gw.example.com.
+              192.168.1.2: host1.example.com.
+              192.168.1.3: host2.example.com.
+              192.168.1.4: printer.example.com.
         unbound_forward_zones:
           - name: "."
             servers:
-            - "8.8.8.8"
+              - "8.8.8.8"
 verifier:
   name: ansible
diff --git a/molecule/default/prepare.yml b/molecule/default/prepare.yml
index cfec10c..8011b9d 100644
--- a/molecule/default/prepare.yml
+++ b/molecule/default/prepare.yml
@@ -6,12 +6,13 @@
 
 - name: Prepare for unbound by installing nsd for testing
   hosts: all
-  roles:
-    - nsd
 
-  tasks:
+  pre_tasks:
     - name: Add dnsutils so dig works in tests
       apt:
         name: "dnsutils"
         state: "present"
         update_cache: true
+
+  roles:
+    - nsd
diff --git a/molecule/default/verify.yml b/molecule/default/verify.yml
index 87b7bc8..99a7c6f 100644
--- a/molecule/default/verify.yml
+++ b/molecule/default/verify.yml
@@ -7,6 +7,32 @@
 - name: Verify
   hosts: all
   tasks:
-  - name: example assertion
-    assert:
-      that: true
+
+    - name: Check for A record
+      command:  # noqa 301
+        cmd: "dig +short gw.example.com @{{ ansible_default_ipv4.address }}"
+      register: a_dig
+      failed_when: "'192.168.1.1' not in a_dig.stdout"
+
+    - name: Check for NS record
+      command:  # noqa 301
+        cmd: "dig ns +short example.com @{{ ansible_default_ipv4.address }}"
+      register: ns_dig
+      failed_when: "'gw.example.com.' not in ns_dig.stdout"
+
+    - name: Check for CNAME record
+      command:  # noqa 301
+        cmd: "dig +short lpr.example.com @{{ ansible_default_ipv4.address }}"
+      register: cn_dig
+      failed_when: "'printer.example.com.\n192.168.1.4' not in cn_dig.stdout"
+
+    - name: Check for reverse IP lookup
+      command:  # noqa 301
+        cmd: "dig -x {{ item.key }} @{{ ansible_default_ipv4.address }}"
+      register: rip_dig
+      failed_when: "item.value not in rip_dig.stdout"
+      with_dict:
+        192.168.1.1: gw.example.com.
+        192.168.1.2: host1.example.com.
+        192.168.1.3: host2.example.com.
+        192.168.1.4: printer.example.com.
diff --git a/tasks/Debian.yml b/tasks/Debian.yml
index db8a3bb..88f9c4b 100644
--- a/tasks/Debian.yml
+++ b/tasks/Debian.yml
@@ -4,6 +4,26 @@
 # SPDX-FileCopyrightText: © 2020 Open Networking Foundation <support@opennetworking.org>
 # SPDX-License-Identifier: Apache-2.0
 
+# network may not be ready for unbound to bind to a specific IP address, so
+# wait for that
+#  docs: https://www.freedesktop.org/wiki/Software/systemd/NetworkTarget/
+
+- name: Create a directory for reconfiguring unbound via systemd
+  file:
+    path: "/etc/systemd/system/unbound.service.d"
+    state: directory
+    owner: root
+    group: root
+    mode: "0644"
+
+- name: Configure systemd unit to wait until network is online to start unbound
+  copy:
+    src: "network_online.conf"
+    dest: "/etc/systemd/system/unbound.service.d/network_online.conf"
+    owner: root
+    group: root
+    mode: "0644"
+
 - name: Install unbound packages (Debian)
   apt:
     name: "unbound"
diff --git a/tasks/OpenBSD.yml b/tasks/OpenBSD.yml
new file mode 100644
index 0000000..789cf13
--- /dev/null
+++ b/tasks/OpenBSD.yml
@@ -0,0 +1,11 @@
+---
+# unbound tasks/OpenBSD.yml
+#
+# SPDX-FileCopyrightText: © 2020 Open Networking Foundation <support@opennetworking.org>
+# SPDX-License-Identifier: Apache-2.0
+
+# unbound installed in base
+
+- name: Set unbound arguments for use with service module
+  set_fact:
+    unbound_arguments: "-c /var/unbound/etc/unbound.conf"
diff --git a/tasks/main.yml b/tasks/main.yml
index f88267f..2a21a0f 100644
--- a/tasks/main.yml
+++ b/tasks/main.yml
@@ -27,6 +27,7 @@
     name: "{{ unbound_service }}"
     enabled: true
     state: started
+    arguments: "{{ unbound_arguments | default(omit) }}"
 
 - name: Flush handlers as listen addresses can conflict with nsd
   meta: flush_handlers
diff --git a/templates/unbound.conf.j2 b/templates/unbound.conf.j2
index dd23b95..d6b3feb 100644
--- a/templates/unbound.conf.j2
+++ b/templates/unbound.conf.j2
@@ -25,9 +25,9 @@
 {% else %}
   # allow queries from localhost
   access-control: 127.0.0.0/24 allow
-{% if unbound_allow_zone_ips and dns_zones %}
+{% if unbound_allow_zone_ips and dns_forward_zones %}
   # allow from networks defined in zones
-{% for key, value in dns_zones.items() %}
+{% for key, value in dns_forward_zones.items() %}
   access-control: {{ value.ip_range }} allow
 {% endfor %}
 {% endif %}
@@ -45,51 +45,66 @@
   interface: {{ ansible_default_ipv4.address }}
 
 {% endif %}
-{% if unbound_listen_zone_ips and dns_zones %}
-{% for key, value in dns_zones.items() %}
+{% if unbound_listen_zone_ips and dns_forward_zones %}
+{% for key, value in dns_forward_zones.items() %}
 {% set if_ip = value.ip_range | ipaddr('next_usable') | ipaddr('address') %}
 {% if if_ip in ansible_all_ipv4_addresses %}
-  # listen on IPs defined by dns_zones: {{ key }}
+  # listen on IPs defined by dns_forward_zones: {{ key }}
   interface: {{ if_ip }}
+
 {% endif %}
 {% endfor %}
-
 {% endif %}
 {% if unbound_listen_ips %}
   # listen on specific IPs
 {% for ip in unbound_listen_ips %}
   interface: {{ ip | ipaddr('address') }}
-{% endfor %}
 
+{% endfor %}
 {% endif %}
+  # disable DNS-over-HTTP (DoH) as it breaks split horizon
+  # https://support.mozilla.org/en-US/kb/configuring-networks-disable-dns-over-https
+  # https://support.mozilla.org/en-US/kb/canary-domain-use-application-dnsnet
+  local-zone: "use-application-dns.net" always_nxdomain
+
+{% if dns_reverse_zones %}
+  # allow reverse queries for RFC1918 addresses, per dns_reverse_zones
+  {% for key, value in dns_reverse_zones.items() %}
+  local-zone: "{{ key | ipaddr('network') | unbound_revdns }}" nodefault
+  {% endfor %}
+{% endif %}
+{% if unbound_reverse_zones %}
+  # allow reverse queries for RFC1918 addresses, per unbound_reverse_zones
+  {% for urz in unbound_reverse_zones %}
+  local-zone: "{{ urz | ipaddr('network') | unbound_revdns }}" nodefault
+  {% endfor %}
+{% endif %}
+
 # allow unbound to query localhost, where authoritative DNS might be listening
 do-not-query-localhost: no
 
-# disable DNS-over-HTTP (DoH) as it breaks split horizon
-# https://support.mozilla.org/en-US/kb/configuring-networks-disable-dns-over-https
-# https://support.mozilla.org/en-US/kb/canary-domain-use-application-dnsnet
-local-zone: "use-application-dns.net" always_nxdomain
-
-{% if dns_zones %}
-# allow reverse queries for RFC1918 addresses
-{% for key, value in dns_zones.items() %}
-local-zone: "{{ value.ip_range | unbound_revdns }}" nodefault
+# zone definitions
+{% if dns_reverse_zones %}
+# reverse zones created by dns_reverse_zones
+{% for key, value in dns_reverse_zones.items() %}
+stub-zone:
+  name: "{{ key | ipaddr('network') | unbound_revdns }}"
+  stub-addr: {{ unbound_authoritative_server_ip }}
 
 {% endfor %}
-
-# stub-zones zones that authoritative DNS is serving
-{% for key, value in dns_zones.items() %}
+{% endif %}
+{% if dns_forward_zones %}
+# forward zones created by dns_forward_zones
+{% for key, value in dns_forward_zones.items() %}
 stub-zone:
   name: "{{ key }}"
   stub-addr: {{ unbound_authoritative_server_ip }}
 
-stub-zone:
-  name: "{{ value.ip_range | unbound_revdns }}"
-  stub-addr: {{ unbound_authoritative_server_ip }}
-
 {% endfor %}
 {% endif %}
+
 {% if unbound_forward_zones %}
+# Forward zones created by: unbound_forward_zones
 {% for fz in unbound_forward_zones %}
 forward-zone:
   name: "{{ fz.name | default('.') }}"
@@ -97,5 +112,4 @@
   forward-addr: {{ fza }}
 {% endfor %}
 {% endfor %}
-
 {% endif %}
diff --git a/vars/OpenBSD.yml b/vars/OpenBSD.yml
new file mode 100644
index 0000000..05d4503
--- /dev/null
+++ b/vars/OpenBSD.yml
@@ -0,0 +1,17 @@
+---
+# unbound vars/OpenBSD.yml
+#
+# SPDX-FileCopyrightText: © 2020 Open Networking Foundation <support@opennetworking.org>
+# SPDX-License-Identifier: Apache-2.0
+#
+# NOTE: Only put platform/OS-specific variables in this file.
+# Put all other variables in the 'defaults/main.yml' file.
+
+# config dir
+unbound_conf_dir: "/var/unbound/etc"
+
+# group with read permissions on config files
+unbound_groupname: "_unbound"
+
+# service name
+unbound_service: "unbound"