Skip to content

OpenWrt router installation

Running the NetTact agent on an OpenWrt router puts the probes at the real edge of the network. A router sees the whole uplink, every wireless client and the actual NAT behaviour — none of which an agent on some desktop inside the LAN can observe.

There are two packages:

PackageContentsArchitecture
nettact-agentprocd service, UCI config, download scriptall
luci-app-nettactLuCI settings and status pagesall

Why the package ships no executable

A full agent is about 11 MB. Bundling it would mean one package per CPU architecture, and it would not fit at all on the 8 and 16 MB routers that benefit most from having it.

So the package contains scripts only, and the agent binary for this device's architecture is downloaded at first start. You choose where it lives:

  • RAM mode (default) — downloaded to /tmp on every boot. Uses no flash at all, at the cost of ~11 MB of RAM and one download per reboot.
  • Flash mode — downloaded once to /usr/lib/nettact. Boots and runs with no internet, needs about 12 MB free on the overlay.

In both modes the agent's identity always lives on flash, in /etc/nettact/data (agent.key and agent.json, under 1 KB together). A reboot therefore brings the router back as the same agent, and it never has to enroll again with a one-time token.

Switching from flash back to RAM deletes the flash copy at the next service start or download. Freeing the overlay is the whole reason for that switch, so leaving 11 MB behind would defeat it.

Install

The console's Agent page generates this command for you, with your server address, a fresh enrollment token and the permission policy already filled in — Agents → Enrollment → OpenWrt:

sh
wget -O /tmp/nettact-openwrt.sh https://d.nettact.org/agent/openwrt.sh && sh /tmp/nettact-openwrt.sh \
  --server-url 'https://nettact.example.com' \
  --token '<one-time enrollment token>'

Why not pipe it straight into sh

A pipeline reports the status of its last command, and a shell handed an empty standard input exits successfully — so wget … | sh prints nothing and looks like a clean install when the download failed. Downloading first makes it a precondition.

It installs both packages, writes the settings into /etc/config/nettact, starts the service and then waits until the router reports itself connected. If that does not happen within the timeout, the install fails and prints the agent's own connection status and recent log rather than reporting a success nobody verified. The service is left enabled either way. Most failures keep retrying on their own — an unreachable server, and also a rejected token, since a site that was at its agent limit can be fixed at the other end and the same token then works. Only two are terminal: no token at all, and a credential the router could not write to disk.

OptionMeaning
--token-file <path>Read the one-time token from a file instead of the command line.
--permissions <list>Comma-separated permission policy, or none. REPLACES the built-in default grant. The console generates a ready-made value.
--mode ram|flashWhere the agent binary lives (default ram), as described above.
--version <tag>Pin the agent binary to a release tag instead of latest.
--auto-updateCheck daily for a newer binary and install it. Cannot be combined with a pinned --version. Written on every run, so re-running without it turns automatic updates back off. See Automatic updates.
--download-base <url>Where the agent binary is fetched from; point it at a local mirror.
--ipk-base <url|dir>Where the two packages come from. A local directory works, which is how an unreleased build is tested.
--tls-insecureAccept a server certificate that does not verify.
--no-luciInstall the agent package without the LuCI pages.
--reinstallEnroll again with the token given here instead of the credential this router already holds — what the console's Reinstall produces. The old credential and queue are discarded, and only after the configuration has been written, so a bad option or a failed UCI write leaves the router exactly as it was.
--wait <seconds>How long to wait for the router to come online (default 180; 0 skips the check).

Re-running it is safe. The agent's identity in /etc/nettact/data is never touched, so an already-enrolled router keeps its credential — and does not even need a token. Every run also refreshes the binary to the configured version before starting the service, which is what makes re-running it the upgrade path.

Manual install

sh
opkg update
opkg install ca-bundle
opkg install https://d.nettact.org/agent/nettact-agent.ipk
opkg install https://d.nettact.org/agent/luci-app-nettact.ipk

HTTPS support

For opkg to fetch over HTTPS the image needs libustream-mbedtls (or the openssl/wolfssl variant) and ca-bundle. Official images include them; if opkg install reports an SSL error, install those first.

After installation the service is stopped and not enabled — installing a package should never make a router start reporting to a server nobody has configured yet. Configure it as below.

Configure

In LuCI, open Services → NetTact, fill in the server URL and enrollment token, choose a storage mode, and enable it.

The status page then answers the question a router owner actually has — not "is the process running" but "is it reporting". Under Server connections each configured server gets its connection state (with a live countdown to the next attempt when it is retrying), the reason it is not connected in plain words, how many entries are waiting to upload, and when it was last connected.

