diff --git a/src/opnsense/scripts/unbound/blocklists.py b/src/opnsense/scripts/unbound/blocklists.py
index 3e42c58eb..05a36acb5 100755
--- a/src/opnsense/scripts/unbound/blocklists.py
+++ b/src/opnsense/scripts/unbound/blocklists.py
@@ -35,7 +35,7 @@ import time
import fcntl
from configparser import ConfigParser
import requests
-import json
+import ujson
def uri_reader(uri):
req_opts = {
@@ -76,8 +76,6 @@ def uri_reader(uri):
'blocklist download : unable to download file from %s (status_code: %d)' % (uri, req.status_code)
)
-
-
if __name__ == '__main__':
# check for a running download process, this may take a while so it's better to check...
try:
@@ -100,9 +98,33 @@ if __name__ == '__main__':
'data': {},
'config': {}
}
+ skip_download = False
if os.path.exists('/tmp/unbound-blocklists.conf'):
cnf = ConfigParser()
cnf.read('/tmp/unbound-blocklists.conf')
+
+ cnf_cache = ConfigParser()
+ if os.path.exists('/tmp/unbound-blocklists.conf.cache'):
+ cnf_cache.read('/tmp/unbound-blocklists.conf.cache')
+ else:
+ cnf_cache.read('/tmp/unbound-blocklists.conf')
+
+ if cnf.sections() and cnf_cache.sections():
+ # get the difference between the old and new configuration, there won't be any
+ # if we're starting up, so it will proceed as normal.
+ diff_cnf = {d: set(map(tuple, v.items())) for d,v in cnf._sections.items()}
+ diff_cnf_cache = {d: set(map(tuple, v.items())) for d,v in cnf_cache._sections.items()}
+ diffs_added = {header: diff_cnf[header] - diff_cnf_cache[header] for header, _ in diff_cnf.items()}
+ diffs_removed = {header: diff_cnf_cache[header] - diff_cnf[header] for header, _ in diff_cnf.items()}
+
+ # we can only skip download if the include option has changed, but it must proceed
+ # if any other option has changed
+ if (diffs_added['include'] or diffs_removed['include']):
+ skip_download = True
+ for (a, r) in zip(diffs_added, diffs_removed):
+ if (a != 'include' and r != 'include') and (diffs_added[a] or diffs_removed[r]):
+ skip_download = False
+
# exclude (white) lists, compile to regex to be used to filter blocklist entries
if cnf.has_section('exclude'):
exclude_list = set()
@@ -123,48 +145,70 @@ if __name__ == '__main__':
whitelist_pattern = re.compile(wp, re.IGNORECASE)
syslog.syslog(syslog.LOG_NOTICE, 'blocklist download : exclude domains matching %s' % wp)
- # fetch all blocklists
- if cnf.has_section('settings'):
- if cnf.has_option('settings', 'address'):
- blocklist_items['config']['dst_addr'] = cnf.get('settings', 'address')
- if cnf.has_option('settings', 'rcode'):
- blocklist_items['config']['rcode'] = cnf.get('settings', 'rcode')
- if cnf.has_section('blocklists'):
- for blocklist in cnf['blocklists']:
- list_type = blocklist.split('_', 1)
- bl_shortcode = 'Custom' if list_type[0] == 'custom' else list_type[1]
- file_stats = {'uri': cnf['blocklists'][blocklist], 'skip' : 0, 'blocklist': 0, 'lines' :0}
- for line in uri_reader(cnf['blocklists'][blocklist]):
- file_stats['lines'] += 1
- # cut line into parts before comment marker (if any)
- tmp = line.split('#')[0].split()
- entry = None
- while tmp:
- entry = tmp.pop(-1)
- if entry not in ['127.0.0.1', '0.0.0.0']:
- break
- if entry:
- domain = entry.lower()
- if whitelist_pattern.match(entry):
- file_stats['skip'] += 1
- else:
- if domain_pattern.match(domain):
- file_stats['blocklist'] += 1
- blocklist_items['data'][entry] = {"bl": bl_shortcode}
- else:
+ if not skip_download:
+ # fetch all blocklists, will replace the existing file used by Unbound
+ if cnf.has_section('settings'):
+ if cnf.has_option('settings', 'address'):
+ blocklist_items['config']['dst_addr'] = cnf.get('settings', 'address')
+ if cnf.has_option('settings', 'rcode'):
+ blocklist_items['config']['rcode'] = cnf.get('settings', 'rcode')
+ if cnf.has_section('blocklists'):
+ for blocklist in cnf['blocklists']:
+ list_type = blocklist.split('_', 1)
+ bl_shortcode = 'Custom' if list_type[0] == 'custom' else list_type[1]
+ file_stats = {'uri': cnf['blocklists'][blocklist], 'skip' : 0, 'blocklist': 0, 'lines' :0}
+ for line in uri_reader(cnf['blocklists'][blocklist]):
+ file_stats['lines'] += 1
+ # cut line into parts before comment marker (if any)
+ tmp = line.split('#')[0].split()
+ entry = None
+ while tmp:
+ entry = tmp.pop(-1)
+ if entry not in ['127.0.0.1', '0.0.0.0']:
+ break
+ if entry:
+ domain = entry.lower()
+ if whitelist_pattern.match(entry):
file_stats['skip'] += 1
+ else:
+ if domain_pattern.match(domain):
+ file_stats['blocklist'] += 1
+ blocklist_items['data'][entry] = {"bl": bl_shortcode}
+ else:
+ file_stats['skip'] += 1
- syslog.syslog(
- syslog.LOG_NOTICE,
- 'blocklist download %(uri)s (lines: %(lines)d exclude: %(skip)d block: %(blocklist)d)' % file_stats
- )
+ syslog.syslog(
+ syslog.LOG_NOTICE,
+ 'blocklist download %(uri)s (lines: %(lines)d exclude: %(skip)d block: %(blocklist)d)' % file_stats
+ )
+
+ # after a download, always apply exact custom matches on top of it
+ if cnf.has_section('include'):
+ for item in cnf['include']:
+ entry = cnf['include'][item].rstrip().lower()
+ if domain_pattern.match(entry):
+ blocklist_items['data'][entry] = {"bl": "Custom"}
+
+ else:
+ # only modify the existing list, administrate on added and removed exact custom matches
+ syslog.syslog(syslog.LOG_NOTICE, 'blocklist: skip download')
+ if (diffs_added['include'] or diffs_removed['include']) and os.path.exists('/var/unbound/data/dnsbl.json'):
+ blocklist_items = ujson.load(open('/var/unbound/data/dnsbl.json', 'r'))
+ for item in diffs_removed['include']:
+ del blocklist_items['data'][item[1].rstrip().lower()]
+ for item in diffs_added['include']:
+ blocklist_items['data'][item[1].rstrip().lower()] = {"bl": "Custom"}
+
+ with open('/tmp/unbound-blocklists.conf.cache', 'w') as cache_config:
+ # cache the current config so we can diff on it the next time
+ cnf.write(cache_config)
# write out results
if not os.path.exists('/var/unbound/data'):
os.makedirs('/var/unbound/data')
with open("/var/unbound/data/dnsbl.json.new", 'w') as unbound_outf:
if blocklist_items:
- json.dump(blocklist_items, unbound_outf)
+ ujson.dump(blocklist_items, unbound_outf)
# atomically replace the current dnsbl so unbound can pick up on it
os.replace('/var/unbound/data/dnsbl.json.new', '/var/unbound/data/dnsbl.json')
diff --git a/src/opnsense/service/templates/OPNsense/Unbound/core/blocklists.conf b/src/opnsense/service/templates/OPNsense/Unbound/core/blocklists.conf
index 3ba0089fa..47d12297b 100644
--- a/src/opnsense/service/templates/OPNsense/Unbound/core/blocklists.conf
+++ b/src/opnsense/service/templates/OPNsense/Unbound/core/blocklists.conf
@@ -69,4 +69,12 @@ custom_pattern_{{loop.index}}={{ pattern }}
{% endfor %}
{% endif %}
+[include]
+{% if not helpers.empty('OPNsense.unboundplus.dnsbl.blocklists')%}
+# user defined
+{% for pattern in OPNsense.unboundplus.dnsbl.blocklists.split(',') %}
+custom_pattern_{{loop.index}}={{ pattern }}
+{% endfor %}
+{% endif %}
+
{% endif %}