Skip to content

Packaging

.ipk for OpenWrt ≤24.10 (opkg), .apk for 25.x+ (apk). Both are built from the same payload by scripts/ipk/build-ipk.sh and scripts/apk/build-apk.sh.

./scripts/ipk/build-ipk.sh <version> <openwrt_arch> <binary_dir> [luci_dir] [output_dir]
./scripts/ipk/build-ipk.sh 1.33.1 aarch64_generic ./binaries

luci_dir defaults to luci-app-nym-vpn/ in the repo, output_dir to the current directory. The script checks every required file is present, assembles the tree, computes installed size from du -sk, fills in the control template, and emits nym-vpn_{version}_{arch}.ipk.

What's in it

nym-vpn_1.33.1_aarch64_generic.ipk (tar.gz)
├── debian-binary          # "2.0"
├── control.tar.gz
│   ├── control            # generated from control.template
│   ├── conffiles          # /etc/config/nym-vpn
│   ├── postinst
│   └── prerm
└── data.tar.gz
    ├── usr/sbin/nym-vpnd
    ├── usr/bin/nym-vpnc
    ├── usr/libexec/rpcd/nym-vpn
    ├── www/luci-static/resources/view/nym-vpn/config.js   # the page (composer)
    ├── www/luci-static/resources/nym-vpn/                  # module tree, copied recursively
    │   ├── api.js, store.js, rpc.js, ui.js, theme.js, countries.js, assets.js
    │   ├── components/   # card, toggle, select, modal, toast, gateway-picker
    │   ├── flows/        # connect, daemon
    │   └── cards/        # connection, tunnel-settings, mixnet-tuning, dns, ...
    ├── etc/init.d/nym-vpnd
    ├── etc/config/nym-vpn
    ├── etc/uci-defaults/luci-app-nym-vpn
    ├── etc/nym/data/                       # empty, for daemon state
    ├── usr/share/luci/menu.d/luci-app-nym-vpn.json
    ├── usr/share/rpcd/acl.d/luci-app-nym-vpn.json
    ├── usr/share/nym-vpn/fw3-include.sh      # run by fw3
    ├── usr/share/nym-vpn/fw4-include.sh      # run by fw4
    ├── usr/share/nym-vpn/fw-boot-guard.sh    # sourced by the includes, init, uci-defaults, prerm; not executable
    ├── usr/share/nym-vpn/fw-rules.sh         # generated by nym-firewall build.rs; sourced by the includes
    ├── lib/upgrade/keep.d/nym-vpn          # sysupgrade keeps /etc/config/nym-vpn and /etc/nym/
    ├── etc/opkg/keys/<fingerprint>
    └── etc/apk/keys/dial0ut.pub

The .apk carries the same payload (scripts/ci/check-pkg-payload.sh compares the two) with three format-specific exceptions: the apk feed key etc/apk/keys/dial0ut-apk.pem instead of the two keys above, and lib/apk/packages/nym-vpn.conffiles plus nym-vpn.conffiles_static, apk's form of the conffiles list (see conffiles).

The opkg key is installed under its fingerprint, not its filename — build-ipk.sh derives it by base64-decoding the second line of scripts/feed/dial0ut.pub and taking bytes from the header. usign looks keys up that way.

etc/uci-defaults/luci-app-nym-vpn runs once on first boot after install. It is what registers the firewall include, so the kill-switch survives fw4 reload, and what declares the nym firewall zone (device 'nym+', masquerade, MSS clamp) and the lan -> nym forwarding that fw3/fw4 render as the tunnel plane.

Dependencies come from one list in scripts/pkg-depends.sh, which both builders format (Depends: for opkg, depends: for apk):