Or edit /etc/config/nettact directly:

sh
uci set nettact.main.server_url='https://nettact.example.com'
uci set nettact.main.enroll_token='<one-time enrollment token>'
uci set nettact.main.mode='ram'
uci set nettact.main.enabled='1'
uci commit nettact
/etc/init.d/nettact enable
/etc/init.d/nettact start

UCI options

config nettact 'main' — the router-wide settings:

OptionDefaultMeaning
enabled0Master switch. The service starts only when this is 1 and a server is configured.
moderamram or flash, as above. Anything unrecognised is treated as ram.
server_modesinglesingle uses the four options below; multi ignores them and uses the config server sections instead.
server_urlemptyThe NetTact server, e.g. https://nettact.example.com.
enroll_tokenemptyOne-time enrollment token. Used only until this router is enrolled; the agent clears it automatically after any successful registration.
enroll_token_fileemptyA file holding the token instead. Mutually exclusive with enroll_token.
tls_insecure0Accept a server certificate that does not verify. Only for a private CA or an IP-address server you control.
upload_interval30sHow often buffered telemetry is uploaded.
wire_formatprotobufprotobuf or json.
persist_enable1Keep an unsent backlog on flash across a reboot; written only while a server connection is down. Set 0 for a memory-only buffer. See what the router build leaves out.
persist_window30mHow long after a disconnect the backlog keeps being written to flash, [1m, 24h].
permission_modedefaultdefault (the agent's built-in grant; recommended is an alias), host_metrics, full, none, or custom — see below. An unrecognised value stops the service rather than falling back to the default.
permissionsList of permission ids, used when permission_mode is custom. It replaces the default grant rather than adding to it, and a permission whose parent is missing is a startup error.
probe_access_modeunsetallowlist or denylist. Unset keeps the default: scope:lan and scope:public allowed, scope:loopback, scope:link-local and scope:metadata denied.
probe_allowlistSelector list: scope:<loopback|lan|link-local|public|metadata|any>, cidr:<prefix>, ip:<address>, host:<name>.
probe_denylistSame syntax. Deny always wins over allow.
min_probe_interval1sFloor on how often one monitor runs. Range 200ms10m.
max_probe_concurrency16Range 1–256.
snapshot_min_interval3sRange 1s10m.
snapshot_timeout10sRange 1s60s.
max_trace_concurrency4Range 1–64.
download_basehttps://d.nettact.org/agentDownload source; point at a local mirror if you prefer.
versionlatestlatest, or a pinned tag such as v1.2.3.
auto_update0Check daily and install a newer agent binary, restarting the service only when the binary actually changed. Ignored while version is pinned. See Automatic updates.

The permission presets match the ones the console offers when you enroll an agent: default is the built-in set (standard probes plus basic network state), host_metrics adds CPU, memory, disk, load, uptime, throughput and temperature, and full grants everything including process and connection snapshots. The full id list is in the agent configuration reference.

Reporting to several servers

Set server_mode to multi and add one repeatable config server section per server. Each is fully independent — its own credential, probe assignments, outages and permissions:

sh
uci set nettact.main.server_mode='multi'

uci add nettact server
uci set nettact.@server[-1].name='home'
uci set nettact.@server[-1].url='https://nettact.example.com'
uci set nettact.@server[-1].enroll_token='<one-time token>'

uci add nettact server
uci set nettact.@server[-1].name='work'
uci set nettact.@server[-1].url='https://nettact.corp.example'
uci set nettact.@server[-1].enroll_token='<one-time token>'
uci set nettact.@server[-1].permission_mode='custom'
uci add_list nettact.@server[-1].permissions='probe.icmp'
uci add_list nettact.@server[-1].permissions='probe.dns'

uci commit nettact
/etc/init.d/nettact restart

A server section takes name, url, enroll_token, enroll_token_file, tls_insecure, permission_mode + permissions, and probe_access_mode + probe_allowlist + probe_denylist. In multi mode the 401 self-heal still re-registers each server with a fresh token, but the automatic token clear is single-server only — a multi-server deployment keeps each section's enroll_token until you remove it by hand.

The name is an identity, not a label

name keys the saved credential and the queued backlog. Renaming an entry makes the agent enroll again and discards whatever it had queued for that server. It cannot be derived from the URL, which you may legitimately edit. Lowercase letters, digits, - and _, up to 64 characters, unique within the file.

A per-entry permission_mode replaces the router-wide grant for that server only. A per-entry probe_access_mode can only narrow the router-wide one — a target must pass both.

Where the configuration actually goes

The init script renders /etc/config/nettact into /var/etc/nettact/agent.yaml and points the agent at it. That path is on tmpfs, so the enrollment token never comes to rest on flash a second time and changing a setting spends no overlay erase cycles. The file is rewritten from UCI at every service start — editing it directly is pointless.

Three settings still travel as environment variables rather than through that file: NETTACT_AGENT_CONFIG_FILE (which config to read), NETTACT_AGENT_DATA_DIR (so a hand-written config that omits data_dir still keeps the identity on flash), and NETTACT_AGENT_STATUS_FILE (the tmpfs path the LuCI status page reads). A hand-written config that sets status_file itself wins over the last of these — a file value always beats the environment.

If you need something UCI does not model, write /etc/nettact/agent.yaml by hand — the whole schema is in the agent configuration reference. When that file exists the init script uses it verbatim and generates nothing, so the two can never disagree about which config is live. The LuCI status page says which one is in effect.

Supported architectures

The download script reads opkg print-architecture (falling back to OPENWRT_ARCH in /etc/os-release) and maps it to a build:

OpenWrt architectureBuild downloadedTypical devices
x86_64amd64x86 soft routers, mini PCs
i386_*386 (softfloat)older x86
aarch64_*arm64Raspberry Pi 4/5, NanoPi, most recent ARM routers
arm_cortex-a5/7/8/9/15/17/53/72*armv7most 32-bit ARM routers
arm_arm1176*, arm_mpcore*armv6Raspberry Pi 1, older ARM11 devices
arm_arm926*, arm_fa526*, arm_xscale*armv5early ARM devices
mipsel_*mipsle-softfloatMT7621 / MT7620 / MT76x8 — most consumer routers
mips_*mips-softfloatath79 and other big-endian MIPS
riscv64_*riscv64D1, JH7110 boards

MIPS ships softfloat only: the MT7621 family has no FPU, and a softfloat build also runs correctly on the few chips that do — so there is no variant here that can be picked wrongly.

ARM and x86 names outside the table fall back: any other arm_* is treated as ARMv7 (every ARM target OpenWrt has added in the last decade is at least that, and the older cores that exist are each listed above), and any other x86* as 386. MIPS and RISC-V are matched by prefix alone, with no per-model fallback. Architecture families with no build at all — PowerPC, LoongArch — fail with a clear message rather than guessing at something that would not run.

If a new ARM model falls back to ARMv7 and does not run, send us the output of opkg print-architecture and we will add it to the table.

What the router build leaves out

Router builds (their asset names contain -lite-) differ in two ways:

  • No WireGuard egress for probes. Userspace WireGuard and the gVisor network stack it dials through are the single largest part of the binary. A monitor pinned to a WireGuard proxy reports a configuration error and does not fall back to a direct dial — that would silently measure a different path. SOCKS5 and HTTP CONNECT proxies still work.
  • The telemetry buffer touches flash only during an outage. The desktop and server builds spill their buffer to disk unconditionally; the router build keeps it in memory while the connection is healthy — flash erase cycles are not spent on data whose whole purpose is to be uploaded immediately. When a server connection drops, the backlog is written to flash for the first 30 minutes after the disconnect (UCI persist_window; a restart mid-outage counts as a fresh disconnect), so rebooting the router mid-outage — the usual reflex — no longer erases the data that shows how the fault began. A crash while connected still loses whatever was buffered at that moment. Set option persist_enable '0' to return to memory-only. Identity is unaffected — it is always on flash.

Everything else is identical: ICMP, DNS, HTTP, TCP, NAT behaviour, traceroute, interface and Wi-Fi state.

Updating and maintenance

Update the agent binary — re-run the one-command installer. Every run refreshes the binary to the configured version before starting the service, so re-running it is the upgrade path:

sh
wget -O /tmp/nettact-openwrt.sh https://d.nettact.org/agent/openwrt.sh && sh /tmp/nettact-openwrt.sh \
  --server-url 'https://your-server' --wait 180

An already-enrolled router keeps its credential, so no token is needed. Or, without the installer — press "Download / update binary" on the LuCI status page, or:

sh
/usr/lib/nettact/fetch.sh install
/etc/init.d/nettact restart

A restart does check, but only lightly: on start the service compares the installed binary against the configured release and installs the newer one before starting — at most once every six hours, and deliberately non-fatal, so an unreachable download source starts what is already there (and never retries inside the window, which is what keeps a no-uplink boot normal and a respawn loop from hammering the mirror). In RAM mode a reboot is still a clean update (/tmp is empty again, and with version set to latest the fresh download resolves to the newest release); in flash mode a router that has been up since before a release keeps running the old build until its next boot or the one-shot reinstall. That matters because an agent too old for its server fails to enroll and respawns every 10s — but now the status page and the log say why the moment it does. To have it keep up on its own, see automatic updates below.

Automatic updates

Off by default. Once on (Automatic updates under LuCI Services → NetTact → Binary, or uci set nettact.main.auto_update='1' followed by /etc/init.d/nettact restart), the package writes a once-daily check into root's crontab:

37 3 * * * /usr/lib/nettact/update.sh # nettact-auto-update
  • The time of day is derived from this device's MAC and always lands between 02:00 and 05:00 — it does not move on every reboot, and it does not point a fleet of routers at the download source simultaneously. This is the same fleet-spreading approach as the Server's host update timer.
  • The service is restarted only when the downloaded binary is genuinely different from the current one; identical content does nothing, so no connection is dropped for nothing.
  • Skipped when version pins a specific release — a pin means stay on that version. The installer refuses --auto-update together with --version <tag> for the same reason.
  • Does nothing while the service is manually stopped, so it will not bring an agent you just stopped back up in the middle of the night.
  • Turning it off (or disabling the service, or removing the package) removes the cron entry.

To enable it at install time, add --auto-update to openwrt.sh (ticking "Automatic updates" on the console's enrollment page generates it). That switch is written on every run of the installer: re-running without the option turns it off, matching the other platforms.

Update the packages — run opkg install against the same .ipk URLs again.

sysupgrade — the package ships /lib/upgrade/keep.d/nettact, so /etc/config/nettact and /etc/nettact/data are carried across automatically and the router does not re-enroll.

Uninstall:

sh
opkg remove luci-app-nettact nettact-agent
rm -rf /etc/nettact        # also removes the agent's identity

opkg remove stops the service and deletes the downloaded binary and the rendered /var/etc/nettact/agent.yaml, but keeps /etc/nettact, so reinstalling does not mean enrolling again. Remove that directory by hand for a clean wipe.

Troubleshooting

sh
logread -e nettact              # service log
cat /tmp/nettact/status.json    # per-server connection state, as JSON
/etc/init.d/nettact status      # is it running
cat /var/etc/nettact/agent.yaml # the configuration UCI actually produced
/usr/lib/nettact/fetch.sh arch  # architecture this device resolved to
ls -l /etc/nettact/data/        # agent.json present means enrollment succeeded

Common cases:

  • The service exits immediately after starting. Check that enabled is 1 and a server is configured. Without both, the init script logs why and exits cleanly.
  • A LuCI change had no effect. Check the status page's "Configuration" row: if it says hand-written, /etc/nettact/agent.yaml exists and is being used instead of these settings. Delete or rename it to go back to UCI.
  • "Server connections" says no status yet, and stays that way. The agent writes /tmp/nettact/status.json and the page reads it, so the section is empty for a few seconds after every start. If it never fills in, a hand-written /etc/nettact/agent.yaml has set its own status_file: — a file value beats the environment, so the package's tmpfs path is ignored. Remove that key to get the panel back; the log still has everything either way.
  • It waits and never starts. The launcher waits up to five minutes each for a plausible clock and a default route. A router with no RTC boots in 1970, which makes every server certificate "not yet valid" — confirm sysntpd is running.
  • The download fails. Confirm ca-bundle is installed and download_base is reachable; try it by hand with uclient-fetch -O- <url>.
  • "no NetTact agent build for architecture". Include the output of opkg print-architecture in the issue.
  • A permission was rejected at startup. Permissions are not auto-completed: granting probe.http.extended without probe.http, or host.process.owner.read without host.process.basic.read, is an error rather than a warning. The LuCI chooser fills parents in for you; a hand-edited /etc/config/nettact does not.

Using a local mirror

To keep routers off the public internet, point download_base at your own mirror. It must serve the same layout:

<base>/versions.json
<base>/<tag>/nettact-agent-lite-linux-<arch>
<base>/<tag>/SHA256SUMS

versions.json needs at least a latest field:

json
{"latest":"v1.2.3","versions":[{"tag":"v1.2.3","prerelease":false}]}

The download script resolves latest to a concrete tag first, then takes both the binary and SHA256SUMS from that one immutable directory, and verifies the SHA256 before installing anything.

The single source of truth for configuration is each binary’s --help output