Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 20 additions & 9 deletions .github/workflows/openwrt-package.yml
Original file line number Diff line number Diff line change
Expand Up @@ -249,15 +249,17 @@ jobs:
needs: [package, package-luci]
runs-on: ubuntu-24.04
container:
image: alpine:3.21
# apk index needs apk-tools 3.x to read OpenWrt 25 .apk packages;
# alpine:3.21 ships 2.14 and fails every package with IO ERROR (#296).
image: alpine:edge
if: github.event_name != 'workflow_dispatch' || inputs.version != ''
permissions:
contents: write
pages: write
steps:
- name: Install dependencies
run: |
apk add --no-cache git nodejs npm openssl abuild apk-tools curl github-cli
apk add --no-cache git nodejs npm openssl curl github-cli zstd tar bash

- uses: actions/checkout@v4

Expand Down Expand Up @@ -285,21 +287,30 @@ jobs:

- name: Set up signing key
env:
APK_SIGN_KEY: ${{ secrets.APK_SIGN_KEY }}
NETGRIP_FEED_KEY: ${{ secrets.NETGRIP_FEED_KEY }}
run: |
if [ -z "$APK_SIGN_KEY" ]; then
echo "::error::APK_SIGN_KEY secret not set. Generate a key with: abuild-keygen -n"
echo "::error::Then add the private key content as APK_SIGN_KEY in repo secrets."
if [ -z "$NETGRIP_FEED_KEY" ]; then
echo "::error::NETGRIP_FEED_KEY secret not set. Generate a key with:"
echo "::error:: usign -G -s netgrip-feed-sec -p deploy/openwrt/keys/netgrip-feed.pub"
echo "::error::Then add the secret key content as NETGRIP_FEED_KEY in repo secrets."
exit 1
fi
mkdir -p ~/.abuild
echo "$APK_SIGN_KEY" > ~/.abuild/netgrip-signing-key.rsa
chmod 600 ~/.abuild/netgrip-signing-key.rsa
echo "$NETGRIP_FEED_KEY" > ~/.abuild/netgrip-feed-sec
chmod 600 ~/.abuild/netgrip-feed-sec

- name: Sign APKs and generate index
run: |
chmod +x deploy/openwrt/sign-apk.sh
./deploy/openwrt/sign-apk.sh feed ~/.abuild/netgrip-signing-key.rsa
# The feed index must be built with the OpenWrt apk-tools: the
# Alpine one (also 3.x) speaks a different package format and
# fails on every apk with "file format not supported" (#296).
SDK_URL_DIR="https://downloads.openwrt.org/releases/25.12.5/targets/x86/64/"
SDK_NAME="$(curl -fsSL "$SDK_URL_DIR" | grep -o 'openwrt-sdk-[^"]*\.tar\.zst' | head -1)"
curl -fsSL "${SDK_URL_DIR}${SDK_NAME}" -o sdk.tar.zst
tar --zstd -xf sdk.tar.zst
export PATH="${PWD}/${SDK_NAME%.tar.zst}/staging_dir/host/bin:$PATH"
./deploy/openwrt/sign-apk.sh feed ~/.abuild/netgrip-feed-sec deploy/openwrt/keys/netgrip-feed.pub

- name: Upload feed artifact
uses: actions/upload-artifact@v4
Expand Down
5 changes: 5 additions & 0 deletions cmd/netgrip/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,14 @@ var version = "dev"

