From d987a7e53eca19362a8935916014d7ac726f9c7a Mon Sep 17 00:00:00 2001 From: Q-Feeds Date: Fri, 19 Dec 2025 09:58:20 +0100 Subject: [PATCH] Feature/dnscrypt proxy blocklist support (#5083) * Add ports to Events page * fixes race condition updating the blocklist * Native integration with DNSCrypt-proxy Added Q-Feeds domains to the DNSBL list of DNSCrypt-Proxy. Changed since the initial way, this is more native. Q-Feeds domains txt files only created if DNSCrypt-proxy is installed and if the list (qf) is selected. --- .../models/OPNsense/Dnscryptproxy/Dnsbl.xml | 1 + .../scripts/OPNsense/Dnscryptproxy/dnsbl.sh | 10 ++ security/q-feeds-connector/Makefile | 2 +- security/q-feeds-connector/pkg-descr | 8 ++ .../dnscryptproxy/blocklists/qfeeds_bl.py | 92 +++++++++++++++++++ .../opnsense/scripts/qfeeds/lib/__init__.py | 21 ++++- .../conf/actions.d/actions_qfeeds.conf | 2 +- 7 files changed, 133 insertions(+), 3 deletions(-) create mode 100644 security/q-feeds-connector/src/opnsense/scripts/dnscryptproxy/blocklists/qfeeds_bl.py diff --git a/dns/dnscrypt-proxy/src/opnsense/mvc/app/models/OPNsense/Dnscryptproxy/Dnsbl.xml b/dns/dnscrypt-proxy/src/opnsense/mvc/app/models/OPNsense/Dnscryptproxy/Dnsbl.xml index 188564ee6..ba05d656e 100644 --- a/dns/dnscrypt-proxy/src/opnsense/mvc/app/models/OPNsense/Dnscryptproxy/Dnsbl.xml +++ b/dns/dnscrypt-proxy/src/opnsense/mvc/app/models/OPNsense/Dnscryptproxy/Dnsbl.xml @@ -19,6 +19,7 @@ Easyprivacy List NoCoin List PornTop1M List + Q-Feeds (when installed) Simple Ad List Simple Tracker List Steven Black List diff --git a/dns/dnscrypt-proxy/src/opnsense/scripts/OPNsense/Dnscryptproxy/dnsbl.sh b/dns/dnscrypt-proxy/src/opnsense/scripts/OPNsense/Dnscryptproxy/dnsbl.sh index 23bb78589..77420eef0 100755 --- a/dns/dnscrypt-proxy/src/opnsense/scripts/OPNsense/Dnscryptproxy/dnsbl.sh +++ b/dns/dnscrypt-proxy/src/opnsense/scripts/OPNsense/Dnscryptproxy/dnsbl.sh @@ -154,6 +154,13 @@ simpletrack() { rm ${WORKDIR}/simpletrack-raw } +qfeeds() { + # Q-Feeds List + if [ -f "/usr/local/etc/dnscrypt-proxy/blacklist-qfeeds.txt" ] && [ -s "/usr/local/etc/dnscrypt-proxy/blacklist-qfeeds.txt" ]; then + sed "/\.$/d" /usr/local/etc/dnscrypt-proxy/blacklist-qfeeds.txt | sed "/^#/d" | sed "/\_/d" | sed "/^[[:space:]]*$/d" | sed "/\.\./d" | sed "s/^\.//g" > ${WORKDIR}/qfeeds + fi +} + install() { # Put all files in correct format for FILE in $(find ${WORKDIR} -type f); do @@ -222,6 +229,9 @@ for CAT in $(echo ${DNSBL} | tr ',' ' '); do yy) yoyo ;; + qf) + qfeeds + ;; esac done diff --git a/security/q-feeds-connector/Makefile b/security/q-feeds-connector/Makefile index b2bf170b2..cf53d91cd 100644 --- a/security/q-feeds-connector/Makefile +++ b/security/q-feeds-connector/Makefile @@ -1,5 +1,5 @@ PLUGIN_NAME= q-feeds-connector -PLUGIN_VERSION= 1.3 +PLUGIN_VERSION= 1.4 PLUGIN_TIER= 2 PLUGIN_COMMENT= Connector for Q-Feeds threat intel PLUGIN_MAINTAINER= devel@qfeeds.com diff --git a/security/q-feeds-connector/pkg-descr b/security/q-feeds-connector/pkg-descr index c336c3b3d..02c1a8ae8 100644 --- a/security/q-feeds-connector/pkg-descr +++ b/security/q-feeds-connector/pkg-descr @@ -2,6 +2,14 @@ Connector for Q-Feeds threat intel Plugin Changelog ================ +1.4 + +* Feature: Added DNSCrypt-Proxy + +1.3 + +* Widget: Added license info +* Events: added source and destination port 1.3 diff --git a/security/q-feeds-connector/src/opnsense/scripts/dnscryptproxy/blocklists/qfeeds_bl.py b/security/q-feeds-connector/src/opnsense/scripts/dnscryptproxy/blocklists/qfeeds_bl.py new file mode 100644 index 000000000..ab04f8192 --- /dev/null +++ b/security/q-feeds-connector/src/opnsense/scripts/dnscryptproxy/blocklists/qfeeds_bl.py @@ -0,0 +1,92 @@ +#!/usr/local/bin/python3 + +""" + Copyright (c) 2025 Deciso B.V. + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, + INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY + AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, + OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF + SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN + CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + POSSIBILITY OF SUCH DAMAGE. +""" + +import os +import glob +import re + +# Check if 'qf' is selected in DNSCrypt-proxy DNSBL configuration +def is_qf_selected(): + rc_conf_file = '/etc/rc.conf.d/dnscrypt_proxy' + if os.path.exists(rc_conf_file): + try: + with open(rc_conf_file, 'r') as f: + for line in f: + # Look for dnscrypt_proxy_dnsbl="..." line + match = re.search(r'dnscrypt_proxy_dnsbl="([^"]*)"', line) + if match: + dnsbl_list = match.group(1) + # Check if 'qf' is in the comma-separated list + return 'qf' in [x.strip() for x in dnsbl_list.split(',')] + except Exception: + pass + return False + +# Q-Feeds domain files directory +qfeeds_tables_dir = '/var/db/qfeeds-tables' + +# Automatically find all domain files (*_domains.txt) in qfeeds-tables directory +# This will include malware_domains.txt, phishing_domains.txt, etc. +qfeeds_filenames = [] +if os.path.isdir(qfeeds_tables_dir): + # Find all files ending with _domains.txt (e.g., malware_domains.txt, phishing_domains.txt) + pattern = os.path.join(qfeeds_tables_dir, '*_domains.txt') + qfeeds_filenames = sorted(glob.glob(pattern)) + +# Collect q-feeds domains +qfeeds_domains = set() +for filename in qfeeds_filenames: + if os.path.exists(filename): + with open(filename, 'r') as f_in: + for line in f_in: + domain = line.strip() + if domain: + qfeeds_domains.add(domain) + +# Write q-feeds domains to blacklist-qfeeds.txt +# dnscrypt-proxy's dnsbl.sh qfeeds() function will read this file when 'qf' is selected in DNSBL config +qfeeds_blacklist_file = '/usr/local/etc/dnscrypt-proxy/blacklist-qfeeds.txt' +dnscrypt_proxy_dir = '/usr/local/etc/dnscrypt-proxy' + +# Only proceed if DNSCrypt-proxy directory exists (plugin is installed) AND 'qf' is selected +if os.path.isdir(dnscrypt_proxy_dir) and is_qf_selected(): + if qfeeds_domains: + # Write q-feeds domains to separate file + with open(qfeeds_blacklist_file, 'w') as f_out: + for domain in sorted(qfeeds_domains): + f_out.write("%s\n" % domain) + else: + # Remove q-feeds blacklist file if no domains available + if os.path.exists(qfeeds_blacklist_file): + os.remove(qfeeds_blacklist_file) +elif os.path.isdir(dnscrypt_proxy_dir): + # DNSCrypt-proxy is installed but 'qf' is not selected - remove the file if it exists + if os.path.exists(qfeeds_blacklist_file): + os.remove(qfeeds_blacklist_file) + + diff --git a/security/q-feeds-connector/src/opnsense/scripts/qfeeds/lib/__init__.py b/security/q-feeds-connector/src/opnsense/scripts/qfeeds/lib/__init__.py index f0f029c1d..87ba013dc 100755 --- a/security/q-feeds-connector/src/opnsense/scripts/qfeeds/lib/__init__.py +++ b/security/q-feeds-connector/src/opnsense/scripts/qfeeds/lib/__init__.py @@ -46,6 +46,7 @@ class QFeedsActions: 'show_index', 'firewall_load', 'unbound_load', + 'dnscryptproxy_load', 'update', 'stats', 'logs' @@ -121,6 +122,24 @@ class QFeedsActions: subprocess.run(['/usr/local/sbin/configctl', 'unbound', 'dnsbl']) yield 'update unbound blocklist' + def dnscryptproxy_load(self): + script_path = '/usr/local/opnsense/scripts/dnscryptproxy/blocklists/qfeeds_bl.py' + dnscrypt_proxy_dir = '/usr/local/etc/dnscrypt-proxy' + if os.path.exists(script_path) and os.path.isdir(dnscrypt_proxy_dir): + subprocess.run([script_path], capture_output=True, text=True) + # Trigger dnscrypt-proxy DNSBL update to merge blacklist-qfeeds.txt + # Only if DNSCrypt-proxy is installed (directory exists) + result = subprocess.run(['/usr/local/sbin/configctl', 'dnscryptproxy', 'dnsbl'], + capture_output=True, text=True) + if result.returncode == 0: + yield 'update dnscrypt-proxy blocklist' + else: + yield 'dnscrypt-proxy not available' + elif not os.path.isdir(dnscrypt_proxy_dir): + yield 'dnscrypt-proxy not installed' + else: + yield 'dnscrypt-proxy blocklist script not found' + def update(self): update_sleep = 99999 try: @@ -136,7 +155,7 @@ class QFeedsActions: if do_update: if 0 < update_sleep <= 300: time.sleep(update_sleep) - for action in ['fetch_index', 'fetch', 'firewall_load', 'unbound_load']: + for action in ['fetch_index', 'fetch', 'firewall_load', 'unbound_load', 'dnscryptproxy_load']: yield from getattr(self, action)() def stats(self): diff --git a/security/q-feeds-connector/src/opnsense/service/conf/actions.d/actions_qfeeds.conf b/security/q-feeds-connector/src/opnsense/service/conf/actions.d/actions_qfeeds.conf index e2fba096e..9558b9c88 100644 --- a/security/q-feeds-connector/src/opnsense/service/conf/actions.d/actions_qfeeds.conf +++ b/security/q-feeds-connector/src/opnsense/service/conf/actions.d/actions_qfeeds.conf @@ -1,5 +1,5 @@ [reconfigure] -command:/usr/local/opnsense/scripts/qfeeds/qfeedsctl.py fetch_index fetch firewall_load unbound_load && echo 'EXIT OK' +command:/usr/local/opnsense/scripts/qfeeds/qfeedsctl.py fetch_index fetch firewall_load unbound_load dnscryptproxy_load && echo 'EXIT OK' parameters: type:script_output message:reconfigure QFeeds