Security: Q-Feeds Connect - add new options as available in integrated blocklists (#5226)

* Security: Q-Feeds Connect - add new options as available in integrated blocklists, closes https://github.com/opnsense/plugins/issues/5197

This adds allowlists (regex patterns), source_nets Q-Feeds applies on, address to return and optional NXDOMAIN responses.

Please note this version is only compatible with current community versions, business edition installs will have to wait for 26.4.

* Security: Q-Feeds Connect - update version and changelog
This commit is contained in:
Ad Schellevis 2026-02-16 16:58:17 +01:00 committed by GitHub
parent 449323e6a5
commit de4c98eee2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 121 additions and 8 deletions

View file

@ -1,6 +1,5 @@
PLUGIN_NAME= q-feeds-connector
PLUGIN_VERSION= 1.4
PLUGIN_REVISION= 1
PLUGIN_VERSION= 1.5
PLUGIN_COMMENT= Connector for Q-Feeds threat intel
PLUGIN_MAINTAINER= devel@qfeeds.com
PLUGIN_TIER= 2

View file

@ -3,6 +3,14 @@ Connector for Q-Feeds threat intel
Plugin Changelog
================
1.5
* Feature: Add passlist option for unbound
* Feature: Add effective networks for unbound
* Feature: Add NXDOMAIN option for unbound
* Feature: Add dest address for unbound
1.4
* Feature: Added DNSCrypt-Proxy integration

View file

@ -15,4 +15,42 @@
<type>checkbox</type>
<help>Use domain feeds in Unbound DNS blocklist, requires blocklists to be enabled in order to have effect</help>
</field>
<field>
<type>header</type>
<label>Unbound blocklist settings</label>
<style>unbound_options</style>
</field>
<field>
<id>connect.unbound.allowlists</id>
<label>Allowlist Domains</label>
<type>select_multiple</type>
<style>tokenize</style>
<allownew>true</allownew>
<help>List of domains to allow. You can use regular expressions. This allow list only applies to blocklist matches on items in this policy.</help>
</field>
<field>
<id>connect.unbound.source_nets</id>
<label>Source Net(s)</label>
<type>select_multiple</type>
<style>tokenize</style>
<allownew>true</allownew>
<help>Source networks to apply policy on. Examples are 192.168.1.0/24 or 192.168.1.1. Leave empty to apply on everything. All specified networks should use the same protocol family and have equal sizes to avoid priority issues. </help>
</field>
<field>
<id>connect.unbound.address</id>
<label>Destination Address</label>
<type>text</type>
<advanced>true</advanced>
<help>
Destination ip address for entries in the blocklist (leave empty to use default: 0.0.0.0).
Not used when "Return NXDOMAIN" is checked.
</help>
</field>
<field>
<id>connect.unbound.nxdomain</id>
<label>Return NXDOMAIN</label>
<type>checkbox</type>
<advanced>true</advanced>
<help>Use the DNS response code NXDOMAIN instead of a destination address.</help>
</field>
</form>

View file

@ -7,5 +7,21 @@
<apikey type="TextField"/>
<enable_unbound_bl type="BooleanField"/>
</general>
<unbound>
<allowlists type="CSVListField"/>
<source_nets type="NetworkField">
<Multiple>Y</Multiple>
<Strict>Y</Strict>
<ValidationMessage>Please specify a valid network segment or address (IPv4/IPv6). If a mask is provided, please omit the host bits.</ValidationMessage>
<WildcardEnabled>N</WildcardEnabled>
<NetMaskRequired>N</NetMaskRequired>
<AsList>Y</AsList>
</source_nets>
<address type="NetworkField">
<NetMaskAllowed>N</NetMaskAllowed>
<AddressFamily>ipv4</AddressFamily>
</address>
<nxdomain type="BooleanField"/>
</unbound>
</items>
</model>

View file

@ -99,6 +99,13 @@ POSSIBILITY OF SUCH DAMAGE.
}
});
$("#connect\\.general\\.enable_unbound_bl").change(function(){
if ($(this).is(':checked')) {
$(".unbound_options").closest('table').show();
} else {
$(".unbound_options").closest('table').hide();
}
});
let selected_tab = window.location.hash != "" ? window.location.hash : "#settings";
$('a[href="' +selected_tab + '"]').tab('show');

View file

@ -27,12 +27,19 @@
"""
import os
import re
import syslog
import uuid
from . import BaseBlocklistHandler
class DefaultBlocklistHandler(BaseBlocklistHandler):
class QFeedsBlocklistHandler(BaseBlocklistHandler):
def __init__(self):
super().__init__('/usr/local/etc/unbound/qfeeds-blocklists.conf')
self.priority = 50
self._compat_id = str(uuid.uuid4())
def _is_enabled(self):
return self.cnf and self.cnf.has_section('settings') and self.cnf.has_option('settings', 'filenames')
def get_config(self):
# do not use, unbound worker settings
@ -41,11 +48,10 @@ class DefaultBlocklistHandler(BaseBlocklistHandler):
def get_blocklist(self):
# Only return domains if integration is enabled (filenames are offered)
qfeeds_filenames = []
if self.cnf and self.cnf.has_section('settings'):
if self.cnf.has_option('settings', 'filenames'):
qfeeds_filenames = self.cnf.get('settings', 'filenames').split(',')
# touch a file to help qfeedsctl detect the current instance uses its list
open('/tmp/qfeeds-unbound-bl.stat', 'w').write('')
if self._is_enabled():
qfeeds_filenames = self.cnf.get('settings', 'filenames').split(',')
# touch a file to help qfeedsctl detect the current instance uses its list
open('/tmp/qfeeds-unbound-bl.stat', 'w').write('')
result = {}
for filename in qfeeds_filenames:
@ -58,3 +64,35 @@ class DefaultBlocklistHandler(BaseBlocklistHandler):
def get_passlist_patterns(self):
return []
def get_policies(self):
if not self._is_enabled():
return []
cfg = {
'source_nets': [],
'address': '',
'rcode': '',
'id': self._compat_id,
'allowlists': []
}
for k,v in self.cnf['settings'].items():
if k in cfg and v.strip() != '':
if type(cfg[k]) is list:
cfg[k] = v.split(',')
else:
cfg[k] = v.strip()
if cfg['allowlists']:
compiled_passlist = set()
for pattern in cfg['allowlists']:
try:
re.compile(pattern, re.IGNORECASE)
compiled_passlist.add(pattern)
except re.error:
syslog.syslog(syslog.LOG_ERR,'Q-Feeds : skip invalid whitelist exclude pattern "%s"' % pattern)
cfg['passlist'] = '|'.join(compiled_passlist)
del cfg['allowlists']
return [cfg]

View file

@ -2,4 +2,11 @@
not helpers.empty('OPNsense.QFeedsConnector.general.enable_unbound_bl') %}
[settings]
filenames=/var/db/qfeeds-tables/malware_domains.txt
{% if not helpers.empty('OPNsense.QFeedsConnector.unbound') %}
allowlists={{OPNsense.QFeedsConnector.unbound.allowlists|default('')}}
source_nets={{OPNsense.QFeedsConnector.unbound.source_nets|default('')}}
address={{OPNsense.QFeedsConnector.unbound.address|default('0.0.0.0')}}
rcode={% if OPNsense.QFeedsConnector.unbound.nxdomain|default('0') == '1' %}NXDOMAIN{%else%}NOERROR{%endif +%}
cache_ttl={{OPNsense.QFeedsConnector.unbound.cache_ttl|default('72000')}}
{% endif %}
{% endif %}