From c59dc90cb1cad525766809032476d27e50bd5738 Mon Sep 17 00:00:00 2001 From: Chris Liu Date: Fri, 15 Oct 2021 19:25:30 +0800 Subject: [PATCH 1/3] Initial dns module --- plugins/module_utils/pfsense.py | 5 +- plugins/modules/pfsense_dns.py | 230 ++++++++++++++++++++++++++++++++ 2 files changed, 234 insertions(+), 1 deletion(-) create mode 100644 plugins/modules/pfsense_dns.py diff --git a/plugins/module_utils/pfsense.py b/plugins/module_utils/pfsense.py index 44fd07ee..b35e41d6 100644 --- a/plugins/module_utils/pfsense.py +++ b/plugins/module_utils/pfsense.py @@ -254,7 +254,10 @@ def copy_dict_to_element(self, src, top_elt, sub=0): elif isinstance(value, list): for item in value: new_elt = self.new_element(key) - new_elt.text = item + if isinstance(item, dict): + self.copy_dict_to_element(item, new_elt, sub=sub+1) + else: + new_elt.text = item top_elt.append(new_elt) else: # Create a new element diff --git a/plugins/modules/pfsense_dns.py b/plugins/modules/pfsense_dns.py new file mode 100644 index 00000000..3471760e --- /dev/null +++ b/plugins/modules/pfsense_dns.py @@ -0,0 +1,230 @@ +# -*- coding: utf-8 -*- + +# Copyright: (c) 2021, Chris Liu +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +# TODO: advance page of DNS and access control is not done here +# TODO: alias for DNS record + +from __future__ import absolute_import, division, print_function +import base64 +import copy +__metaclass__ = type +from ansible_collections.pfsensible.core.plugins.module_utils.module_base import PFSenseModuleBase +from ansible.module_utils.basic import AnsibleModule + +DNS_DOMAIN_OVERRIDE_SPEC = dict( + domain=dict(required=True, type='str'), + ip=dict(required=True, type='str'), + descr=dict(default='', type='str'), + tls_hostname=dict(default='', type='str'), + forward_tls_upstream=dict(default='', type='str'), +) + +DNS_HOST_ALIAS_SPEC = dict( + host=dict(required=True, type='str'), + domain=dict(required=True, type='str'), + description=dict(required=True, type='str'), +) + +DNS_HOST_SPEC = dict( + host=dict(required=True, type='str'), + domain=dict(required=True, type='str'), + ip=dict(required=True, type='str'), + descr=dict(default="", type='str'), + aliases=dict(default=[], type='list', elements='dict', options=DNS_HOST_ALIAS_SPEC), +) + +dns_ARGUMENT_SPEC = dict( + state=dict(default='present', choices=['present', 'absent']), + + list_active_interface_descr=dict(default=["all"], type='list', elements='str'), + list_outgoing_interface_descr=dict(default=["all"], type='list', elements='str'), + custom_options=dict(default="", type='str'), + hideidentity=dict(default=True, type='bool'), + hideversion=dict(default=True, type='bool'), + dnssecstripped=dict(default=True, type='bool'), + port=dict(default=53, type='int'), + tlsport=dict(default=853, type='int'), + sslcertdescr=dict(default="", type='str'), #need transform + forwarding=dict(default=False, type='bool'), + system_domain_local_zone_type=dict(default='transparent', choice=['deny', 'refuse', 'static', 'transparent', 'typetransparent', 'redirect', 'inform', 'inform_deny', 'inform_deny']), + regdhcp=dict(default=False, type='bool'), + regdhcpstatic=dict(default=False, type='bool'), + regovpnclients=dict(default=False, type='bool'), + enablessl=dict(default=False, type='bool'), + dnssec=dict(default=False, type='bool'), + forward_tls_upstream=dict(default=False, type='bool'), + hosts=dict(default=[], type='list', elements='dict', options=DNS_HOST_SPEC), + domainoverrides=dict(default=[], type='list', elements='dict', options=DNS_DOMAIN_OVERRIDE_SPEC) +) + +dns_REQUIRED_IF = [] + + +class PFSenseDNSModule(PFSenseModuleBase): + """ module managing pfsense dnss """ + + @staticmethod + def get_argument_spec(): + """ return argument spec """ + return dns_ARGUMENT_SPEC + + ############################## + # init + # + def __init__(self, module, pfsense=None): + super(PFSenseDNSModule, self).__init__(module, pfsense) + self.name = "pfsense_dns" + self.root_elt = self.pfsense.get_element('unbound') + self.obj = dict() + self.interface_elt = None + self.dynamic = False + + if self.root_elt is None: + self.root_elt = self.pfsense.new_element('unbound') + self.pfsense.root.append(self.root_elt) + + def get_interface_by_display_name(self, if_descr:str): + if if_descr.lower() == "all": + return "all" + else: + return self.pfsense.get_interface_by_display_name(if_descr) + + def _params_to_obj(self): + """ return a dict from module params """ + params = self.params + + obj = dict() + + if params["state"] == "present": + + obj["enable"] = "" + obj["active_interface"] = ",".join(self.get_interface_by_display_name(x) for x in params["list_active_interface_descr"]) + obj["outgoing_interface"] = ",".join(self.get_interface_by_display_name(x) for x in params["list_outgoing_interface_descr"]) + obj["custom_options"] = base64.b64encode(bytes(params['custom_options'],'utf-8')).decode() + self._get_ansible_param_bool(obj, "hideidentity", value="") + self._get_ansible_param_bool(obj, "hideversion", value="") + self._get_ansible_param_bool(obj, "dnssecstripped", value="") + self._get_ansible_param(obj, "port") + self._get_ansible_param(obj, "tlsport") + if params["sslcertdescr"]: + obj["sslcertref"] = self.pfsense.find_cert_elt(params["sslcertdescr"]).find("refid").text + self._get_ansible_param_bool(obj, "forwarding", value="") + self._get_ansible_param(obj, "system_domain_local_zone_type") + self._get_ansible_param_bool(obj, "regdhcp", value="") + self._get_ansible_param_bool(obj, "regdhcpstatic", value="") + self._get_ansible_param_bool(obj, "regovpnclients", value="") + self._get_ansible_param_bool(obj, "enablessl", value="") + self._get_ansible_param_bool(obj, "dnssec", value="") + self._get_ansible_param_bool(obj, "forward_tls_upstream", value="") + self._get_ansible_param(obj, "hosts") + self._get_ansible_param(obj, "domainoverrides") + + if obj["active_interface"] != "all": + obj["active_interface"] += ",lo0" + + # wrap to all hosts.alias + for host in obj["hosts"]: + if host["aliases"]: + tmp_aliases = host["aliases"] + host["aliases"] = { + "item": tmp_aliases + } + + return obj + + def _validate_params(self): + """ do some extra checks on input parameters """ + params = self.params + + if params["sslcertdescr"] and not self.pfsense.find_cert_elt(params["sslcertdescr"]): + self.module.fail_json(msg=f'sslcertdescr, {params["sslcertdescr"]} is not a valid description of cert') + + for host in params["hosts"]: + if not self.pfsense.is_ipv4_address(host["ip"]): + self.module.fail_json(msg=f'ip, {host["ip"]} is not a ipv4 address') + + for if_descr in params["list_active_interface_descr"]+params["list_outgoing_interface_descr"]: + if not self.pfsense.is_interface_display_name(if_descr) and if_descr.lower()!="all": + self.module.fail_json(msg=f'if_descr, {if_descr}, is not exist') + + + ############################## + # XML processing + # + def _create_target(self): + """ create the XML target_elt """ + return self.root_elt + + def _find_target(self): + """ find the XML target_elt """ + return self.root_elt + + def _get_params_to_remove(self): + """ returns the list of params to remove if they are not set """ + if self.params["state"] == "absent": + return ["enable"] + else: + return [] + + ############################## + # run + # + def _update(self): + """ make the target pfsense reload """ + return self.pfsense.phpshell(''' +require_once("unbound.inc"); +require_once("pfsense-utils.inc"); +require_once("system.inc"); + +services_unbound_configure(); +system_resolvconf_generate(); +system_dhcpleases_configure(); +clear_subsystem_dirty("unbound"); +''') + + ############################## + # Logging + # + def _get_obj_name(self): + """ return obj's name """ + return self.name + + def _log_fields(self, before=None): + """ generate pseudo-CLI command fields parameters to create an obj """ + values = '' + + values += self.format_updated_cli_field(self.obj, before, 'enable', fvalue=self.fvalue_bool, add_comma=(values), log_none=False) + values += self.format_updated_cli_field(self.obj, before, 'active_interface', add_comma=(values), log_none=False) + values += self.format_updated_cli_field(self.obj, before, 'outgoing_interface', add_comma=(values), log_none=False) + values += self.format_updated_cli_field(self.obj, before, 'custom_options', fvalue=self.fvalue_bool, add_comma=(values), log_none=False) + values += self.format_updated_cli_field(self.obj, before, 'hideidentity', fvalue=self.fvalue_bool, add_comma=(values), log_none=False) + values += self.format_updated_cli_field(self.obj, before, 'hideversion', fvalue=self.fvalue_bool, add_comma=(values), log_none=False) + values += self.format_updated_cli_field(self.obj, before, 'dnssecstripped', fvalue=self.fvalue_bool, add_comma=(values), log_none=False) + values += self.format_updated_cli_field(self.obj, before, 'port', fvalue=self.fvalue_bool, add_comma=(values), log_none=False) + values += self.format_updated_cli_field(self.obj, before, 'tlsport', fvalue=self.fvalue_bool, add_comma=(values), log_none=False) + values += self.format_updated_cli_field(self.obj, before, 'sslcertref', fvalue=self.fvalue_bool, add_comma=(values), log_none=False) + values += self.format_updated_cli_field(self.obj, before, 'forwarding', fvalue=self.fvalue_bool, add_comma=(values), log_none=False) + values += self.format_updated_cli_field(self.obj, before, 'system_domain_local_zone_type', fvalue=self.fvalue_bool, add_comma=(values), log_none=False) + values += self.format_updated_cli_field(self.obj, before, 'regdhcp', fvalue=self.fvalue_bool, add_comma=(values), log_none=False) + values += self.format_updated_cli_field(self.obj, before, 'regdhcpstatic', fvalue=self.fvalue_bool, add_comma=(values), log_none=False) + + # todo: hosts and domainoverrides is not logged + return values + + + +def main(): + module = AnsibleModule( + argument_spec=dns_ARGUMENT_SPEC, + required_if=dns_REQUIRED_IF, + supports_check_mode=True) + + pfmodule = PFSenseDNSModule(module) + pfmodule.run(module.params) + pfmodule.commit_changes() + + +if __name__ == '__main__': + main() From af7679f9ff27691bd9a529014652e64600900144 Mon Sep 17 00:00:00 2001 From: Daniel Huss Date: Sat, 8 Jul 2023 10:58:40 -0400 Subject: [PATCH 2/3] [pfsense_dns_resolver] Initial module --- README.md | 1 + plugins/module_utils/pfsense.py | 2 +- plugins/modules/pfsense_dns.py | 230 ---------- plugins/modules/pfsense_dns_resolver.py | 573 ++++++++++++++++++++++++ 4 files changed, 575 insertions(+), 231 deletions(-) delete mode 100644 plugins/modules/pfsense_dns.py create mode 100644 plugins/modules/pfsense_dns_resolver.py diff --git a/README.md b/README.md index 250030c1..af87ffbe 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,7 @@ The following modules are currently available: * [pfsense_cert](https://github.com/pfsensible/core/wiki/pfsense_cert) for Certificates * [pfsense_default_gateway](https://github.com/pfsensible/core/wiki/pfsense_default_gateway) for setting the default gateways * [pfsense_dhcp_static](https://github.com/pfsensible/core/wiki/pfsense_dhcp_static) for static DHCP entries +* pfsense_dns_resolver for DNS resolver (unbound) settings * [pfsense_gateway](https://github.com/pfsensible/core/wiki/pfsense_gateway) for routing gateways * [pfsense_group](https://github.com/pfsensible/core/wiki/pfsense_group) for user groups * [pfsense_interface](https://github.com/pfsensible/core/wiki/pfsense_interface) for interfaces diff --git a/plugins/module_utils/pfsense.py b/plugins/module_utils/pfsense.py index b35e41d6..2edc1d34 100644 --- a/plugins/module_utils/pfsense.py +++ b/plugins/module_utils/pfsense.py @@ -255,7 +255,7 @@ def copy_dict_to_element(self, src, top_elt, sub=0): for item in value: new_elt = self.new_element(key) if isinstance(item, dict): - self.copy_dict_to_element(item, new_elt, sub=sub+1) + self.copy_dict_to_element(item, new_elt, sub=sub + 1) else: new_elt.text = item top_elt.append(new_elt) diff --git a/plugins/modules/pfsense_dns.py b/plugins/modules/pfsense_dns.py deleted file mode 100644 index 3471760e..00000000 --- a/plugins/modules/pfsense_dns.py +++ /dev/null @@ -1,230 +0,0 @@ -# -*- coding: utf-8 -*- - -# Copyright: (c) 2021, Chris Liu -# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) - -# TODO: advance page of DNS and access control is not done here -# TODO: alias for DNS record - -from __future__ import absolute_import, division, print_function -import base64 -import copy -__metaclass__ = type -from ansible_collections.pfsensible.core.plugins.module_utils.module_base import PFSenseModuleBase -from ansible.module_utils.basic import AnsibleModule - -DNS_DOMAIN_OVERRIDE_SPEC = dict( - domain=dict(required=True, type='str'), - ip=dict(required=True, type='str'), - descr=dict(default='', type='str'), - tls_hostname=dict(default='', type='str'), - forward_tls_upstream=dict(default='', type='str'), -) - -DNS_HOST_ALIAS_SPEC = dict( - host=dict(required=True, type='str'), - domain=dict(required=True, type='str'), - description=dict(required=True, type='str'), -) - -DNS_HOST_SPEC = dict( - host=dict(required=True, type='str'), - domain=dict(required=True, type='str'), - ip=dict(required=True, type='str'), - descr=dict(default="", type='str'), - aliases=dict(default=[], type='list', elements='dict', options=DNS_HOST_ALIAS_SPEC), -) - -dns_ARGUMENT_SPEC = dict( - state=dict(default='present', choices=['present', 'absent']), - - list_active_interface_descr=dict(default=["all"], type='list', elements='str'), - list_outgoing_interface_descr=dict(default=["all"], type='list', elements='str'), - custom_options=dict(default="", type='str'), - hideidentity=dict(default=True, type='bool'), - hideversion=dict(default=True, type='bool'), - dnssecstripped=dict(default=True, type='bool'), - port=dict(default=53, type='int'), - tlsport=dict(default=853, type='int'), - sslcertdescr=dict(default="", type='str'), #need transform - forwarding=dict(default=False, type='bool'), - system_domain_local_zone_type=dict(default='transparent', choice=['deny', 'refuse', 'static', 'transparent', 'typetransparent', 'redirect', 'inform', 'inform_deny', 'inform_deny']), - regdhcp=dict(default=False, type='bool'), - regdhcpstatic=dict(default=False, type='bool'), - regovpnclients=dict(default=False, type='bool'), - enablessl=dict(default=False, type='bool'), - dnssec=dict(default=False, type='bool'), - forward_tls_upstream=dict(default=False, type='bool'), - hosts=dict(default=[], type='list', elements='dict', options=DNS_HOST_SPEC), - domainoverrides=dict(default=[], type='list', elements='dict', options=DNS_DOMAIN_OVERRIDE_SPEC) -) - -dns_REQUIRED_IF = [] - - -class PFSenseDNSModule(PFSenseModuleBase): - """ module managing pfsense dnss """ - - @staticmethod - def get_argument_spec(): - """ return argument spec """ - return dns_ARGUMENT_SPEC - - ############################## - # init - # - def __init__(self, module, pfsense=None): - super(PFSenseDNSModule, self).__init__(module, pfsense) - self.name = "pfsense_dns" - self.root_elt = self.pfsense.get_element('unbound') - self.obj = dict() - self.interface_elt = None - self.dynamic = False - - if self.root_elt is None: - self.root_elt = self.pfsense.new_element('unbound') - self.pfsense.root.append(self.root_elt) - - def get_interface_by_display_name(self, if_descr:str): - if if_descr.lower() == "all": - return "all" - else: - return self.pfsense.get_interface_by_display_name(if_descr) - - def _params_to_obj(self): - """ return a dict from module params """ - params = self.params - - obj = dict() - - if params["state"] == "present": - - obj["enable"] = "" - obj["active_interface"] = ",".join(self.get_interface_by_display_name(x) for x in params["list_active_interface_descr"]) - obj["outgoing_interface"] = ",".join(self.get_interface_by_display_name(x) for x in params["list_outgoing_interface_descr"]) - obj["custom_options"] = base64.b64encode(bytes(params['custom_options'],'utf-8')).decode() - self._get_ansible_param_bool(obj, "hideidentity", value="") - self._get_ansible_param_bool(obj, "hideversion", value="") - self._get_ansible_param_bool(obj, "dnssecstripped", value="") - self._get_ansible_param(obj, "port") - self._get_ansible_param(obj, "tlsport") - if params["sslcertdescr"]: - obj["sslcertref"] = self.pfsense.find_cert_elt(params["sslcertdescr"]).find("refid").text - self._get_ansible_param_bool(obj, "forwarding", value="") - self._get_ansible_param(obj, "system_domain_local_zone_type") - self._get_ansible_param_bool(obj, "regdhcp", value="") - self._get_ansible_param_bool(obj, "regdhcpstatic", value="") - self._get_ansible_param_bool(obj, "regovpnclients", value="") - self._get_ansible_param_bool(obj, "enablessl", value="") - self._get_ansible_param_bool(obj, "dnssec", value="") - self._get_ansible_param_bool(obj, "forward_tls_upstream", value="") - self._get_ansible_param(obj, "hosts") - self._get_ansible_param(obj, "domainoverrides") - - if obj["active_interface"] != "all": - obj["active_interface"] += ",lo0" - - # wrap to all hosts.alias - for host in obj["hosts"]: - if host["aliases"]: - tmp_aliases = host["aliases"] - host["aliases"] = { - "item": tmp_aliases - } - - return obj - - def _validate_params(self): - """ do some extra checks on input parameters """ - params = self.params - - if params["sslcertdescr"] and not self.pfsense.find_cert_elt(params["sslcertdescr"]): - self.module.fail_json(msg=f'sslcertdescr, {params["sslcertdescr"]} is not a valid description of cert') - - for host in params["hosts"]: - if not self.pfsense.is_ipv4_address(host["ip"]): - self.module.fail_json(msg=f'ip, {host["ip"]} is not a ipv4 address') - - for if_descr in params["list_active_interface_descr"]+params["list_outgoing_interface_descr"]: - if not self.pfsense.is_interface_display_name(if_descr) and if_descr.lower()!="all": - self.module.fail_json(msg=f'if_descr, {if_descr}, is not exist') - - - ############################## - # XML processing - # - def _create_target(self): - """ create the XML target_elt """ - return self.root_elt - - def _find_target(self): - """ find the XML target_elt """ - return self.root_elt - - def _get_params_to_remove(self): - """ returns the list of params to remove if they are not set """ - if self.params["state"] == "absent": - return ["enable"] - else: - return [] - - ############################## - # run - # - def _update(self): - """ make the target pfsense reload """ - return self.pfsense.phpshell(''' -require_once("unbound.inc"); -require_once("pfsense-utils.inc"); -require_once("system.inc"); - -services_unbound_configure(); -system_resolvconf_generate(); -system_dhcpleases_configure(); -clear_subsystem_dirty("unbound"); -''') - - ############################## - # Logging - # - def _get_obj_name(self): - """ return obj's name """ - return self.name - - def _log_fields(self, before=None): - """ generate pseudo-CLI command fields parameters to create an obj """ - values = '' - - values += self.format_updated_cli_field(self.obj, before, 'enable', fvalue=self.fvalue_bool, add_comma=(values), log_none=False) - values += self.format_updated_cli_field(self.obj, before, 'active_interface', add_comma=(values), log_none=False) - values += self.format_updated_cli_field(self.obj, before, 'outgoing_interface', add_comma=(values), log_none=False) - values += self.format_updated_cli_field(self.obj, before, 'custom_options', fvalue=self.fvalue_bool, add_comma=(values), log_none=False) - values += self.format_updated_cli_field(self.obj, before, 'hideidentity', fvalue=self.fvalue_bool, add_comma=(values), log_none=False) - values += self.format_updated_cli_field(self.obj, before, 'hideversion', fvalue=self.fvalue_bool, add_comma=(values), log_none=False) - values += self.format_updated_cli_field(self.obj, before, 'dnssecstripped', fvalue=self.fvalue_bool, add_comma=(values), log_none=False) - values += self.format_updated_cli_field(self.obj, before, 'port', fvalue=self.fvalue_bool, add_comma=(values), log_none=False) - values += self.format_updated_cli_field(self.obj, before, 'tlsport', fvalue=self.fvalue_bool, add_comma=(values), log_none=False) - values += self.format_updated_cli_field(self.obj, before, 'sslcertref', fvalue=self.fvalue_bool, add_comma=(values), log_none=False) - values += self.format_updated_cli_field(self.obj, before, 'forwarding', fvalue=self.fvalue_bool, add_comma=(values), log_none=False) - values += self.format_updated_cli_field(self.obj, before, 'system_domain_local_zone_type', fvalue=self.fvalue_bool, add_comma=(values), log_none=False) - values += self.format_updated_cli_field(self.obj, before, 'regdhcp', fvalue=self.fvalue_bool, add_comma=(values), log_none=False) - values += self.format_updated_cli_field(self.obj, before, 'regdhcpstatic', fvalue=self.fvalue_bool, add_comma=(values), log_none=False) - - # todo: hosts and domainoverrides is not logged - return values - - - -def main(): - module = AnsibleModule( - argument_spec=dns_ARGUMENT_SPEC, - required_if=dns_REQUIRED_IF, - supports_check_mode=True) - - pfmodule = PFSenseDNSModule(module) - pfmodule.run(module.params) - pfmodule.commit_changes() - - -if __name__ == '__main__': - main() diff --git a/plugins/modules/pfsense_dns_resolver.py b/plugins/modules/pfsense_dns_resolver.py new file mode 100644 index 00000000..90002b5d --- /dev/null +++ b/plugins/modules/pfsense_dns_resolver.py @@ -0,0 +1,573 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (c) 2021, Chris Liu +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +ANSIBLE_METADATA = {'metadata_version': '1.1', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = """ +--- +module: pfsense_dns_resolver +version_added: 0.6.0 +author: Chris liu (@chris-cyliu), Daniel Huss (@danhuss) +short_description: Manage pfSense DNS resolver (unbound) settings +description: + - Manage pfSense DNS resolver (unbound) settings +notes: +options: + state: + description: Enable/Disable DNS Resolver + default: present + choices: [ "present", "absent" ] + type: str + port: + description: Listen Port + required: false + default: null + type: int + enablessl: + description: Enable SSL/TLS Service + required: false + default: false + type: bool + sslcert: + description: Description of the server certificate to use for SSL/TLS service. + required: false + default: "" + type: str + tlsport: + description: SSL/TLS Listen Port + required: false + default: null + type: int + active_interface: + description: Interface IPs used by the DNS Resolver for responding to queries from clients. + required: false + default: [ "all" ] + type: list + elements: str + outgoing_interface: + description: Utilize different network interface(s) that the DNS Resolver will use to send queries to authoritative servers and receive their replies. + required: false + default: [ "all" ] + type: list + elements: str + system_domain_local_zone_type: + description: The local-zone type used for the pfSense system domain. + required: false + default: "transparent" + type: str + choices: [ "deny", "refuse", "static", "transparent", "typetransparent", "redirect", "inform", "inform_deny", "nodefault" ] + dnssec: + description: Enable DNSSEC Support + required: false + default: false + type: bool + forwarding: + description: DNS Query Forwarding. + required: false + default: false + type: bool + forward_tls_upstream: + description: Use SSL/TLS for DNS Query Forwarding. + required: false + default: false + type: bool + regdhcp: + description: Register DHCP leases in the DNS Resolver + required: false + default: false + type: bool + regdhcpstatic: + description: Register DHCP static mappings in the DNS Resolver + required: false + default: false + type: bool + regovpnclients: + description: Register OpenVPN clients in the DNS Resolver + required: false + default: false + type: bool + custom_options: + description: additional configuration parameters + required: false + default: "" + type: str + hosts: + description: Individual hosts for which the resolver's standard DNS lookup should be overridden. + required: false + default: [] + type: list + elements: dict + suboptions: + host: + description: Name of the host, without the domain part. + required: true + type: str + domain: + description: Parent domain of the host. + required: true + type: str + ip: + description: IPv4 or IPv6 comma-separated addresses to be returned for the host + required: true + type: str + descr: + description: A description may be entered here for administrative reference. + required: false + type: str + aliases: + description: Additional names for this host. + required: false + type: list + elements: dict + suboptions: + host: + description: Name of the host, without the domain part. + required: true + type: str + domain: + description: Parent domain of the host. + required: true + type: str + description: + description: A description may be entered here for administrative reference. + required: true + type: str + domainoverrides: + description: Domains for which the resolver's standard DNS lookup should be overridden. + required: false + default: [] + type: list + elements: dict + suboptions: + domain: + description: Domain whose lookups will be directed to a user-specified DNS lookup server. + required: true + type: str + ip: + description: IPv4 or IPv6 address of the authoritative DNS server for this domain. + required: true + type: str + forward_tls_upstream: + description: Use SSL/TLS for DNS Queries forwarded to this server + required: false + default: '' + type: str + tls_hostname: + description: An optional TLS hostname used to verify the server certificate when performing TLS Queries. + required: false + default: '' + type: str + descr: + description: A description may be entered here for administrative reference. + required: false + type: str + hideidentity: + description: id.server and hostname.bind queries are refused. + required: false + default: true + type: bool + hideversion: + description: version.server and version.bind queries are refused. + required: false + default: true + type: bool + prefetch: + description: Message cache elements are prefetched before they expire to help keep the cache up to date. + required: false + default: false + type: bool + prefetchkey: + description: DNSKEYs are fetched earlier in the validation process when a Delegation signer is encountered. + required: false + default: false + type: bool + dnssecstripped: + description: If enabled, DNSSEC data is required for trust-anchored zones. + required: false + default: true + type: bool + msgcachesize: + description: Message cache size in MB + required: false + default: 4 + choices: [ 4, 10, 20, 50, 100, 250, 512 ] + type: int + outgoing_num_tcp: + description: Number of outgoing TCP buffers to allocate per thread. + required: false + default: 10 + choices: [ 0, 10, 20, 30, 50 ] + type: int + incoming_num_tcp: + description: Number of incoming TCP buffers to allocate per thread. + required: false + default: 10 + choices: [ 0, 10, 20, 30, 50 ] + type: int + edns_buffer_size: + description: Number of bytes to advertise as the EDNS reassembly buffer size. + required: false + default: "4096" + choices: [ "auto", "512", "1220", "1232", "1432", "1480", "4096" ] + type: str + num_queries_per_thread: + description: Number of queries that every thread will service simultaneously. + required: false + default: 512 + choices: [ 512, 1024, 2048 ] + type: int + jostle_timeout: + description: This timeout (in milliseconds) is used for when the server is very busy. + required: false + default: 200 + choices: [ 100, 200, 500, 1000 ] + type: int + cache_max_ttl: + description: The Maximum Time to Live (in seconds) for RRsets and messages in the cache. + required: false + default: 86400 + type: int + cache_min_ttl: + description: The Minimum Time to Live (in seconds) for RRsets and messages in the cache. + required: false + default: 0 + type: int + infra_host_ttl: + description: Time to Live, in seconds, for entries in the infrastructure host cache. + required: false + default: 900 + choices: [ 60, 120, 300, 600, 900 ] + type: int + infra_cache_numhosts: + description: Number of infrastructure hosts for which information is cached. + required: false + default: 10000 + choices: [ 1000, 5000, 10000, 20000, 50000, 100000, 200000 ] + type: int + unwanted_reply_threshold: + description: If enabled, a total number of unwanted replies is kept track of in every thread. + required: false + default: "disabled" + choices: [ "disabled", "5000000", "10000000", "20000000", "40000000", "50000000" ] + type: str + log_verbosity: + description: The level of detail to be logged. + required: false + default: 1 + choices: [ 0, 1, 2, 3, 4, 5 ] + type: int +""" + +EXAMPLES = """ +- name: Enable DNS Resolver + pfsense_dns_resolver: + state: present + +- name: Enable DNS Resolver with some options + pfsense_dns_resolver: + state: present + enablessl: true + sslcert: "webConfigurator default" + dnssec: true + regdhcp: true + regdhcpstatic: true + hosts: + - { host: test, domain: home.local, ip: 192.168.1.100, descr: "Example host override", + aliases: [{ host: test-admin, domain: home.local, description: "Example aliases" }] } + +- name: Disable DNS Resolver + pfsense_dns_resolver: + state: absent +""" + +RETURN = """ + +""" + +from ansible_collections.pfsensible.core.plugins.module_utils.module_base import PFSenseModuleBase +from ansible.module_utils.basic import AnsibleModule +import base64 + +# TODO: access control is not done here +# TODO: alias for DNS record + +DNS_RESOLVER_DOMAIN_OVERRIDE_SPEC = dict( + domain=dict(required=True, type='str'), + ip=dict(required=True, type='str'), + descr=dict(default='', type='str'), + tls_hostname=dict(default='', type='str'), + forward_tls_upstream=dict(default='', type='str'), +) + +DNS_RESOLVER_HOST_ALIAS_SPEC = dict( + host=dict(required=True, type='str'), + domain=dict(required=True, type='str'), + description=dict(required=True, type='str'), +) + +DNS_RESOLVER_HOST_SPEC = dict( + host=dict(required=True, type='str'), + domain=dict(required=True, type='str'), + ip=dict(required=True, type='str'), + descr=dict(default="", type='str'), + aliases=dict(default=[], type='list', elements='dict', options=DNS_RESOLVER_HOST_ALIAS_SPEC), +) + +DNS_RESOLVER_ARGUMENT_SPEC = dict( + state=dict(default='present', choices=['present', 'absent']), + + # General Settings + port=dict(default=None, type='int'), + enablessl=dict(default=False, type='bool'), + sslcert=dict(default="", type='str'), # need transform + tlsport=dict(default=None, type='int'), + active_interface=dict(default=["all"], type='list', elements='str'), + outgoing_interface=dict(default=["all"], type='list', elements='str'), + # TODO: Strict Outgoing Network interface Binding: check box option + system_domain_local_zone_type=dict(default='transparent', choices=['deny', 'refuse', 'static', 'transparent', 'typetransparent', 'redirect', 'inform', + 'inform_deny', 'nodefault']), + dnssec=dict(default=False, type='bool'), + # TODO: Python Module: Enable the Python Module. These 3 options omited when disabled + # python=dict(default=False, type='bool'), + # python_order=dict(default="pre_validator", type='str', choices=["pre_validator", "post_validator"]), + # python_script=dict(default="", type='str'), #Not sure what this is or how to handle it. + forwarding=dict(default=False, type='bool'), + forward_tls_upstream=dict(default=False, type='bool'), + regdhcp=dict(default=False, type='bool'), + regdhcpstatic=dict(default=False, type='bool'), + regovpnclients=dict(default=False, type='bool'), + custom_options=dict(default="", type='str'), + hosts=dict(default=[], type='list', elements='dict', options=DNS_RESOLVER_HOST_SPEC), + domainoverrides=dict(default=[], type='list', elements='dict', options=DNS_RESOLVER_DOMAIN_OVERRIDE_SPEC), + # Advanced Settings + hideidentity=dict(default=True, type='bool'), + hideversion=dict(default=True, type='bool'), + # TODO: Query Name Minimization + # TODO: Strict Query Name Minimization + prefetch=dict(default=False, type='bool'), + prefetchkey=dict(default=False, type='bool'), + dnssecstripped=dict(default=True, type='bool'), + # TODO: Serve Expired + # TODO: Aggressive NSEC + msgcachesize=dict(default=4, type='int', choices=[4, 10, 20, 50, 100, 250, 512]), + outgoing_num_tcp=dict(default=10, type='int', choices=[0, 10, 20, 30, 50]), + incoming_num_tcp=dict(default=10, type='int', choices=[0, 10, 20, 30, 50]), + edns_buffer_size=dict(default="4096", type='str', choices=["auto", "512", "1220", "1232", "1432", "1480", "4096"]), + num_queries_per_thread=dict(default=512, type='int', choices=[512, 1024, 2048]), + jostle_timeout=dict(default=200, type='int', choices=[100, 200, 500, 1000]), + cache_max_ttl=dict(default=86400, type='int'), + cache_min_ttl=dict(default=0, type='int'), + infra_host_ttl=dict(default=900, type='int', choices=[60, 120, 300, 600, 900]), + infra_cache_numhosts=dict(default=10000, type='int', choices=[1000, 5000, 10000, 20000, 50000, 100000, 200000]), + unwanted_reply_threshold=dict(default="disabled", type='str', choices=["disabled", "5000000", "10000000", "20000000", "40000000", "50000000"]), + log_verbosity=dict(default=1, type='int', choices=[0, 1, 2, 3, 4, 5]) + # TODO: Disable Auto-added Access Control + # TODO: Disable Auto-added Host Entries + # TODO: Experimental Bit 0x20 Support + # TODO: DNS64 Support +) + +DNS_RESOLVER_REQUIRED_IF = [] + + +class PFSenseDNSResolverModule(PFSenseModuleBase): + """ module managing pfsense dns resolver (unbound) """ + + @staticmethod + def get_argument_spec(): + """ return argument spec """ + return DNS_RESOLVER_ARGUMENT_SPEC + + ############################## + # init + # + def __init__(self, module, pfsense=None): + super(PFSenseDNSResolverModule, self).__init__(module, pfsense) + self.name = "pfsense_dns_resolver" + self.root_elt = self.pfsense.get_element('unbound') + self.obj = dict() + self.interface_elt = None + self.dynamic = False + + if self.root_elt is None: + self.root_elt = self.pfsense.new_element('unbound') + self.pfsense.root.append(self.root_elt) + + def get_interface_by_display_name(self, if_descr: str): + if if_descr.lower() == "all": + return "all" + else: + return self.pfsense.get_interface_by_display_name(if_descr) + + def _params_to_obj(self): + """ return a dict from module params """ + params = self.params + + obj = dict() + + if params["state"] == "present": + + obj["enable"] = "" + obj["active_interface"] = ",".join(self.get_interface_by_display_name(x) for x in params["active_interface"]) + obj["outgoing_interface"] = ",".join(self.get_interface_by_display_name(x) for x in params["outgoing_interface"]) + obj["custom_options"] = base64.b64encode(bytes(params['custom_options'], 'utf-8')).decode() + self._get_ansible_param_bool(obj, "hideidentity", value="") + self._get_ansible_param_bool(obj, "hideversion", value="") + self._get_ansible_param_bool(obj, "dnssecstripped", value="") + self._get_ansible_param(obj, "port") + self._get_ansible_param(obj, "tlsport") + if params["sslcert"]: + obj["sslcertref"] = self.pfsense.find_cert_elt(params["sslcert"]).find("refid").text + self._get_ansible_param_bool(obj, "forwarding", value="") + self._get_ansible_param(obj, "system_domain_local_zone_type") + self._get_ansible_param_bool(obj, "regdhcp", value="") + self._get_ansible_param_bool(obj, "regdhcpstatic", value="") + self._get_ansible_param_bool(obj, "regovpnclients", value="") + self._get_ansible_param_bool(obj, "enablessl", value="") + self._get_ansible_param_bool(obj, "dnssec", value="") + self._get_ansible_param_bool(obj, "forward_tls_upstream", value="") + self._get_ansible_param_bool(obj, "prefetch", value="") + self._get_ansible_param_bool(obj, "prefetchkey", value="") + self._get_ansible_param(obj, "msgcachesize") + self._get_ansible_param(obj, "outgoing_num_tcp") + self._get_ansible_param(obj, "incoming_num_tcp") + self._get_ansible_param(obj, "edns_buffer_size") + self._get_ansible_param(obj, "num_queries_per_thread") + self._get_ansible_param(obj, "jostle_timeout") + self._get_ansible_param(obj, "cache_max_ttl") + self._get_ansible_param(obj, "cache_min_ttl") + self._get_ansible_param(obj, "infra_host_ttl") + self._get_ansible_param(obj, "infra_cache_numhosts") + self._get_ansible_param(obj, "unwanted_reply_threshold") + self._get_ansible_param(obj, "log_verbosity") + self._get_ansible_param(obj, "hosts") + self._get_ansible_param(obj, "domainoverrides") + + if obj["active_interface"] != "all": + obj["active_interface"] += ",lo0" + + # wrap to all hosts.alias + for host in obj["hosts"]: + if host["aliases"]: + tmp_aliases = host["aliases"] + host["aliases"] = { + "item": tmp_aliases + } + + return obj + + def _validate_params(self): + """ do some extra checks on input parameters """ + params = self.params + + if params["sslcert"] and not self.pfsense.find_cert_elt(params["sslcert"]): + self.module.fail_json(msg=f'sslcert, {params["sslcert"]} is not a valid description of cert') + + for host in params["hosts"]: + if not self.pfsense.is_ipv4_address(host["ip"]): + self.module.fail_json(msg=f'ip, {host["ip"]} is not a ipv4 address') + + for if_descr in params["active_interface"] + params["outgoing_interface"]: + if not self.pfsense.is_interface_display_name(if_descr) and if_descr.lower() != "all": + self.module.fail_json(msg=f'if_descr, {if_descr}, is not exist') + + ############################## + # XML processing + # + def _create_target(self): + """ create the XML target_elt """ + return self.root_elt + + def _find_target(self): + """ find the XML target_elt """ + return self.root_elt + + def _get_params_to_remove(self): + """ returns the list of params to remove if they are not set """ + if self.params["state"] == "absent": + return ["enable"] + else: + return [] + + ############################## + # run + # + def _update(self): + """ make the target pfsense reload """ + return self.pfsense.phpshell(''' +require_once("unbound.inc"); +require_once("pfsense-utils.inc"); +require_once("system.inc"); + +services_unbound_configure(); +system_resolvconf_generate(); +system_dhcpleases_configure(); +clear_subsystem_dirty("unbound"); +''') + + ############################## + # Logging + # + def _get_obj_name(self): + """ return obj's name """ + return self.name + + def _log_fields(self, before=None): + """ generate pseudo-CLI command fields parameters to create an obj """ + values = '' + + values += self.format_updated_cli_field(self.obj, before, 'enable', fvalue=self.fvalue_bool, add_comma=(values), log_none=False) + values += self.format_updated_cli_field(self.obj, before, 'active_interface', add_comma=(values), log_none=False) + values += self.format_updated_cli_field(self.obj, before, 'outgoing_interface', add_comma=(values), log_none=False) + values += self.format_updated_cli_field(self.obj, before, 'custom_options', fvalue=self.fvalue_bool, add_comma=(values), log_none=False) + values += self.format_updated_cli_field(self.obj, before, 'hideidentity', fvalue=self.fvalue_bool, add_comma=(values), log_none=False) + values += self.format_updated_cli_field(self.obj, before, 'hideversion', fvalue=self.fvalue_bool, add_comma=(values), log_none=False) + values += self.format_updated_cli_field(self.obj, before, 'dnssecstripped', fvalue=self.fvalue_bool, add_comma=(values), log_none=False) + values += self.format_updated_cli_field(self.obj, before, 'port', fvalue=self.fvalue_bool, add_comma=(values), log_none=False) + values += self.format_updated_cli_field(self.obj, before, 'tlsport', fvalue=self.fvalue_bool, add_comma=(values), log_none=False) + values += self.format_updated_cli_field(self.obj, before, 'sslcertref', fvalue=self.fvalue_bool, add_comma=(values), log_none=False) + values += self.format_updated_cli_field(self.obj, before, 'forwarding', fvalue=self.fvalue_bool, add_comma=(values), log_none=False) + values += self.format_updated_cli_field(self.obj, before, 'system_domain_local_zone_type', fvalue=self.fvalue_bool, add_comma=(values), log_none=False) + values += self.format_updated_cli_field(self.obj, before, 'regdhcp', fvalue=self.fvalue_bool, add_comma=(values), log_none=False) + values += self.format_updated_cli_field(self.obj, before, 'regdhcpstatic', fvalue=self.fvalue_bool, add_comma=(values), log_none=False) + values += self.format_updated_cli_field(self.obj, before, 'prefetch', fvalue=self.fvalue_bool, add_comma=(values), log_none=False) + values += self.format_updated_cli_field(self.obj, before, 'prefetchkey', fvalue=self.fvalue_bool, add_comma=(values), log_none=False) + values += self.format_updated_cli_field(self.obj, before, 'msgcachesize', add_comma=(values), log_none=False) + values += self.format_updated_cli_field(self.obj, before, 'outgoing_num_tcp', add_comma=(values), log_none=False) + values += self.format_updated_cli_field(self.obj, before, 'incoming_num_tcp', add_comma=(values), log_none=False) + values += self.format_updated_cli_field(self.obj, before, 'edns_buffer_size', add_comma=(values), log_none=False) + values += self.format_updated_cli_field(self.obj, before, 'num_queries_per_thread', add_comma=(values), log_none=False) + values += self.format_updated_cli_field(self.obj, before, 'jostle_timeout', add_comma=(values), log_none=False) + values += self.format_updated_cli_field(self.obj, before, 'cache_max_ttl', add_comma=(values), log_none=False) + values += self.format_updated_cli_field(self.obj, before, 'cache_min_ttl', add_comma=(values), log_none=False) + values += self.format_updated_cli_field(self.obj, before, 'infra_host_ttl', add_comma=(values), log_none=False) + values += self.format_updated_cli_field(self.obj, before, 'infra_cache_numhosts', add_comma=(values), log_none=False) + values += self.format_updated_cli_field(self.obj, before, 'unwanted_reply_threshold', add_comma=(values), log_none=False) + values += self.format_updated_cli_field(self.obj, before, 'log_verbosity', add_comma=(values), log_none=False) + + # todo: hosts and domainoverrides is not logged + return values + + +def main(): + module = AnsibleModule( + argument_spec=DNS_RESOLVER_ARGUMENT_SPEC, + required_if=DNS_RESOLVER_REQUIRED_IF, + supports_check_mode=True) + + pfmodule = PFSenseDNSResolverModule(module) + pfmodule.run(module.params) + pfmodule.commit_changes() + + +if __name__ == '__main__': + main() From bb0c1ec5eac79ecd3e0afb1230a1074a06333a10 Mon Sep 17 00:00:00 2001 From: Orion Poplawski Date: Tue, 2 Jan 2024 20:55:55 -0700 Subject: [PATCH 3/3] [pfsense_dns_resolver] Fixup defaults; Add wiki link --- README.md | 2 +- plugins/modules/pfsense_dns_resolver.py | 14 +++++++++----- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index af87ffbe..9991c21d 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,7 @@ The following modules are currently available: * [pfsense_cert](https://github.com/pfsensible/core/wiki/pfsense_cert) for Certificates * [pfsense_default_gateway](https://github.com/pfsensible/core/wiki/pfsense_default_gateway) for setting the default gateways * [pfsense_dhcp_static](https://github.com/pfsensible/core/wiki/pfsense_dhcp_static) for static DHCP entries -* pfsense_dns_resolver for DNS resolver (unbound) settings +* [pfsense_dns_resolver](https://github.com/pfsensible/core/wiki/pfsense_dns_resolver) for DNS resolver (unbound) settings * [pfsense_gateway](https://github.com/pfsensible/core/wiki/pfsense_gateway) for routing gateways * [pfsense_group](https://github.com/pfsensible/core/wiki/pfsense_group) for user groups * [pfsense_interface](https://github.com/pfsensible/core/wiki/pfsense_interface) for interfaces diff --git a/plugins/modules/pfsense_dns_resolver.py b/plugins/modules/pfsense_dns_resolver.py index 90002b5d..ba218925 100644 --- a/plugins/modules/pfsense_dns_resolver.py +++ b/plugins/modules/pfsense_dns_resolver.py @@ -122,10 +122,12 @@ descr: description: A description may be entered here for administrative reference. required: false + default: "" type: str aliases: description: Additional names for this host. required: false + default: [] type: list elements: dict suboptions: @@ -144,7 +146,6 @@ domainoverrides: description: Domains for which the resolver's standard DNS lookup should be overridden. required: false - default: [] type: list elements: dict suboptions: @@ -216,7 +217,7 @@ edns_buffer_size: description: Number of bytes to advertise as the EDNS reassembly buffer size. required: false - default: "4096" + default: "auto" choices: [ "auto", "512", "1220", "1232", "1432", "1480", "4096" ] type: str num_queries_per_thread: @@ -303,7 +304,7 @@ DNS_RESOLVER_DOMAIN_OVERRIDE_SPEC = dict( domain=dict(required=True, type='str'), ip=dict(required=True, type='str'), - descr=dict(default='', type='str'), + descr=dict(type='str'), tls_hostname=dict(default='', type='str'), forward_tls_upstream=dict(default='', type='str'), ) @@ -347,7 +348,7 @@ regovpnclients=dict(default=False, type='bool'), custom_options=dict(default="", type='str'), hosts=dict(default=[], type='list', elements='dict', options=DNS_RESOLVER_HOST_SPEC), - domainoverrides=dict(default=[], type='list', elements='dict', options=DNS_RESOLVER_DOMAIN_OVERRIDE_SPEC), + domainoverrides=dict(type='list', elements='dict', options=DNS_RESOLVER_DOMAIN_OVERRIDE_SPEC), # Advanced Settings hideidentity=dict(default=True, type='bool'), hideversion=dict(default=True, type='bool'), @@ -361,7 +362,7 @@ msgcachesize=dict(default=4, type='int', choices=[4, 10, 20, 50, 100, 250, 512]), outgoing_num_tcp=dict(default=10, type='int', choices=[0, 10, 20, 30, 50]), incoming_num_tcp=dict(default=10, type='int', choices=[0, 10, 20, 30, 50]), - edns_buffer_size=dict(default="4096", type='str', choices=["auto", "512", "1220", "1232", "1432", "1480", "4096"]), + edns_buffer_size=dict(default="auto", type='str', choices=["auto", "512", "1220", "1232", "1432", "1480", "4096"]), num_queries_per_thread=dict(default=512, type='int', choices=[512, 1024, 2048]), jostle_timeout=dict(default=200, type='int', choices=[100, 200, 500, 1000]), cache_max_ttl=dict(default=86400, type='int'), @@ -462,6 +463,9 @@ def _params_to_obj(self): host["aliases"] = { "item": tmp_aliases } + else: + # Default is an empty element + host["aliases"] = "\n\t\t\t" return obj