func main() {
listen := flag.String("listen", "0.0.0.0", "listen address")
showVersion := flag.Bool("version", false, "print version and exit")
port := flag.Int("port", 8090, "listen port")
rpcdURL := flag.String("rpcd-url", auth.DefaultRPCdURL, "rpcd JSON-RPC endpoint used for login validation")
flag.Parse()
if *showVersion {
fmt.Println(version)
return
}

// The flag always has a value (its default), so only treat it as an
// explicit override when it differs from the default endpoint.
Expand Down
2 changes: 2 additions & 0 deletions deploy/openwrt/keys/netgrip-feed.pub
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
untrusted comment: netgrip apk feed
RWT74wNSfNVjV5+i6KacA0hd9nHVN17YDGuZ0nH/Jx5rgjadiww/WyQv
3 changes: 2 additions & 1 deletion deploy/openwrt/netgrip/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ define Package/netgrip/install
$(INSTALL_BIN) ./files/etc/init.d/netgrip $(1)/etc/init.d/netgrip
$(INSTALL_DIR) $(1)/usr/libexec
$(INSTALL_BIN) ./files/usr/libexec/netgrip-restore-rules $(1)/usr/libexec/netgrip-restore-rules
$(INSTALL_BIN) ./files/usr/libexec/netgrip-heal-register $(1)/usr/libexec/netgrip-heal-register
endef

define Package/netgrip/postinst
Expand Down Expand Up @@ -75,7 +76,7 @@ if [ -z "$${IPKG_INSTROOT}" ]; then
# Survive sysupgrades: the apk registry does not survive, but the
# preserved files do, so procd starts the panel on first boot.
# /etc/netgrip/ keeps the netpulse embedded-agent env across upgrades.
for f in /usr/sbin/netgrip /etc/init.d/netgrip /etc/rc.d/S99netgrip /usr/libexec/netgrip-restore-rules /etc/netgrip/; do
for f in /usr/sbin/netgrip /etc/init.d/netgrip /etc/rc.d/S99netgrip /usr/libexec/netgrip-restore-rules /usr/libexec/netgrip-heal-register /etc/netgrip/; do
grep -qxF "$$f" /etc/sysupgrade.conf 2>/dev/null || echo "$$f" >> /etc/sysupgrade.conf
done
fi
Expand Down
4 changes: 4 additions & 0 deletions deploy/openwrt/netgrip/files/netgrip.init
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,8 @@ start() {
start_service
# Reapply MAC ACL and storm control rules after boot
[ -x /usr/libexec/netgrip-restore-rules ] && /usr/libexec/netgrip-restore-rules &
# Restore the package registry entry after attended sysupgrades (#296):
# owut/ASU drops packages outside official feeds from the image, files
# survive via /etc/sysupgrade.conf but the apk/opkg registry does not.
[ -x /usr/libexec/netgrip-heal-register ] && /usr/libexec/netgrip-heal-register &
}
114 changes: 114 additions & 0 deletions deploy/openwrt/netgrip/files/usr/libexec/netgrip-heal-register
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
#!/bin/sh
# netgrip-heal-register: restore the package registry entry after an
# attended sysupgrade (#296).
#
# owut/ASU only builds images with packages from official feeds, so an
# attended upgrade drops netgrip from the image. The files survive the
# flash via /etc/sysupgrade.conf (postinst adds them) and procd starts the
# panel on first boot, but the apk/opkg registry entry is gone: the package
# stops being "installed" and package managers never upgrade it again.
#
# This script runs in the background from the init script and reinstalls
# the running version from the latest GitHub release:
# - apk (OpenWrt 25): the matching netgrip-<ver>-r1-<arch>.apk asset,
# installed with --allow-untrusted (custom feeds would need a
# PGP-signed packages.adb, which the CI cannot produce yet)
# - opkg (OpenWrt 24): the matching netgrip_<ver>-1_<arch>.ipk asset
# Dev builds newer than the latest release are left alone. Failures are
# logged and retried on the next boot. Never blocks the panel.

PKG=netgrip
BIN=/usr/sbin/netgrip
RELEASES_API=https://api.github.com/repos/gnacho/netgrip/releases/latest

log() { logger -t netgrip-heal "$*"; }

lock=/var/run/netgrip-heal.pid
if [ -f "$lock" ] && kill -0 "$(cat "$lock" 2>/dev/null)" 2>/dev/null; then
exit 0
fi
echo $$ > "$lock"
trap 'rm -f "$lock"' EXIT INT TERM

registered() {
if command -v apk >/dev/null 2>&1; then
apk info "$PKG" >/dev/null 2>&1
else
opkg list-installed "$PKG" 2>/dev/null | grep -q .
fi
}

if registered; then
exit 0
fi

ver=$("$BIN" -version 2>/dev/null)
log "registry entry lost (running ${ver:-unknown}): restoring"

# Resolve the release asset for this flavor and architecture.
if command -v apk >/dev/null 2>&1; then
arch=$(apk --print-arch 2>/dev/null)
case "$arch" in
aarch64*) asset=netgrip-*-arm64.apk ;;
x86_64*) asset=netgrip-*-amd64.apk ;;
*) log "no release asset for apk arch $arch"; exit 1 ;;
esac
pkgfile=/tmp/netgrip-heal.apk
else
arch=$(opkg print-architecture 2>/dev/null | awk 'tolower($1)=="arch"{print $2; exit}')
[ -n "$arch" ] || arch=$(opkg status 2>/dev/null | sed -n 's/^Architecture: *//p' | head -n1)
[ -n "$arch" ] || { log "cannot determine architecture"; exit 1; }
asset="netgrip*_${arch}.ipk"
pkgfile=/tmp/netgrip-heal.ipk
fi

