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.
This commit is contained in:
Q-Feeds 2025-12-19 09:58:20 +01:00 committed by GitHub
parent 81f3e21a1a
commit d987a7e53e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 133 additions and 3 deletions

View file

@ -19,6 +19,7 @@
<ep>Easyprivacy List</ep>
<nc>NoCoin List</nc>
<pt>PornTop1M List</pt>
<qf>Q-Feeds (when installed)</qf>
<sa>Simple Ad List</sa>
<st>Simple Tracker List</st>
<sb>Steven Black List</sb>

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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)

View file

@ -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):

View file

@ -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