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
33 changes: 26 additions & 7 deletions packages/ns-api/files/ns.threatshield
Original file line number Diff line number Diff line change
Expand Up @@ -736,12 +736,29 @@ def dns_list_zones(e_uci):
zones.append(zone['name'])
return { 'data': zones }

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 = set()
for zone in zones:
devices.update(utils.get_all_devices_by_zone(e_uci, zone, exclude_aliases=True))
return sorted(devices)

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']
# 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. Installations
# predating the device-based enforcement are converted by 96_ns-threat_shield, so the
# option is always set at this point
zones = list(e_uci.get('adblock', 'global', 'ns_tsdns_zones', list=True, default=['lan']))
try:
ports = list(e_uci.get_all('adblock', 'global', 'adb_nftportforce'))
except:
Expand All @@ -750,13 +767,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 @@ -43,6 +43,7 @@ define Package/ns-threat_shield/install
$(INSTALL_DIR) $(1)/usr/share/ns-plug/hooks/unregister
$(INSTALL_DIR) $(1)/usr/libexec/ns-api/post-commit
$(INSTALL_DIR) $(1)/usr/libexec/ns-api/pre-commit
$(INSTALL_DIR) $(1)/usr/libexec
$(INSTALL_BIN) ./files/ts-dns $(1)/usr/sbin/ts-dns
$(INSTALL_BIN) ./files/ts-ip $(1)/usr/sbin/ts-ip
$(INSTALL_BIN) ./files/20_threat_shield $(1)/etc/uci-defaults
Expand All @@ -56,9 +57,12 @@ 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_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/96_ns-threat_shield $(1)/etc/uci-defaults/96_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
17 changes: 17 additions & 0 deletions packages/ns-threat_shield/files/96_ns-threat_shield
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#!/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.
#
# This must run after 95-adblock-housekeeping: on systems coming from adblock 4.1.5
# the selection still lives in adb_zonelist, and it is that script which copies it
# into adb_nftdevforce

set -e

/usr/libexec/ts-dns-migrate-devices
58 changes: 58 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,58 @@
#!/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 96_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 firewall, 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

if not all(firewall.zone_exists(e_uci, zone) for zone in zones):
# the stored values are device names already, or zones that no longer exist:
# they can't be mapped back to a selection, leave the configuration untouched
return False

devices = set()
for zone in zones:
devices.update(utils.get_all_devices_by_zone(e_uci, zone, exclude_aliases=True))

# record the selection even when it yields no device, so the API keeps reporting
# the zones the user picked instead of falling back to the default one
e_uci.set('adblock', 'global', 'ns_tsdns_zones', zones)
if devices:
e_uci.set('adblock', 'global', 'adb_nftdevforce', sorted(devices))
else:
# no device in the selected zones: adblock skips the enforcement altogether
e_uci.delete('adblock', 'global', 'adb_nftdevforce')
e_uci.commit('adblock')
return True


if __name__ == "__main__":
if migrate_zones():
subprocess.run(["/etc/init.d/adblock", "restart"], capture_output=True)
38 changes: 38 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,38 @@
#!/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 = set()
for zone in zones:
devices.update(utils.get_all_devices_by_zone(uci, zone, exclude_aliases=True))
# 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