# Wait for WAN, up to ~3 minutes.
i=0
while [ "$i" -lt 36 ]; do
if wget -q -O /dev/null -T 5 https://api.github.com 2>/dev/null; then
break
fi
sleep 5
i=$((i + 1))
done

json=$(wget -q -O - -T 15 "$RELEASES_API" 2>/dev/null)
[ -n "$json" ] || { log "cannot reach GitHub releases API"; exit 1; }

url=$(printf '%s' "$json" | tr ',' '\n' | grep -o '"browser_download_url": *"[^"]*"' | cut -d'"' -f4 | grep -E "/$(printf '%s' "$asset" | sed 's/\*/[^/]*/g')$" | head -n1)
[ -n "$url" ] || { log "no release asset matching $asset"; exit 1; }

# Never downgrade a dev build: only heal when the asset matches the
# running version.
if [ -n "$ver" ]; then
case "$url" in
*"netgrip-${ver}-"*|*"netgrip_${ver}-"*) ;;
*)
log "latest release does not match running $ver: skipping (reinstall manually)"
exit 0
;;
esac
fi

if ! wget -q -O "$pkgfile" -T 120 "$url"; then
rm -f "$pkgfile"
log "download failed: $url"
exit 1
fi

if command -v apk >/dev/null 2>&1; then
if apk add --allow-untrusted "$pkgfile" >/dev/null 2>&1; then
rm -f "$pkgfile"
log "restored from $url"
exit 0
fi
else
if opkg install "$pkgfile" >/dev/null 2>&1; then
rm -f "$pkgfile"
log "restored from $url"
exit 0
fi
fi
rm -f "$pkgfile"
log "install failed"
exit 1
4 changes: 3 additions & 1 deletion deploy/openwrt/package.sh
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ cp "$REPO_ROOT/deploy/openwrt/netgrip/files/netgrip.init" "$PKG_DIR/etc/init.d/n
chmod 755 "$PKG_DIR/etc/init.d/netgrip"
cp "$REPO_ROOT/deploy/openwrt/netgrip/files/netgrip-restore-rules" "$PKG_DIR/usr/libexec/netgrip-restore-rules"
chmod 755 "$PKG_DIR/usr/libexec/netgrip-restore-rules"
cp "$REPO_ROOT/deploy/openwrt/netgrip/files/usr/libexec/netgrip-heal-register" "$PKG_DIR/usr/libexec/netgrip-heal-register"
chmod 755 "$PKG_DIR/usr/libexec/netgrip-heal-register"

