Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 34 additions & 7 deletions packages/ns-api/files/ns.threatshield
Original file line number Diff line number Diff line change
Expand Up @@ -736,12 +736,37 @@ def dns_list_zones(e_uci):
zones.append(zone['name'])
return { 'data': zones }

def dns_get_enforced_zones(e_uci):
# the user selection is stored in ns_tsdns_zones: adb_nftdevforce holds the
# devices derived from it and can't be mapped back to zones without ambiguity
zones = list(e_uci.get('adblock', 'global', 'ns_tsdns_zones', list=True, default=[]))
if zones:
return zones
# before the switch to device-based enforcement the selection was stored inside
# adb_nftdevforce: keep reading it until the migration has run
return list(e_uci.get('adblock', 'global', 'adb_nftdevforce', list=True, default=['lan']))
Comment on lines +739 to +747

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After the uci default, this is not needed, right?
We can safely remove it and put this logic underneath


def dns_zones_to_devices(e_uci, zones):
# adb_nftdevforce is rendered as an nft iifname match, so it needs the devices
# of the selected zones and not the zone names
devices = []
for zone in zones:
for device in utils.get_all_devices_by_zone(e_uci, zone, exclude_aliases=True):
if device not in devices:
devices.append(device)
return sorted(devices)
Comment on lines +752 to +757

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Next time you can use a set, so you don't need to check for presence in the list, for the number of occurrences, do not bother changing it


def dns_set_enforced_devices(e_uci, zones):
devices = dns_zones_to_devices(e_uci, zones)
if devices:
e_uci.set('adblock', 'global', 'adb_nftdevforce', devices)
elif e_uci.get('adblock', 'global', 'adb_nftdevforce', list=True, default=[]):
# no device in the selected zones: adblock skips the enforcement altogether
e_uci.delete('adblock', 'global', 'adb_nftdevforce')

def dns_list_settings(e_uci):
ts_enabled = e_uci.get('adblock', 'global', 'ts_enabled', default='0')
try:
zones = list(e_uci.get_all('adblock', 'global', 'adb_nftdevforce'))
except:
zones = ['lan']
zones = dns_get_enforced_zones(e_uci)
try:
ports = list(e_uci.get_all('adblock', 'global', 'adb_nftportforce'))
except:
Expand All @@ -750,13 +775,15 @@ def dns_list_settings(e_uci):

def dns_edit_settings(e_uci, payload):
if payload['enabled']:
if 'zones' in payload and 'wan' in payload['zones']:
raise ValidationError('zones', 'wan_zone_not_allowed', payload['zones'])
zones = payload.get('zones', ['lan'])
if 'wan' in zones:
raise ValidationError('zones', 'wan_zone_not_allowed', zones)
e_uci.set('adblock', 'global', 'ts_enabled', '1')
e_uci.set('adblock', 'global', 'adb_enabled', '1')
e_uci.set('adblock', 'global', 'adb_triggerdelay', '30')
e_uci.set('adblock', 'global', 'adb_nftforce', '1')
e_uci.set('adblock', 'global', 'adb_nftdevforce', payload.get('zones', ['lan']))
e_uci.set('adblock', 'global', 'ns_tsdns_zones', zones)
dns_set_enforced_devices(e_uci, zones)
e_uci.set('adblock', 'global', 'adb_nftportforce', payload.get('ports', ['53', '853']))
else:
e_uci.set('adblock', 'global', 'ts_enabled', '0')
Expand Down
4 changes: 4 additions & 0 deletions packages/ns-threat_shield/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,13 @@ define Package/ns-threat_shield/install
$(INSTALL_DATA) ./files/banip.nethesis.feeds $(1)/etc/banip
$(INSTALL_BIN) ./files/adjust-banip.py $(1)/usr/libexec/ns-api/post-commit/
$(INSTALL_BIN) ./files/configure-banip-wans.py $(1)/usr/libexec/ns-api/pre-commit/
$(INSTALL_BIN) ./files/configure-adblock-devices.py $(1)/usr/libexec/ns-api/pre-commit/
$(INSTALL_DIR) $(1)/usr/libexec

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move it at the top where all install dir resides, this missing could've cause issues.