Package Why
libc musl libc; libc (>= 1.2) on the 32-bit architectures (arm, i386, mips, mipsel), whose binaries import musl 1.2's time64 symbols. OpenWrt's libc package carries the musl version (21.02: 1.1.24), so opkg refuses the install there instead of leaving a daemon that cannot load
libgcc libgcc_s.so.1, which the binaries link against for unwinding (OpenWrt's libgcc1 provides it)
kmod-tun TUN device for userspace WireGuard
kmod-ipt-conntrack-extra conntrack marking for inbound exemptions on fw3
luci-base LuCI web framework
rpcd RPC backend for LuCI

For apk these go in a single -I "depends:..." flag. Repeated -I depends: flags overwrite each other rather than accumulating — every .apk up to 1.30.5 shipped depending on rpcd alone, so kmod-tun was never pulled in and the daemon died at TUN device creation.

postinst

  1. TUN device — mknod /dev/net/tun c 10 200 if it does not already exist
  2. Feed registration — reads the architecture from /etc/openwrt_release, falling back to /etc/os-release since not every image ships both. Then:
    • apk: removes any stale nym-vpn lines from /etc/apk/repositories and /etc/apk/repositories.d/*.list, then writes /etc/apk/repositories.d/nym-vpn.list
    • opkg: appends a src/gz nym-vpn line to /etc/opkg/customfeeds.conf
  3. Service — enables and restarts nym-vpnd (restart, because on an upgrade the old daemon is still running; the new init script's restart path keeps the kill-switch armed). Then the one-shot migration of the retired shell watchdog (packages ≤ 1.34.x): if UCI nym-vpn.settings.always_on is 1, a detached job waits up to 30 s for the daemon socket and runs nym-vpnc tunnel set --always-on on (detached for the same reason as the rpcd refresh below: postinst must not block on the daemon); either way delete the always_on/watchdog_interval/watchdog_max_retries options, remove the old service's /etc/rc.d symlinks, kill a leftover watchdog process by its pidfile and remove the pidfile, /var/run/nym-watchdog.state and /tmp/nym-watchdog.state. The block is a no-op on a fresh install and is due to be dropped two releases after it shipped.
  4. LuCI plumbing — clears /tmp/luci-indexcache* and /tmp/luci-modulecache/, then refreshes rpcd from a detached job that waits for the opkg/apk process to exit (bounded at two minutes) before it fires. rpcd scans /usr/libexec/rpcd only at start-up and computes a session's ACL grants at login, so a fresh install restarts it. An upgrade compares the new package's exported rpcd method list (/usr/libexec/rpcd/nym-vpn list, which is nym-vpnc rpcd list behind a fixed wrapper) and ACL file against the checksums prerm stashed in /var/run/nym-vpn.rpcd-state: nothing changed → no refresh; only the method list changed → rpcd reload (SIGHUP — rpcd re-executes itself and keeps its sessions); the ACL file changed, or no stash → rpcd restart, which drops the sessions and forces the re-login that picks up the new grants. Deferred because LuCI's Software page runs opkg/apk through rpcd (file exec): a restart inside the transaction kills the request the page is waiting on (#13). Detached means a subshell with every fd on /dev/null — opkg only waits for the script's pid, but apk also reads its stdout/stderr pipes to EOF, so an inherited pipe would make apk sit out the delay.
  5. Prints both upgrade commands, opkg and apk, without picking between them

prerm

On an upgrade (PKG_UPGRADE=1) prerm only stashes md5sums of the exported rpcd method list (from the binary still installed) and the ACL file in /var/run/nym-vpn.rpcd-state for postinst's rpcd decision above. The daemon keeps running through the file swap, and the firewall, DNS and data are left for the incoming version to reuse; tearing the firewall down here would open WAN egress for the whole transaction.

On a real removal (opkg remove, apk del, and the remove half of opkg install --force-reinstall):

  1. Stop and disable nym-vpnd
  2. Firewall cleanup — the part that matters. The inet nym_boot table wherever nft exists; fw4: nft delete table inet nym (nothing of ours is inside inet fw4). fw3: run the include under the state lock with the stop marker present, which removes the jumps and the NYM_* chains; an inline teardown of the same chains covers an install whose include is gone. Skip this and an uninstall leaves the kill-switch up with nothing to take it down.
  3. Stray routing rules — flush any ip rule entries matching the daemon's fwmarks, v4 and v6. The daemon normally clears these itself on stop; a crashed one does not. Matching on fwmark rather than priority avoids touching unrelated rules.
  4. UCI and split tunnel — delete the firewall.nym_vpn include and the nym_zone / nym_lan_fwd sections (the tunnel zone and its lan -> nym forwarding), commit, remove the split-tunnel drop-in /etc/nftables.d/30-nym-split.nft, reload the firewall
  5. DNS — revert dhcp.@dnsmasq[0].resolvfile to stock, delete the split-tunnel domain sets (dhcp.nym_split_ipset), remove the managed resolv file and the adblock drop-in (/tmp/dnsmasq.d/nym-adblock.conf), and restart dnsmasq only if one of those was live. On upgrade none of this happens: the new daemon reconverges without a restart and reuses the adblock list without a download, and restarting dnsmasq here would be a LAN-wide DNS outage for no reason.
  6. Logs and feed — remove /var/log/nym-vpnd, strip nym-vpn lines from customfeeds.conf, remove /etc/apk/repositories.d/nym-vpn.list

/etc/nym is kept, and prerm says so: it holds the account (mnemonic, device keys, the ticket databases) and the daemon settings, and a removal also runs on a force-reinstall. A reinstall picks the same account up again. rm -rf /etc/nym after the removal erases it. A downgrade is an upgrade to both package managers (PKG_UPGRADE=1), so it never took the removal path.

apk hook mapping

apk does not run post-install/pre-deinstall on an upgrade; it runs pre-upgrade/post-upgrade instead, always from the incoming package. build-apk.sh registers postinst as both post-install and post-upgrade, prerm as pre-deinstall, and a copy of prerm with PKG_UPGRADE=1 exported as pre-upgrade — apk never sets that variable itself. One visible consequence: on the first upgrade to a package that stashes the rpcd checksums, apk runs the new prerm (stash present, checksums compared) while opkg runs the old one (no stash, rpcd restart).

conffiles

/etc/config/nym-vpn is marked as a config file, so an upgrade keeps your version and writes new defaults to .new alongside it. The .ipk lists it in control/conffiles; the .apk ships the same list as /lib/apk/packages/nym-vpn.conffiles and a nym-vpn.conffiles_static of <path> <sha256> lines, the files OpenWrt's buildroot generates and sysupgrade reads on apk systems.

sysupgrade

/lib/upgrade/keep.d/nym-vpn lists /etc/config/nym-vpn and /etc/nym/ (recursively, so the SQLite -wal/-shm files come along). sysupgrade backs them up and restores them into the new image, so after reinstalling the package the router has the same account and settings, and the boot guard finds the kill-switch flag in /etc/nym/nym-vpnd.json instead of reading it as off. The package itself is not part of a stock image: reinstall it after the upgrade (or build it into the image).

The shipped default:

config nym-vpn 'settings'
    option enabled '0'
    option network 'mainnet'

Nothing reads enabled or network today; they stay for compatibility. Daemon settings — Always On included — live in /etc/nym/nym-vpnd.json and are changed over gRPC, not UCI. The file also holds the split-tunnel exclusion sections the rpcd bridge parses.