# CONTROL files
cat > "$PKG_DIR/CONTROL/control" << CTRL
Expand Down Expand Up @@ -125,7 +127,7 @@ fi
# Survive sysupgrades: the apk registry does not survive, but the
# preserved files do, so procd starts the panel on first boot.
# /etc/netgrip/ keeps the netpulse embedded-agent env across upgrades.
for f in /usr/sbin/netgrip /etc/init.d/netgrip /etc/rc.d/S99netgrip /usr/libexec/netgrip-restore-rules /etc/netgrip/; do
for f in /usr/sbin/netgrip /etc/init.d/netgrip /etc/rc.d/S99netgrip /usr/libexec/netgrip-restore-rules /usr/libexec/netgrip-heal-register /etc/netgrip/; do
grep -qxF "$f" /etc/sysupgrade.conf 2>/dev/null || echo "$f" >> /etc/sysupgrade.conf
done
exit 0
Expand Down
62 changes: 41 additions & 21 deletions deploy/openwrt/sign-apk.sh
Original file line number Diff line number Diff line change
@@ -1,46 +1,66 @@
#!/bin/sh
# sign-apk.sh - Generates and signs a signed APKINDEX for OpenWrt .apk feeds.
# sign-apk.sh - Builds the OpenWrt 25 .apk feed index and signs it.
#
# Usage:
# ./deploy/openwrt/sign-apk.sh [output-dir] [private-key]
# sign-apk.sh [output-dir] [usign-secret-key] [public-key]
#
# Requires: apk-tools (3.x), abuild-sign
# Requires: apk-tools 3 built for OpenWrt (the SDK host build; the Alpine
# apk-tools speaks a different package format) and usign, both under
# staging_dir/host/bin of an extracted OpenWrt SDK.
#
# For apk v3 (OpenWrt 25.12+) packages are pre-built by the SDK and do not need
# per-package repacking/signature. The feed only needs a signed APKINDEX.tar.gz.
# This script:
# 1. Generates APKINDEX.tar.gz from all *.apk in output-dir
# 2. Signs APKINDEX.tar.gz with the private key
# 3. Extracts the matching public key into output-dir
# Produces in output-dir:
# packages.adb - repository index (v3), the file repositories.d
# entries must point at
# packages.adb.asc - usign detached signature of the index
# <public-key copy> - the matching public key, for /etc/apk/keys
#
# The release apks are signed by the SDK build keys, which this run does
# not trust; the index is built with --allow-untrusted and the trust lives
# in the usign signature of packages.adb.
#
# NOTE (#296): OpenWrt 25.12 routers currently only verify indexes signed
# with the official OpenWrt build-system PGP key, so third-party adb feeds
# are not verifiable on-device yet. The structure published here is the
# correct feed layout; routers can still install the apks directly with
# `apk add --allow-untrusted`, which is what netgrip-heal-register does.

set -eu

out_dir="${1:-feed}"
privkey="${2:-~/.abuild/netgrip-signing-key.rsa}"
privkey="${2:?usage: sign-apk.sh <output-dir> <usign-secret-key> <public-key>}"
pubkey="${3:?usage: sign-apk.sh <output-dir> <usign-secret-key> <public-key>}"

if [ ! -d "$out_dir" ]; then
echo "Error: output directory not found: $out_dir" >&2
exit 1
fi

if [ ! -f "$privkey" ]; then
echo "Error: Private key not found: $privkey" >&2
echo "Error: usign secret key not found: $privkey" >&2
exit 1
fi

if [ ! -f "$pubkey" ]; then
echo "Error: public key not found: $pubkey" >&2
exit 1
fi

# Resolve inputs to absolute paths before changing directory.
out_dir="$(cd "$(dirname "$out_dir")" && pwd)/$(basename "$out_dir")"
pubkey="$(cd "$(dirname "$pubkey")" && pwd)/$(basename "$pubkey")"

cd "$out_dir"

echo "==> Generating APKINDEX"
apk index -o APKINDEX.tar.gz --description "NetGrip $(date +%Y-%m-%d)" *.apk || {
echo "Warning: apk index failed, continuing without index" >&2
exit 0
}
echo "==> Generating packages.adb"
# Fail closed: a feed without a signed index is a broken feed. This used
# to warn and exit 0, which published apk files nobody could install (#296).
apk mkndx --allow-untrusted -o packages.adb *.apk

echo "==> Signing APKINDEX"
abuild-sign -k "$privkey" APKINDEX.tar.gz
echo "==> Signing packages.adb"
usign -S -s "$privkey" -m packages.adb -x packages.adb.asc

echo "==> Extracting public key"
openssl rsa -in "$privkey" -pubout -out netgrip.rsa.pub 2>/dev/null
echo "==> Publishing public key"
cp "$pubkey" .

echo "==> Done"
echo "Signed index: $out_dir/APKINDEX.tar.gz"
echo "Index: $out_dir/packages.adb (signed: packages.adb.asc)"
Binary file added netgrip
Binary file not shown.
Loading