diff --git a/docs/source/modules/2_list.rst b/docs/source/modules/2_list.rst index 6a38d17..79d2587 100644 --- a/docs/source/modules/2_list.rst +++ b/docs/source/modules/2_list.rst @@ -21,7 +21,7 @@ In most cases the returned type of this module ist a list of dictionaries. :header: "Parameter", "Type", "Required", "Default", "Aliases", "Comment" :widths: 15 10 10 10 10 45 - "target","string","true","\-","tgt, t","What part of the running config should be queried/listed. One of: 'alias', 'rule', 'route', 'cron', 'syslog', 'package', 'unbound_general', 'unbound_acl', 'unbound_host', 'unbound_domain', 'unbound_dot', 'unbound_forward', 'unbound_host_alias', 'ipsec_cert', 'shaper_pipe', 'shaper_queue', 'shaper_rule', 'monit_service', 'monit_test', 'monit_alert', 'wireguard_server', 'wireguard_peer', 'interface_lagg', 'interface_vlan', 'interface_vxlan', 'source_nat', 'frr_bfd', 'frr_bgp_general', 'frr_bgp_neighbor', 'frr_bgp_prefix_list', 'frr_bgp_community_list', 'frr_bgp_as_path', 'frr_bgp_route_map', 'frr_ospf_general', 'frr_ospf_prefix_list', 'frr_ospf_interface', 'frr_ospf_route_map', 'frr_ospf_network', 'frr_ospf3_general', 'frr_ospf3_interface', 'frr_rip', 'bind_general', 'bind_blocklist', 'bind_acl', 'bind_domain', 'bind_record', 'interface_vip', 'webproxy_general', 'webproxy_cache', 'webproxy_parent', 'webproxy_traffic', 'webproxy_forward', 'webproxy_acl', 'webproxy_icap', 'webproxy_auth', 'webproxy_remote_acl', 'webproxy_pac_proxy', 'webproxy_pac_match', 'webproxy_pac_rule'" + "target","string","true","\-","tgt, t","What part of the running config should be queried/listed. One of: 'alias', 'rule', 'route', 'cron', 'syslog', 'package', 'unbound_general', 'unbound_acl', 'unbound_host', 'unbound_domain', 'unbound_dot', 'unbound_forward', 'unbound_host_alias', 'ipsec_cert', 'shaper_pipe', 'shaper_queue', 'shaper_rule', 'monit_service', 'monit_test', 'monit_alert', 'wireguard_server', 'wireguard_peer', 'interface_lagg', 'interface_vlan', 'interface_vxlan', 'source_nat', 'frr_bfd', 'frr_bgp_general', 'frr_bgp_neighbor', 'frr_bgp_prefix_list', 'frr_bgp_community_list', 'frr_bgp_as_path', 'frr_bgp_route_map', 'frr_ospf_general', 'frr_ospf_prefix_list', 'frr_ospf_interface', 'frr_ospf_route_map', 'frr_ospf_network', 'frr_ospf3_general', 'frr_ospf3_interface', 'frr_rip', 'bind_general', 'bind_blocklist', 'bind_acl', 'bind_domain', 'bind_record', 'interface_vip', 'webproxy_general', 'webproxy_cache', 'webproxy_parent', 'webproxy_traffic', 'webproxy_forward', 'webproxy_acl', 'webproxy_icap', 'webproxy_auth', 'webproxy_remote_acl', 'webproxy_pac_proxy', 'webproxy_pac_match', 'webproxy_pac_rule', 'unbound_dnsbl'" .. include:: ../_include/param_basic.rst diff --git a/docs/source/modules/unbound_dnsbl.rst b/docs/source/modules/unbound_dnsbl.rst new file mode 100644 index 0000000..f7ab61d --- /dev/null +++ b/docs/source/modules/unbound_dnsbl.rst @@ -0,0 +1,88 @@ +.. _modules_unbound_dnsbl: + +.. include:: ../_include/head.rst + +============ +MODULE TITLE +============ + +**STATE**: unstable + +**TESTS**: `Playbook `_ + +**API Docs**: `unbound_dnsbl `_ + +**Service Docs**: `Unbound DNS - Blocklists `_ + + +Definition +********** + +.. csv-table:: Definition + :header: "Parameter", "Type", "Required", "Default", "Aliases", "Comment" + :widths: 15 10 10 10 10 45 + + "parameter name","parameter type","if is required","default value","aliases","description" + "safesearch","boolean","false","false","\-","Force the usage of SafeSearch on Google, DuckDuckGo, Bing, Qwant, PixaBay and YouTube" + "type","list of strings","false","[]","\-","Select which kind of DNSBL you want to use" + "whitelists","list of strings","false","[]","whitelist, allowlist, allowlists","List of domains to whitelist. You can use regular expressions" + "blocklists","list of strings","false","[]","blocklist","List of domains to blocklist. Only exact matches are supported" + "wildcards","list of strings","false","[]","wildcard","List of wildcard domains to blocklist. All subdomains of the given domain will be blocked. Blocking first-level domains is not supported" + "address","strings","false","","\-","Destination ip address for entries in the blocklist (leave empty to use default: 0.0.0.0). Not used when "Return NXDOMAIN" is checked" + "nxdomain","bool","false","false","\-","Use the DNS response code NXDOMAIN instead of a destination address" + "enabled","boolean","false","true","\-","Enable the usage of DNS blocklists" + "reload","boolean","false","true","\-", .. include:: ../_include/param_reload.rst + +.. include:: ../_include/param_basic.rst + +Usage +***** + +Manage Unbound DNS blocklists and exceptions. + +Examples +******** + +.. code-block:: yaml + + - hosts: localhost + gather_facts: false + module_defaults: + group/ansibleguy.opnsense.all: + firewall: 'opnsense.template.ansibleguy.net' + api_credential_file: '/home/guy/.secret/opn.key' + + ansibleguy.opnsense.list: + target: 'unbound_dnsbl' + + tasks: + # add optional parameters commented-out + # required ones normally + # add their default values to get a brief overview of how the module works + - name: Example + ansibleguy.opnsense.unbound_dnsbl: + type: atl + # safesearch: false + # lists: ['https://example.com/dns.blocklist'] + # whitelists: ['ansibleguy.net'] + # blocklists: ['example.net'] + # wildcards: ['example.net'] + # address: 192.168.254.254 + # nxdomain: false + # enable: false + # state: 'absent' + # debug: false + + - name: Configuring DNS Blocklists + ansibleguy.opnsense.unbound_dnsbl: + type: atl + enable: true + + - name: Listing current config + ansibleguy.opnsense.list: + # target: 'unbound_dnsbl' + register: dnsbl_config + + - name: Printing + ansible.builtin.debug: + var: dnsbl_config.data diff --git a/meta/runtime.yml b/meta/runtime.yml index 7db756a..b0a5ddc 100644 --- a/meta/runtime.yml +++ b/meta/runtime.yml @@ -20,6 +20,7 @@ action_groups: - ansibleguy.opnsense.unbound_host - ansibleguy.opnsense.unbound_domain - ansibleguy.opnsense.unbound_host_alias + - ansibleguy.opnsense.unbound_dnsbl ipsec: - ansibleguy.opnsense.ipsec_auth_local - ansibleguy.opnsense.ipsec_auth_remote diff --git a/plugins/module_utils/main/unbound_dnsbl.py b/plugins/module_utils/main/unbound_dnsbl.py new file mode 100644 index 0000000..24e348a --- /dev/null +++ b/plugins/module_utils/main/unbound_dnsbl.py @@ -0,0 +1,36 @@ +from ansible.module_utils.basic import AnsibleModule + +from ansible_collections.ansibleguy.opnsense.plugins.module_utils.base.api import \ + Session +from ansible_collections.ansibleguy.opnsense.plugins.module_utils.base.cls import GeneralModule + + +# Supported as of OPNsense 23.7 +class DnsBL(GeneralModule): + CMDS = { + 'set': 'set', + 'search': 'get', + } + API_KEY_PATH = 'unbound.dnsbl' + API_KEY_PATH_REQ = API_KEY_PATH + API_MOD = 'unbound' + API_CONT = 'settings' + API_CONT_REL = 'service' + API_CMD_REL = 'reconfigureGeneral' + FIELDS_CHANGE = [ + 'enabled', 'safesearch', 'type', 'lists', 'whitelists', 'blocklists', 'wildcards', 'address', 'nxdomain' + ] + FIELDS_ALL = FIELDS_CHANGE + FIELDS_TYPING = { + 'bool': ['enabled', 'safesearch', 'nxdomain'], + 'list': ['type', 'lists', 'whitelists', 'blocklists', 'wildcards'], + } + + def __init__(self, module: AnsibleModule, result: dict, session: Session = None): + GeneralModule.__init__(self=self, m=module, r=result, s=session) + + def check(self) -> None: + # pylint: disable=W0201 + self.settings = self._search_call() + + self._build_diff() diff --git a/plugins/modules/list.py b/plugins/modules/list.py index bb00f1c..eb2cc44 100644 --- a/plugins/modules/list.py +++ b/plugins/modules/list.py @@ -36,7 +36,7 @@ 'ipsec_child', 'ipsec_vti', 'ipsec_auth_local', 'ipsec_auth_remote', 'frr_general', 'unbound_general', 'unbound_acl', 'ids_general', 'ids_policy', 'ids_rule', 'ids_ruleset', 'ids_user_rule', 'ids_policy_rule', 'openvpn_instance', 'openvpn_static_key', 'openvpn_client_override', 'dhcrelay_destination', 'dhcrelay_relay', - 'interface_lagg', 'interface_loopback', + 'interface_lagg', 'interface_loopback', 'unbound_dnsbl', ] @@ -119,6 +119,10 @@ def run_module(): from ansible_collections.ansibleguy.opnsense.plugins.module_utils.main.unbound_forward \ import Forward as Target_Obj + elif target == 'unbound_dnsbl': + from ansible_collections.ansibleguy.opnsense.plugins.module_utils.main.unbound_dnsbl import \ + DnsBL as Target_Obj + elif target == 'syslog': from ansible_collections.ansibleguy.opnsense.plugins.module_utils.main.syslog import \ Syslog as Target_Obj diff --git a/plugins/modules/unbound_dnsbl.py b/plugins/modules/unbound_dnsbl.py new file mode 100644 index 0000000..4319eeb --- /dev/null +++ b/plugins/modules/unbound_dnsbl.py @@ -0,0 +1,91 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (C) 2024, AnsibleGuy +# GNU General Public License v3.0+ (see https://www.gnu.org/licenses/gpl-3.0.txt) + +# see: https://docs.opnsense.org/development/api/plugins/unbound.html + +from ansible.module_utils.basic import AnsibleModule + +from ansible_collections.ansibleguy.opnsense.plugins.module_utils.base.handler import \ + module_dependency_error, MODULE_EXCEPTIONS + +try: + from ansible_collections.ansibleguy.opnsense.plugins.module_utils.helper.wrapper import module_wrapper + from ansible_collections.ansibleguy.opnsense.plugins.module_utils.defaults.main import \ + OPN_MOD_ARGS, EN_ONLY_MOD_ARG, RELOAD_MOD_ARG + from ansible_collections.ansibleguy.opnsense.plugins.module_utils.main.unbound_dnsbl import DnsBL + +except MODULE_EXCEPTIONS: + module_dependency_error() + + +# DOCUMENTATION = 'https://opnsense.ansibleguy.net/en/latest/modules/unbound_general.html' +# EXAMPLES = 'https://opnsense.ansibleguy.net/en/latest/modules/unbound_general.html' + + +def run_module(): + module_args = dict( + safesearch=dict( + type='bool', required=False, default=False, + description='Force the usage of SafeSearch on Google, DuckDuckGo, Bing, Qwant, PixaBay and YouTube' + ), + type=dict( + type='list', elements='str', required=False, default=[], aliases=['bl'], + description='Select which kind of DNSBL you want to use' + ), + lists=dict( + type='list', elements='str', required=False, default=[], aliases=['list'], + description='List of urls from where blocklist will be downloaded' + ), + whitelists=dict( + type='list', elements='str', required=False, default=[], aliases=['whitelist', 'allowlist', 'allowlists'], + description='List of domains to whitelist. You can use regular expressions' + ), + blocklists=dict( + type='list', elements='str', required=False, default=[], aliases=['blocklist'], + description='List of domains to blocklist. Only exact matches are supported' + ), + wildcards=dict( + type='list', elements='str', required=False, default=[], aliases=['wildcard'], + description='List of wildcard domains to blocklist. All subdomains of the given domain will be blocked. ' + 'Blocking first-level domains is not supported' + ), + address=dict( + type='str', required=False, + description='Destination ip address for entries in the blocklist (leave empty to use default: 0.0.0.0). ' + 'Not used when "Return NXDOMAIN" is checked' + ), + nxdomain=dict( + type='bool', required=False, default=False, + description='Use the DNS response code NXDOMAIN instead of a destination address' + ), + **EN_ONLY_MOD_ARG, + **OPN_MOD_ARGS, + **RELOAD_MOD_ARG, + ) + + result = dict( + changed=False, + diff={ + 'before': {}, + 'after': {}, + } + ) + + module = AnsibleModule( + argument_spec=module_args, + supports_check_mode=True, + ) + + module_wrapper(DnsBL(module=module, result=result)) + module.exit_json(**result) + + +def main(): + run_module() + + +if __name__ == '__main__': + main() diff --git a/tests/unbound_dnsbl.yml b/tests/unbound_dnsbl.yml new file mode 100644 index 0000000..f246413 --- /dev/null +++ b/tests/unbound_dnsbl.yml @@ -0,0 +1,123 @@ +--- + +- name: Testing Unbound DNS general settings + hosts: localhost + gather_facts: no + module_defaults: + group/ansibleguy.opnsense.all: + firewall: "{{ lookup('ansible.builtin.env', 'TEST_FIREWALL') }}" + api_credential_file: "{{ lookup('ansible.builtin.env', 'TEST_API_KEY') }}" + ssl_verify: false + + ansibleguy.opnsense.list: + target: 'unbound_dnsbl' + + tasks: + - name: Listing + ansibleguy.opnsense.list: + register: opn_pre1 + failed_when: > + opn_pre1.failed or + 'data' not in opn_pre1 + + - name: Configuring - failing because of invalid type + ansibleguy.opnsense.unbound_dnsbl: + type: + - ANSIBLE_TEST_1 + register: opn_fail1 + failed_when: not opn_fail1.failed + when: not ansible_check_mode + + - name: Configuring - failing because of invalid address + ansibleguy.opnsense.unbound_dnsbl: + address: INVALID + register: opn_fail2 + failed_when: not opn_fail2.failed + when: not ansible_check_mode + + - name: Reset Configuring + ansibleguy.opnsense.unbound_dnsbl: + when: not ansible_check_mode + + - name: Configuring + ansibleguy.opnsense.unbound_dnsbl: + type: + - atf + lists: ['https://example.com/dns.blocklist'] + whitelists: + - opnsense.org + - ansibleguy.net + blocklists: + - example.tor + wildcards: + - example.tor + address: 192.168.255.255 + nxdomain: false + enabled: true + register: opn1 + failed_when: > + opn1.failed or + not opn1.changed + + - name: Changing + ansibleguy.opnsense.unbound_dnsbl: + type: + - atf + whitelists: + - opnsense.org + - ansibleguy.net + blocklists: + - example.tor + wildcards: + - example.tor + nxdomain: true + enabled: true + register: opn2 + failed_when: > + opn2.failed or + not opn2.changed + + - name: Disabling 1 + ansibleguy.opnsense.unbound_dnsbl: + type: + - atf + whitelists: + - opnsense.org + - ansibleguy.net + blocklists: + - example.tor + wildcards: + - example.tor + nxdomain: true + enabled: false + reload: false # speed + register: opn3 + failed_when: > + opn3.failed or + not opn3.changed + when: not ansible_check_mode + + - name: Nothing changed + ansibleguy.opnsense.unbound_dnsbl: + type: + - atf + whitelists: + - opnsense.org + - ansibleguy.net + blocklists: + - example.tor + wildcards: + - example.tor + nxdomain: true + enabled: false + reload: false # speed + register: opn4 + failed_when: > + opn4.failed or + opn4.changed + when: not ansible_check_mode + + - name: Cleanup + ansibleguy.opnsense.unbound_dnsbl: + reload: false + when: not ansible_check_mode