Files
op-packages/safeshield

SafeShield

Lint OpenWrt License

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.

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 Stable SafeShield Beta SafeShield
SmartSafeHub UI Stable SmartSafeHub UI Beta SmartSafeHub UI
Standalone SafeShield UI Stable LuCI SafeShield Beta LuCI SafeShield

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

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

Contributors

Support

For product information, supported devices, installation guidance, and firmware downloads, visit:

https://www.smartsafehub.com/

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