$(INSTALL_BIN) ./files/adblock-devices-migrate.py $(1)/usr/libexec/ts-dns-migrate-devices
$(INSTALL_DIR) $(1)/etc/uci-defaults
$(INSTALL_BIN) ./files/banip-defaults $(1)/etc/uci-defaults/99-nethsec-banip
$(INSTALL_BIN) ./files/35_ns-threat_shield $(1)/etc/uci-defaults/35_ns-threat_shield
$(INSTALL_BIN) ./files/36_ns-threat_shield $(1)/etc/uci-defaults/36_ns-threat_shield
gzip -9n $(1)/usr/share/threat_shield/nethesis-dns.sources
gzip -9n $(1)/usr/share/threat_shield/community-dns.sources
endef
Expand Down
9 changes: 8 additions & 1 deletion packages/ns-threat_shield/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,16 @@ ts-ip
Threat shield DNS (`ts-dns`) is a special configuration for [adblock](https://github.com/openwrt/packages/tree/master/net/adblock).
The `ts-dns` is invoked every time adblock is started or reloaded.

The package adds a new option to `adblock`:
The package adds new options to `adblock`:

- `ts_enabled`: if set to `1`, it enables the download of enterprise categories and community free categories.
- `ns_tsdns_zones`: the firewall zones where the local DNS enforcement is applied.

Since adblock 4.5.5 the enforcement is rendered as nft rules matching on `iifname`, so the
`adb_nftdevforce` option needs network devices and not zone names. It is therefore a derived
value: it is computed from `ns_tsdns_zones` by the API and kept aligned with the network setup
by the `configure-adblock-devices` pre-commit hook, so that adding an interface to an enforced
zone does not leave its DNS traffic unfiltered. Do not edit `adb_nftdevforce` by hand.

If `ts_enabled` is set to 1:

Expand Down
13 changes: 13 additions & 0 deletions packages/ns-threat_shield/files/36_ns-threat_shield
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#!/bin/sh

#
# Copyright (C) 2026 Nethesis S.r.l.
# SPDX-License-Identifier: GPL-2.0-only
#

# Rebuild the adblock local DNS enforcement on systems upgraded from adblock 4.1.5,
# where adb_nftdevforce ended up holding firewall zone names instead of devices.

set -e

/usr/libexec/ts-dns-migrate-devices
54 changes: 54 additions & 0 deletions packages/ns-threat_shield/files/adblock-devices-migrate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
#!/usr/bin/env python

#
# Copyright (C) 2026 Nethesis S.r.l.
# SPDX-License-Identifier: GPL-2.0-only
#

# this script is supposed to be run by the 36_ns-threat_shield uci defaults
#
# Up to adblock 4.1.5 the local DNS enforcement was configured through adb_zonelist,
# holding firewall zone names, and adblock turned it into uci redirect sections
# letting fw4 resolve the zones. Since 4.5.5 the enforcement is rendered as nft rules
# matching on iifname, so adb_nftdevforce needs device names: the zone names stored
# there produce rules that never match and DNS traffic is no longer redirected.
#
# Move the selection to ns_tsdns_zones and rebuild adb_nftdevforce out of it.

import subprocess

from euci import EUci
from nethsec import utils


def migrate_zones():
e_uci = EUci()

if list(e_uci.get('adblock', 'global', 'ns_tsdns_zones', list=True, default=[])):
# already migrated
return False

zones = list(e_uci.get('adblock', 'global', 'adb_nftdevforce', list=True, default=[]))
if not zones:
return False

devices = []
for zone in zones:
for device in utils.get_all_devices_by_zone(e_uci, zone, exclude_aliases=True):
if device not in devices:
devices.append(device)

if not devices:
# the stored values are not zone names, or the zones have no interface:
# leave the configuration untouched rather than clearing it
return False

e_uci.set('adblock', 'global', 'ns_tsdns_zones', zones)
e_uci.set('adblock', 'global', 'adb_nftdevforce', sorted(devices))
e_uci.commit('adblock')
return True


if __name__ == "__main__":
if migrate_zones():
subprocess.run(["/etc/init.d/adblock", "restart"], capture_output=True)
40 changes: 40 additions & 0 deletions packages/ns-threat_shield/files/configure-adblock-devices.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
#!/usr/bin/python

#
# Copyright (C) 2026 Nethesis S.r.l.
# SPDX-License-Identifier: GPL-2.0-only
#

# This script keeps the adblock local DNS enforcement aligned with the network setup:
# adb_nftdevforce is rendered as an nft iifname match, so it must list the devices of
# the zones selected in ns_tsdns_zones. Without this, adding an interface to an
# enforced zone would silently leave its DNS traffic unfiltered.

# The changes variable is already within the scope from the caller
if 'adblock' in changes or 'firewall' in changes or 'network' in changes:
import syslog
from euci import EUci
from nethsec import utils

uci = EUci()
zones = list(uci.get('adblock', 'global', 'ns_tsdns_zones', list=True, default=[]))

if zones:
devices = []
for zone in zones:
for device in utils.get_all_devices_by_zone(uci, zone, exclude_aliases=True):
if device not in devices:
devices.append(device)
# keep a stable order, the value is compared before being rewritten
devices = sorted(devices)

current = list(uci.get('adblock', 'global', 'adb_nftdevforce', list=True, default=[]))
if devices != current:
if devices:
uci.set('adblock', 'global', 'adb_nftdevforce', devices)
else:
uci.delete('adblock', 'global', 'adb_nftdevforce')
uci.save('adblock')
# adblock is reloaded by the uci reload_config at the end of the commit
if 'adblock' not in changes:
changes['adblock'] = {}
Loading