SafeShield
SafeShield is a lightweight DNS protection engine for OpenWrt and the core network-protection component used by SmartSafeHub. It blocks advertising, tracking, and phishing domains at the router through dnsmasq, so connected devices can benefit without installing a separate client application.
For most users, the recommended way to run SafeShield is through the SmartSafeHub firmware and Web UI. The standalone luci-app-safeshield package remains available for users who want a dedicated SafeShield-only LuCI interface, but current product UI development is focused on luci-app-smartsafehub.
- SmartSafeHub: https://www.smartsafehub.com/
- Firmware downloads: https://www.smartsafehub.com/firmware/
- Installation guide: https://www.smartsafehub.com/docs/installation/
Features
- DNS-based blocking of ads, tracking, and phishing domains
- Fully compatible with dnsmasq
- Lightweight design suitable for low-resource OpenWrt devices
- Integrated management through the actively developed SmartSafeHub Web UI
- Optional standalone management through
luci-app-safeshield - Automatic blocklist download and refresh
- Multiple Hub artifact sources with independent block/allow actions and checksum verification
- Support for custom allowlist and blocklist
- Modular shell-based architecture for easy customization and maintenance
- Lean runtime surface with regression coverage guarding retired/unused shell helpers from returning
Package versions
The badges below show the versions currently published to each repository channel.
| Package | Stable | Beta |
|---|---|---|
| SafeShield | ||
| SmartSafeHub UI | ||
| Standalone SafeShield UI |
luci-app-smartsafehub is the primary user-facing UI and the focus of current product development. luci-app-safeshield is retained as an optional standalone SafeShield interface and is not the primary target for new product UI work.
How it works
SafeShield provides DNS-level protection by automatically applying the appropriate dnsmasq-compatible blocklist for each device.
It helps block ads, trackers, and phishing domains with scheduled updates and optional local allow/block overrides.
Hub artifact source contract
SafeShield accepts both the legacy single-artifact response and the preferred
multi-source response from the SmartSafeHub Hub API. Existing deployments can
continue returning artifact.download_url.
For multiple independently licensed or managed datasets, the Hub should return
artifact.sources:
{
"artifact": {
"tier": "pro",
"version": "20260830T120000Z",
"sources": [
{
"id": "hagezi-pro",
"action": "block",
"download_url": "https://example.invalid/hagezi-pro.txt",
"sha256": "..."
},
{
"id": "smartsafehub-pro",
"action": "block",
"download_url": "https://example.invalid/smartsafehub-pro.txt",
"sha256": "..."
},
{
"id": "smartsafehub-allow",
"action": "allow",
"download_url": "https://example.invalid/smartsafehub-allow.txt",
"sha256": "..."
}
]
}
}
Each source is downloaded, checksum-verified, normalized, and retained as an
independent runtime cache file. SafeShield merges all block and allow sources
only on the router when generating the active dnsmasq configuration. This keeps
the Hub artifacts separate while preserving override precedence as local allow
local block>Hub allow>Hub block.
SafeShield also sends a canonical device.device_code with Hub resolve requests.
SmartSafeHub firmware is authoritative when /usr/share/smartsafehub/firmware.json
contains a valid device_code. Standalone SafeShield installations fall back only
to exact OpenWrt board-name mappings for officially recognized hardware; unknown
hardware sends an empty device_code instead of guessing from vendor, model,
architecture, or memory. device.device_code_source reports
smartsafehub_firmware, board, or unknown for diagnostics. The same values
are exposed through ubus call safeshield status.
System Requirements
SafeShield requires the following environment:
- OpenWrt 25.12 or later
- dnsmasq 2.93 or later (the SafeShield dnsmasq extension enables cumulative statistics; stock dnsmasq falls back to standard block rules with statistics disabled)
- At least 16 MB free flash storage
- Internet access for downloading blocklists
Installation
Recommended: SmartSafeHub firmware
For supported hardware, the recommended installation path is a complete SmartSafeHub OpenWrt firmware image. This gives users the integrated SmartSafeHub Web UI together with SafeShield and the firmware-specific integration expected by the product.
Download firmware without signing in:
https://www.smartsafehub.com/firmware/
The firmware page currently provides releases for supported devices including:
- ipTIME AX3000SM
- ipTIME AX3000SE
- ipTIME A3004T
- GL.iNet GL-MT300N-V2
- Xiaomi Router AX3000T (International version)
Choose the image type that matches your installation method:
- Initramfs — for temporarily booting SmartSafeHub/OpenWrt in RAM during an initial installation or recovery process.
- Factory — for installing SmartSafeHub/OpenWrt from the vendor firmware or through a device-specific recovery process.
- Sysupgrade — for upgrading a router that already runs SmartSafeHub/OpenWrt.
Not every image type is available for every device. Follow the installation guide for your router and use only the image type provided for that installation method. Always verify the exact hardware model and follow the device-specific installation guide before flashing. Installing an image for a different model can prevent the router from booting.
See the installation guide for the current procedure:
https://www.smartsafehub.com/docs/installation/
Advanced: install SafeShield packages on OpenWrt
SafeShield is also published through the SmartSafeHub OpenWrt package repository for advanced users, development environments, and compatible custom OpenWrt installations. OpenWrt 25.12 uses the apk package manager.
Add the SmartSafeHub signing key and Stable repository, replacing <architecture> with the target package architecture such as aarch64_cortex-a53, mipsel_24kc, or x86_64:
mkdir -p /etc/apk/keys /etc/apk/repositories.d
uclient-fetch -O /etc/apk/keys/smartsafehub.pem \
https://repo.smartsafehub.com/stable/packages/<architecture>/smartsafehub/smartsafehub.pem
printf '%s\n' \
'https://repo.smartsafehub.com/stable/packages/<architecture>/smartsafehub/packages.adb' \
> /etc/apk/repositories.d/smartsafehub.list
apk update
Install SafeShield with the primary SmartSafeHub UI:
apk add safeshield luci-app-smartsafehub
If you specifically want the older standalone SafeShield-only LuCI interface, it remains available as an optional package:
apk add luci-app-safeshield
The standalone UI is not the primary target for new product UI development. New dashboard, device-management, firmware-update, and broader SmartSafeHub features are developed in luci-app-smartsafehub.
After installation, restart the relevant services if they are not already restarted by the package lifecycle:
/etc/init.d/rpcd restart
/etc/init.d/uhttpd restart
/etc/init.d/safeshield restart
Verify SafeShield:
ubus call safeshield status
/etc/init.d/safeshield status
logread | grep -i safeshield
Build from source
SafeShield is built as an OpenWrt package. Build it inside an OpenWrt buildroot or SDK that matches your target device and OpenWrt version.
Build with OpenWrt buildroot
Clone OpenWrt and prepare feeds:
git clone -b production https://github.com/Junatum/openwrt.git
cd openwrt
./scripts/feeds update -a
./scripts/feeds install -a
Add SafeShield under the OpenWrt package/ directory:
git clone https://github.com/Junatum/safeshield package/safeshield
If you also want the current SmartSafeHub product UI, add
luci-app-smartsafehub:
git clone https://github.com/Junatum/luci-app-smartsafehub package/luci-app-smartsafehub
The standalone luci-app-safeshield repository is still available, but it is not the primary target for new UI development. Clone it only when you intentionally need the dedicated SafeShield-only interface.
Select your target device:
make menuconfig
For example, for MediaTek Filogic devices such as ipTIME AX3000SM:
Target System ---> MediaTek Ralink ARM
Subtarget ---> Filogic 8x0
Target Profile ---> your device profile
Enable SafeShield as a module:
cat >> .config <<'EOF'
CONFIG_PACKAGE_safeshield=m
CONFIG_PACKAGE_luci-app-smartsafehub=m
EOF
make defconfig
Build the packages:
make package/safeshield/clean V=s
make package/safeshield/compile V=s
make package/luci-app-smartsafehub/clean V=s
make package/luci-app-smartsafehub/compile V=s
Find the generated packages:
find bin -type f \( \
-name 'safeshield*.apk' \
-o -name 'luci-app-smartsafehub*.apk' \
\) -print
Depending on the OpenWrt version and package location, the output can appear under one of these paths:
bin/targets/<target>/<subtarget>/packages/
bin/packages/<architecture>/<feed-name>/
For example, a MediaTek Filogic apk build may generate packages under:
bin/targets/mediatek/filogic/packages/
Build with OpenWrt SDK
You can also build SafeShield with the OpenWrt SDK for your target. This is faster when you only need package artifacts.
tar xf openwrt-sdk-*.tar.*
cd openwrt-sdk-*
./scripts/feeds update -a
./scripts/feeds install -a
git clone https://github.com/Junatum/safeshield package/safeshield
git clone https://github.com/Junatum/luci-app-smartsafehub package/luci-app-smartsafehub
cat >> .config <<'EOF'
CONFIG_PACKAGE_safeshield=m
CONFIG_PACKAGE_luci-app-smartsafehub=m
EOF
make defconfig
make package/safeshield/compile V=s
make package/luci-app-smartsafehub/compile V=s
Install local build artifacts
For OpenWrt apk based builds, copy the generated artifacts to the
device:
find bin -type f \( -name 'safeshield-*.apk' -o -name 'luci-app-smartsafehub-*.apk' \) -print
scp path/to/safeshield-*.apk root@192.168.1.1:/tmp/
scp path/to/luci-app-smartsafehub-*.apk root@192.168.1.1:/tmp/
Install them on the device:
ssh root@192.168.1.1
apk add --allow-untrusted /tmp/safeshield-*.apk
apk add --allow-untrusted /tmp/luci-app-smartsafehub-*.apk
/etc/init.d/rpcd restart
/etc/init.d/uhttpd restart
/etc/init.d/safeshield restart
Verify the installation:
ubus call safeshield status
/etc/init.d/safeshield status
logread | grep -i safeshield
Development linting
SafeShield uses the same shell lint entrypoint locally and in GitHub Actions:
sh scripts/lint.sh
The lint script runs shfmt -d -ci, ShellCheck, and sh -n against tracked shell files. Install shfmt and shellcheck on the development machine before running it.
ShellSpec support helpers are linted by the same command; mocks consumed by dynamically sourced production functions are exported explicitly so the test harness remains ShellCheck-clean without changing runtime behavior.
Run the regression suite separately when changing runtime, statistics, HTTP transport, or rpcd behavior:
REQUIRE_UCODE=1 shellspec
The suite includes full refresh/local-rule rollback state machines, refresh lock contention, init lifecycle transitions, artifact retry/size/SHA-256 integrity checks, refreshd scheduling, OpenWrt uclient-fetch authenticated POST handling, statistics uploader recovery/state transitions, artifact resolve identity payloads, and direct rpcd runtime/status-store validation paths.
The refresh-lock regression uses a test-only portable lock shim, so the suite does not require a host flock(1) binary on macOS; production SafeShield continues to use OpenWrt's flock.
To enable the repository pre-commit hook, install pre-commit and register the hook once:
pre-commit install
After that, git commit runs the shell lint automatically. You can also check the whole repository manually:
pre-commit run --all-files
Internal rpcd ucode layout
SafeShield keeps the rpcd registration entrypoint intentionally small and loads feature modules from /usr/share/rpcd/ucode/safeshield/:
/usr/share/rpcd/ucode/safeshield.uc
/usr/share/rpcd/ucode/safeshield/
├── core.uc
├── runtime.uc
├── status.uc
├── config.uc
├── refresh.uc
├── rules.uc
└── statistics.uc
The entrypoint prepends /usr/share/rpcd/ucode/safeshield/*.uc to ucode's REQUIRE_SEARCH_PATH, so the private RPC modules stay colocated with the rpcd plugin instead of using the global /usr/share/ucode tree.
The public ubus contract remains exposed through the single safeshield object; the module split is an internal implementation detail.
SafeShield ubus API
The safeshield package owns the public management API used by LuCI and
SmartSafeHub clients. The API keeps UCI mutations, service lifecycle,
refresh scheduling and local rule files behind one ubus object.
Available methods:
safeshield.status
safeshield.config
safeshield.statistics
safeshield.config_update
safeshield.set_enabled
safeshield.refresh
safeshield.rules_list
safeshield.rule_add
safeshield.rule_delete
Read the current public configuration:
ubus call safeshield config
Read lightweight local DNS statistics:
ubus call safeshield statistics
/etc/init.d/safeshield statistics
Statistics are collected from dnsmasq cumulative SafeShield UBus counters. Raw queried domains
are never written to the statistics state. Loopback (127.0.0.0/8 and ::1)
queries are excluded so SafeShield's own DNS health checks do not inflate user
query or blocked counters. In addition to global hourly query/block counters,
SafeShield keeps per-device totals and per-device hourly buckets locally. DHCP
clients are identified by MAC address using the dnsmasq lease file, with the
current IP and hostname included for the local UI. The collector also retains a
non-MAC-derived DHCP client-id when one is available. IPv4 clients can be
resolved through the kernel ARP table, while IPv6 clients combine the kernel NDP
neighbor cache (ip -6 neigh show) with odhcpd ipv6leases DUID metadata. A
stable DHCP client-id or DHCPv6 DUID therefore moves retained history from an
older private/randomized MAC to the currently observed MAC instead of exposing
both MACs as separate devices. Identified clients include these stable identifiers
in the Hub statistics payload as identities[] entries (dhcp_client_id,
dhcpv6_duid, and mac) so the backend can preserve one canonical network
client across later MAC rotations.
For older retained data that predates those strong identifiers, SafeShield uses
a conservative hostname fallback only when exactly one active MAC advertises a
non-generic hostname and a matching historical MAC is no longer active. Generic
names such as iPhone, Android, phone, or laptop are never merged by
hostname alone. Unresolved IP-only identities remain separate internally for a
short reconciliation window, but the public statistics JSON serializes them as
one unknown device so IPv6 privacy addresses do not create a device record per
address. IP-only entries with no stable DHCP identity are compacted into that
local unknown bucket after six hours. Device statistics remain capped at 128
active retained identities to bound memory use on low-end routers.
Each retained statistics dataset has a stable generation_id. started_at is
the creation time of that retained dataset and is restored together with the
generation when the collector restarts. session_started_at is the start time
of the current collector process and therefore changes on every collector
restart. snapshot_seq is monotonically increasing within the generation and
is the ordering/idempotency key for Hub ingestion; wall-clock updated_at is
kept only as event metadata. Persistence-enabled devices reserve sequence
ranges ahead in /etc/safeshield/statistics-sequence.tsv, so a reboot can skip
unconsumed values instead of reusing a sequence already observed by Hub. This
distinction lets cloud ingestion reject duplicate or out-of-order snapshots
without treating a process restart as a new dataset.
When statistics are enabled, a separate uploader can synchronize aggregate
statistics with the SmartSafeHub Hub at /api/v1/statistics. Cloud upload is
controlled entirely by the Hub response from /api/v1/devices/sync. SafeShield
authenticates that sync with the SmartSafeHub device credential; it does not use
or require a local license key. If the Hub omits statistics credentials, network
upload is disabled while local statistics collection continues normally.
For an entitled device, the uploader stores the short-lived statistics bearer
credential only under /tmp/safeshield/statistics/; it is never written to flash
or included in status output. Routine uploads run every 30 minutes and contain a
consistent two-hour projection of the local hourly data. A full retained snapshot
is sent on initial synchronization, after a failed upload has recovered, and
periodically (approximately every 12 hours) to reconcile any missed window.
The uploader preserves the exact pending JSON across network retries. If the Hub
committed a request but the HTTP acknowledgement was lost, retrying the same
generation_id and snapshot_seq is therefore idempotent. An HTTP 401 clears
the cached statistics credential and performs device sync once before retrying.
If the refreshed response no longer grants statistics upload, SafeShield discards
the cloud pending payload and stops statistics POST requests. A denied entitlement
is rechecked at most once every 12 hours, independent of plan or prior license-key
state, so account/subscription changes can recover automatically. Local collection
remains independent of upload entitlement and upload success, so WAN or Hub
outages never interrupt on-router statistics.
The collector implementation is split into focused AWK modules under
/usr/lib/safeshield/statistics/ for common helpers, recovery, client identity,
aggregation, persistence, output, and the collector lifecycle. safeshield-statsd
loads the modules together as one AWK program, preserving the single collector
process used on resource-constrained routers.
Hot statistics live under /tmp/safeshield/statistics/ and are retained for up
to 168 hourly buckets. Supported profiles additionally persist aggregate state
with a compact base snapshot plus hourly journal, while constrained profiles
may stay tmpfs-only. The collector no longer follows dnsmasq query logs. It
polls dnsmasq's cumulative SafeShield UBus counters at the effective snapshot
interval and calculates deltas in RAM. instance_id identifies the current
dnsmasq counter epoch so a dnsmasq restart can be distinguished from a normal
counter increment.
The default statistics settings are:
statistics_enabled=1
statistics_snapshot_interval_s=60
statistics_retention_hours=168
When statistics are enabled, SafeShield calls ubus call dnsmasq safeshield_stats through a small ucode poll helper. The helper accepts only
schema version 1 snapshots with a valid instance_id, cumulative totals, and
client array; malformed or incompatible replies are treated as poll failures
instead of silently becoming zero counters. No log-queries or log-async
setting is required. Existing pre-0.3.20-r2 statistics logging configuration is
removed once during upgrade. SafeShield persists aggregate counters only; raw
DNS query names are never collected.
statistics_enabled is the configured user preference. Runtime availability is
reported separately through effective_enabled and collector_running. If the
SafeShield dnsmasq extension is unavailable, SafeShield keeps the configured
preference intact, stops only the statistics runtime, and automatically resumes
collection after a compatible dnsmasq becomes available. Disabling Statistics
or SafeShield creates a rebaseline marker so the first snapshot after re-enable
is used only as a new cumulative-counter baseline; traffic from the disabled
period is not backfilled.
When the SafeShield dnsmasq extension is available, SafeShield emits block rules
as safeshield-block=/domain/. Only these SafeShield-owned rules increment the
dnsmasq blocked counter. If the extension is unavailable, SafeShield defensively
uses standard address=/domain/# rules so DNS protection remains available
without installing an unsupported directive. The statistics source also exposes
dnsmasq's instance_id, transport_scope, client-table capacity,
tracked-client count, untracked_queries, and untracked_blocked. The OpenWrt 25.12 dnsmasq integration reports transport_scope=udp+tcp, so both UDP and TCP DNS requests are included in the cumulative SafeShield counters.
Update writable configuration values. enabled is intentionally excluded and
has a dedicated method. Device/account authentication is managed by SmartSafeHub
and is not writable through the SafeShield RPC API:
ubus call safeshield config_update '{
"values": {
"refresh_interval_s": 28800,
"refresh_on_boot": true,
"require_wan": true,
"apply_local_overrides": true,
"statistics_enabled": true,
"statistics_snapshot_interval_s": 60,
"statistics_retention_hours": 168
}
}'
Enable or disable SafeShield. This method commits the desired UCI value and requests the full SafeShield enable/disable lifecycle. Runtime convergence is asynchronous under procd, so a successful response reports the accepted target state instead of returning a potentially stale status snapshot:
ubus call safeshield set_enabled '{"enabled":true}'
A successful response has this shape:
{
"ok": true,
"changed": true,
"accepted": true,
"target_enabled": true,
"reconciled": false
}
Clients should poll safeshield.status until runtime state converges. For an
enable request, wait for enabled=true, active=true and
runtime.refreshd_running=true. For a disable request, wait for
enabled=false, active=false and runtime.refreshd_running=false.
Request an immediate refresh:
ubus call safeshield refresh
List local allow/block overrides:
ubus call safeshield rules_list
ubus call safeshield rules_list '{"action":"allow"}'
ubus call safeshield rules_list '{"action":"block"}'
Add or delete a local rule. Rule changes request an asynchronous refresh by
default so the active dnsmasq blocklist is rebuilt. For bulk edits, pass
"refresh":false on intermediate mutations and invoke safeshield.refresh
after the last mutation.
ubus call safeshield rule_add '{
"action": "block",
"domain": "example.com"
}'
ubus call safeshield rule_delete '{
"action": "block",
"domain": "example.com"
}'
Local rule fast apply
rule_add and rule_delete keep the public SafeShield API unchanged, but their automatic apply path no longer performs a full Hub artifact refresh. SafeShield retains the normalized Hub domains in /tmp/safeshield/api.block.txt, rebuilds only the local allow/block inputs, merges them atomically, restarts dnsmasq, and verifies runtime DNS. The local apply worker shares the normal refresh lock, so a rule edit made during a full refresh waits for that refresh and then reapplies the newest local state. If the cached Hub artifact is missing, SafeShield falls back to one normal full refresh.
SafeShield does not store or manage a user-entered license key. SmartSafeHub owns
the device credential and the Hub resolves the current account/subscription
entitlement during /api/v1/devices/sync. SafeShield status may expose the
resolved plan/status for presentation, but never exposes or accepts account
credentials.
The package also installs an rpcd ACL named safeshield. Read access covers
status, config and rules_list; configuration, refresh, and local-rule
mutations are declared as write access.
Contributors
Support
For product information, supported devices, installation guidance, and firmware downloads, visit:
For SafeShield source-code issues or feature requests, open an issue on GitHub:
https://github.com/Junatum/safeshield/issues
Bug reports and pull requests are welcome.
Third-party blocklist data
SafeShield may install SmartSafeHub artifacts derived from third-party DNS
blocklists, including HaGeZi's DNS Blocklists.
HaGeZi-derived artifacts remain subject to GNU GPL v3.0. See
THIRD_PARTY_NOTICES.md for the upstream project,
license, source inventory, and installed notice location.
License
SafeShield is under the GNU Public License version 3