Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

nuci

Declarative OpenWrt UCI configuration compiler & SSH deployer.

Nix (writeUci) ──► uci.json ──► nuci compile ──► UCI Batch
                                  │
                                  ├─► nuci diff   (read-only)
                                  └─► nuci deploy ──► SSH ──► Router

CLI Quick Reference

nuci compile ./uci.json --no-sops                    # Pure UCI compilation
nuci diff    ./uci.json --target root@192.168.1.1    # Read-only diff
nuci deploy  ./uci.json --target root@192.168.1.1    # Deploy with 60s watchdog
nix run .#example -- "root@192.168.1.1" --force      # Flake one-shot deploy

Core Modules

  • Architecture — 4-domain code layout, UCI idempotency, watchdog + boot hook, async reload.
  • Nix Options — Complete option specification generated from nix/module-options.nix.
  • Examples — Copy-paste Nix snippets.

Architecture

Project Layout

src/
  config/   models.rs, uci_key.rs, validation.rs
  compile/  generator.rs, pipeline.rs, secrets.rs
  target/   deploy.rs, diff.rs
  utils/    error.rs, helpers.rs

Single compilation seam: compile::pipeline::compile_config.

UCI Idempotency Strategy

  • Named Section (network.lan): Wiped via uci delete, rebuilt via uci set. Removed Nix options are deleted on target.
  • Anonymous List (wireless.@wifi-iface[0]): Wiped via while uci -q delete config.@type[0]; do :; done, re-added sequentially.
  • Array Diff: Joined via \u{1f} (Unit Separator) control character, making element reordering diff-neutral.
  • Libuci Protection: Automatically emits touch /etc/config/<cfg> before uci batch to avoid silent file creation failures.

Anti-Brick Rollback System

LayerTriggerAction
Layer A (In-Session)Network loss / SSH timeoutBackground watchdog (trap '' HUP; sleep 60). Killed on SSH handshake success.
Layer B (Boot-Time)Power loss / Reboot mid-deployInit script S15nuci_rollback restores /etc/config from /etc/.uci-rollback-backup on boot, then self-deletes.

Async Detached Reload

Avoids SSH exit status 255 (TCP Reset on interface/sshd restart) by running reloads in a background subshell:

( sleep 1; <reload_commands> ) >/dev/null 2>&1 &

The script exits 0 immediately, closing SSH cleanly before services restart.

Target Execution Requirements

  • Text Files: POSIX cat > path <<'NUCI_FILE_{i}_EOF' (quoted delimiter prevents shell expansion, zero base64 dependency).
  • Binary Files: Base64 decoded via echo '<b64>' | base64 -d > path.
  • Root Password: chpasswd <<'CHPWD' or passwd fallback (POSIX heredoc).
  • Local Packages: Streamed via pure Rust tar::Builder into target /tmp/ without host disk files.

Nix Module Options Specification

Verified 1:1 against nix/module-options.nix and nix/default.nix.

Options Reference

OptionTypeDefaultDescription / Output Mapping
uci.packageManagerenum [ "opkg" "apk" ]"opkg"Package backend (opkg ≤ 23.05, apk 24.10+).
uci.settings(pkgs.formats.json {}).type{}UCI configuration attrset (config → section → option).
uci.secrets.sops.fileslistOf path[]SOPS encrypted files decrypted in-memory at compile time.
uci.packageslistOf str[]Packages to install. Prefix with - to remove (-pkg).
uci.packageSources.feedslistOf str[]Repository lines (/etc/opkg/customfeeds.conf or /etc/apk/repositories.d/customfeeds.list).
uci.packageSources.localPackageslistOf (either str path)[]Local .ipk/.apk paths, streamed via in-memory tar to /tmp/.
uci.sshKeyslistOf str[]Public keys deployed to /etc/dropbear/authorized_keys (0600).
uci.watchdogTimeoutint60Rollback watchdog timeout in seconds.
uci.rawUcilistOf str[]Raw UCI lines (must start with "uci "). Auto-touches missing /etc/config/<file>.
uci.fileslistOf (submodule)[]Custom file payloads (spec below).

uci.files.* Submodule Options

OptionTypeDefaultDescription
pathstr(required)Absolute destination path on target.
contentnullOr strnullText content (written via POSIX cat heredoc). Empty string creates a zero-byte file.
base64nullOr strnullBase64 binary payload (decoded via base64 -d). Mutually exclusive with content.
checksumnullOr strnullExpected SHA256 hex. Skips write if target hash matches.
executableboolfalseFile mode (true0755, false0644).

Nix Library Functions (nix/default.nix)

writeUci configuration

Evaluates Nix configuration module and returns:

  • json: Derivation generating uci.json.
  • command: Wrapper script executing nuci compile or nuci deploy (forwards $@ flags).

buildFirmware { configuration, profile, release ? null }

Combines nuci with nix-openwrt-imagebuilder:

  • Bakes compiled UCI directives into /etc/uci-defaults/99-nuci-bootstrap.
  • Inherits settings, packages, packageSources, sshKeys, files, and rawUci.

Configuration Examples

Basic Network & Firewall

{
  uci.settings = {
    system.system = [ { _type = "system"; hostname = "rauter"; } ];
    network = {
      lan = { _type = "interface"; device = "br-lan"; proto = "dhcp"; };
      wan = { _type = "interface"; proto = "pppoe"; username = "@wan_user@"; password = "@wan_pass@"; };
    };
    firewall.guest = { _type = "zone"; name = "guest"; network = [ "guest" ]; input = "REJECT"; output = "ACCEPT"; };
  };
  uci.watchdogTimeout = 120;
}

Wireless & SOPS Secrets

{
  uci.settings.wireless = {
    radio0 = { _type = "wifi-device"; type = "mac80211"; channel = "auto"; band = "2g"; };
    default_radio0 = {
      _type = "wifi-iface"; device = "radio0"; network = "lan";
      mode = "ap"; ssid = "home-2.4"; encryption = "sae-mixed"; key = "@wifi_password@";
    };
  };
  uci.secrets.sops.files = [ ./secrets.yml ];
}

Package Management & Feeds

{
  uci.packageManager = "opkg"; # or "apk"
  uci.packages = [ "-tcpdump" "htop" ];
  uci.packageSources = {
    feeds = [ "src/gz custom https://dl.openwrt.org/packages" ];
    localPackages = [ "./packages/luci-app-custom_1.0_all.ipk" ];
  };
}

Custom Files

{
  uci.files = [
    # Text File (POSIX cat heredoc)
    { path = "/etc/config.txt"; content = "key=value\n"; }
    # Binary File (Base64 + SHA256 checksum guard)
    {
      path = "/usr/bin/blob";
      base64 = "aGVsbG8=";
      executable = true;
      checksum = "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824";
    }
  ];
}

Escape Hatch (rawUci)

{
  uci.rawUci = [
    "uci rename network.lan=lan0"
    "uci reorder wireless.@wifi-iface[0]=1"
  ];
}

Day-1 Firmware Build

exampleFirmware = uci.buildFirmware {
  configuration = ./example.nix;
  profile = "linksys_e8450-ubi";
};