mirror of
https://github.com/jmcorgan/fips.git
synced 2026-07-30 19:46:15 +00:00
Merge branch 'master' into next
This commit is contained in:
@@ -24,6 +24,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
package_version: ${{ steps.version.outputs.package_version }}
|
||||
apk_version: ${{ steps.version.outputs.apk_version }}
|
||||
release_channel: ${{ steps.channel.outputs.release_channel }}
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
@@ -35,13 +36,20 @@ jobs:
|
||||
shell: bash
|
||||
run: |
|
||||
: ${GITHUB_OUTPUT:=/tmp/github_output}
|
||||
# package_version is the human-readable label used in artifact
|
||||
# filenames; apk_version is the apk-tools-compatible string embedded
|
||||
# in the .apk metadata. apk_version is built directly from the same
|
||||
# structured inputs (tag, or commit height) — no reparse of the
|
||||
# flattened package_version. See packaging/openwrt-apk/apk-version.sh.
|
||||
if [[ "$GITHUB_REF" == refs/tags/* ]]; then
|
||||
echo "package_version=${GITHUB_REF_NAME}" >> "$GITHUB_OUTPUT"
|
||||
echo "apk_version=$(sh packaging/openwrt-apk/apk-version.sh tag "${GITHUB_REF_NAME}")" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
BRANCH=$(echo "$GITHUB_REF_NAME" | sed 's|/|-|g')
|
||||
HEIGHT=$(git rev-list --count HEAD)
|
||||
HASH=$(git rev-parse --short HEAD)
|
||||
echo "package_version=${BRANCH}.${HEIGHT}.${HASH}" >> "$GITHUB_OUTPUT"
|
||||
echo "apk_version=$(sh packaging/openwrt-apk/apk-version.sh dev "${HEIGHT}")" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
- name: Determine release channel
|
||||
@@ -64,8 +72,8 @@ jobs:
|
||||
echo "release_channel=dev" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
build:
|
||||
name: Build .ipk (${{ matrix.openwrt_arch }})
|
||||
compile-binaries:
|
||||
name: Cross-compile (${{ matrix.openwrt_arch }})
|
||||
runs-on: ubuntu-latest
|
||||
needs: determine-versioning
|
||||
|
||||
@@ -100,14 +108,6 @@ jobs:
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set SOURCE_DATE_EPOCH from git
|
||||
run: echo "SOURCE_DATE_EPOCH=$(git log -1 --format=%ct)" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Initialize
|
||||
run: |
|
||||
PACKAGE_FILENAME=${{ env.PACKAGE_NAME }}_${{ needs.determine-versioning.outputs.package_version }}_${{ matrix.openwrt_arch }}.ipk
|
||||
echo "PACKAGE_FILENAME=$PACKAGE_FILENAME" >> $GITHUB_ENV
|
||||
|
||||
- name: Install Rust toolchain (stable)
|
||||
if: matrix.rust_channel == 'stable'
|
||||
uses: actions-rust-lang/setup-rust-toolchain@v1
|
||||
@@ -153,6 +153,64 @@ jobs:
|
||||
- name: Install llvm-strip
|
||||
run: sudo apt-get update && sudo apt-get install -y --no-install-recommends llvm
|
||||
|
||||
# Cross-compile + strip once; both the .ipk and .apk packagers consume
|
||||
# these artifacts via --bin-dir, so the Rust build runs a single time
|
||||
# per architecture instead of once per package format.
|
||||
- name: Cross-compile and strip binaries
|
||||
run: |
|
||||
set -euo pipefail
|
||||
cargo zigbuild --release --target ${{ matrix.rust_target }} \
|
||||
--bin fips --bin fipsctl --bin fipstop --bin fips-gateway
|
||||
RELEASE_DIR="target/${{ matrix.rust_target }}/release"
|
||||
mkdir -p out
|
||||
for b in fips fipsctl fipstop fips-gateway; do
|
||||
llvm-strip "$RELEASE_DIR/$b" 2>/dev/null || true
|
||||
cp "$RELEASE_DIR/$b" "out/$b"
|
||||
done
|
||||
ls -lh out/
|
||||
|
||||
- name: Upload binaries artifact
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: fips-bins-${{ matrix.openwrt_arch }}
|
||||
path: out/
|
||||
retention-days: 1
|
||||
|
||||
build:
|
||||
name: Build .ipk (${{ matrix.openwrt_arch }})
|
||||
runs-on: ubuntu-latest
|
||||
needs: [determine-versioning, compile-binaries]
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
# Must be a subset of compile-binaries' arches (this job consumes those
|
||||
# binary artifacts). Currently both ship aarch64 + x86_64.
|
||||
include:
|
||||
- build_arch: aarch64
|
||||
openwrt_arch: aarch64_cortex-a53
|
||||
- build_arch: x86_64
|
||||
openwrt_arch: x86_64
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set SOURCE_DATE_EPOCH from git
|
||||
run: echo "SOURCE_DATE_EPOCH=$(git log -1 --format=%ct)" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Initialize
|
||||
run: |
|
||||
PACKAGE_FILENAME=${{ env.PACKAGE_NAME }}_${{ needs.determine-versioning.outputs.package_version }}_${{ matrix.openwrt_arch }}.ipk
|
||||
echo "PACKAGE_FILENAME=$PACKAGE_FILENAME" >> $GITHUB_ENV
|
||||
|
||||
- name: Download prebuilt binaries
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: fips-bins-${{ matrix.openwrt_arch }}
|
||||
path: bins
|
||||
|
||||
- name: Install nak
|
||||
shell: bash
|
||||
run: |
|
||||
@@ -201,8 +259,7 @@ jobs:
|
||||
- name: Build .ipk
|
||||
env:
|
||||
PKG_VERSION: ${{ needs.determine-versioning.outputs.package_version }}
|
||||
LLVM_STRIP: llvm-strip
|
||||
run: ./packaging/openwrt-ipk/build-ipk.sh --arch ${{ matrix.build_arch }}
|
||||
run: ./packaging/openwrt-ipk/build-ipk.sh --arch ${{ matrix.build_arch }} --bin-dir "$GITHUB_WORKSPACE/bins"
|
||||
|
||||
- name: Install shellcheck (if missing)
|
||||
shell: bash
|
||||
@@ -389,7 +446,7 @@ jobs:
|
||||
- name: SHA-256 hashes
|
||||
run: |
|
||||
echo "==> Binaries:"
|
||||
sha256sum target/${{ matrix.rust_target }}/release/fips target/${{ matrix.rust_target }}/release/fipsctl target/${{ matrix.rust_target }}/release/fipstop
|
||||
sha256sum bins/fips bins/fipsctl bins/fipstop
|
||||
echo "==> Package:"
|
||||
sha256sum dist/${{ env.PACKAGE_FILENAME }}
|
||||
|
||||
@@ -452,6 +509,7 @@ jobs:
|
||||
--tag A="${{ matrix.openwrt_arch }}" \
|
||||
--tag v="$VERSION" \
|
||||
--tag n="${{ env.PACKAGE_NAME }}" \
|
||||
--tag format="ipk" \
|
||||
--tag compression="none" \
|
||||
> event.json 2> event.err
|
||||
|
||||
@@ -509,25 +567,327 @@ jobs:
|
||||
echo " Release EventId: ${{ steps.publish.outputs.eventId }}"
|
||||
echo " Blossom URL: ${{ steps.blossom_upload.outputs.url }}"
|
||||
|
||||
build-apk:
|
||||
name: Build .apk (${{ matrix.openwrt_arch }})
|
||||
runs-on: ubuntu-latest
|
||||
needs: [determine-versioning, compile-binaries]
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
# Must be a subset of compile-binaries' arches.
|
||||
include:
|
||||
- build_arch: aarch64
|
||||
openwrt_arch: aarch64_cortex-a53
|
||||
# MT3000, MT6000, Flint 2, RPi 3/4/5 on OpenWrt 25+
|
||||
- build_arch: x86_64
|
||||
openwrt_arch: x86_64
|
||||
# x86 routers / VMs on OpenWrt 25+
|
||||
|
||||
env:
|
||||
# apk-tools commit OpenWrt pins for the .apk (ADB) format. Keep in sync
|
||||
# with package/system/apk/Makefile in the targeted OpenWrt release so the
|
||||
# packages we produce are readable by the apk on the device.
|
||||
APK_TOOLS_VERSION: "3.0.5"
|
||||
APK_TOOLS_COMMIT: "b5a31c0d865342ad80be10d68f1bb3d3ad9b0866"
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set SOURCE_DATE_EPOCH from git
|
||||
run: echo "SOURCE_DATE_EPOCH=$(git log -1 --format=%ct)" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Initialize
|
||||
run: |
|
||||
PACKAGE_FILENAME=${{ env.PACKAGE_NAME }}_${{ needs.determine-versioning.outputs.package_version }}_${{ matrix.openwrt_arch }}.apk
|
||||
echo "PACKAGE_FILENAME=$PACKAGE_FILENAME" >> $GITHUB_ENV
|
||||
|
||||
- name: Download prebuilt binaries
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: fips-bins-${{ matrix.openwrt_arch }}
|
||||
path: bins
|
||||
|
||||
- name: Install fakeroot
|
||||
run: sudo apt-get update && sudo apt-get install -y --no-install-recommends fakeroot
|
||||
|
||||
# apk mkpkg lives in apk-tools v3, which is not packaged for Ubuntu, so we
|
||||
# build the pinned release from source. This is the SDK-free equivalent of
|
||||
# how the .ipk path uses plain tar — one small C tool, no OpenWrt SDK.
|
||||
- name: Build apk-tools (${{ env.APK_TOOLS_VERSION }}) from source
|
||||
run: |
|
||||
set -euo pipefail
|
||||
sudo apt-get install -y --no-install-recommends \
|
||||
git ca-certificates build-essential meson ninja-build pkg-config \
|
||||
zlib1g-dev libssl-dev libzstd-dev liblzma-dev lua5.4-dev scdoc
|
||||
git clone --quiet https://gitlab.alpinelinux.org/alpine/apk-tools.git /tmp/apk-tools
|
||||
cd /tmp/apk-tools
|
||||
git checkout --quiet "${APK_TOOLS_COMMIT}"
|
||||
meson setup build
|
||||
ninja -C build src/apk
|
||||
APK_BIN=/tmp/apk-tools/build/src/apk
|
||||
"$APK_BIN" --version 2>/dev/null || "$APK_BIN" version 2>/dev/null || true
|
||||
echo "APK_BIN=$APK_BIN" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Build .apk
|
||||
env:
|
||||
PKG_VERSION: ${{ needs.determine-versioning.outputs.package_version }}
|
||||
APK_VERSION: ${{ needs.determine-versioning.outputs.apk_version }}
|
||||
run: ./packaging/openwrt-apk/build-apk.sh --arch ${{ matrix.build_arch }} --bin-dir "$GITHUB_WORKSPACE/bins"
|
||||
|
||||
- name: Verify apk structural integrity
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
APK="dist/${{ env.PACKAGE_FILENAME }}"
|
||||
if [ ! -s "$APK" ]; then
|
||||
echo "FAIL: produced apk not found or empty at $APK"
|
||||
exit 1
|
||||
fi
|
||||
echo "==> file type:"; file "$APK"
|
||||
|
||||
# apk v3 packages are ADB containers; dump the whole manifest with the
|
||||
# apk-tools we just built. Print it in full so the exact schema is
|
||||
# always visible in the log if an assertion needs adjusting.
|
||||
DUMP=$(mktemp)
|
||||
"$APK_BIN" adbdump "$APK" > "$DUMP" 2>/dev/null || {
|
||||
echo "FAIL: 'apk adbdump' could not read $APK"; exit 1; }
|
||||
echo "==> full adbdump:"; cat "$DUMP"
|
||||
|
||||
fail=0
|
||||
# Package metadata (flat keys under info:).
|
||||
for needle in "name: fips" "version: ${{ needs.determine-versioning.outputs.apk_version }}" "arch: ${{ matrix.openwrt_arch }}"; do
|
||||
if grep -qF "$needle" "$DUMP"; then
|
||||
echo " PASS meta: $needle"
|
||||
else
|
||||
echo " FAIL meta: missing '$needle'"; fail=1
|
||||
fi
|
||||
done
|
||||
|
||||
# installed-size reflects the bundled binaries (4 stripped Rust
|
||||
# binaries, several MB). A payload regression that drops them shows up
|
||||
# here regardless of how the path tree is formatted.
|
||||
SIZE=$(awk '/^[[:space:]]*installed-size:/ {print $2; exit}' "$DUMP")
|
||||
echo " installed-size: ${SIZE:-unknown}"
|
||||
if [ -z "${SIZE:-}" ] || [ "$SIZE" -lt 1000000 ]; then
|
||||
echo " FAIL: installed-size implausibly small (binaries missing?)"; fail=1
|
||||
else
|
||||
echo " PASS: installed-size >= 1MB"
|
||||
fi
|
||||
|
||||
# The adbdump paths: block is hierarchical. Each directory is a
|
||||
# top-level list item "- name: <full relative dir>"; its files are
|
||||
# "- name: <basename>" nested one indent level deeper under "files:".
|
||||
# (There are no "path:" keys.) Reconstruct full file paths by keying
|
||||
# off the indentation of the directory-level list items.
|
||||
RECON=$(awk '
|
||||
/^paths:/ {p=1; diri=-1; next}
|
||||
p && /^[^ #-]/ {p=0} # a new top-level key ends paths:
|
||||
!p {next}
|
||||
match($0, /^ *- /) {
|
||||
ind=RLENGTH; rest=substr($0, RLENGTH+1)
|
||||
if (diri==-1) diri=ind # first list item = directory indent
|
||||
if (ind==diri) { # directory entry (or the root acl: entry)
|
||||
if (rest ~ /^name: /) { dir=rest; sub(/^name: /,"",dir) } else dir=""
|
||||
next
|
||||
}
|
||||
if (rest ~ /^name: /) { # deeper item = a file under files:
|
||||
f=rest; sub(/^name: /,"",f); print (dir==""?f:dir"/"f)
|
||||
}
|
||||
}
|
||||
' "$DUMP")
|
||||
echo "==> reconstructed paths:"; printf '%s\n' "$RECON"
|
||||
|
||||
for path in \
|
||||
usr/bin/fips usr/bin/fipsctl usr/bin/fipstop usr/bin/fips-gateway \
|
||||
etc/init.d/fips etc/init.d/fips-gateway \
|
||||
etc/fips/fips.yaml etc/fips/firewall.sh etc/dnsmasq.d/fips.conf \
|
||||
etc/sysctl.d/fips-gateway.conf etc/sysctl.d/fips-bridge.conf \
|
||||
etc/hotplug.d/net/99-fips etc/uci-defaults/90-fips-setup \
|
||||
lib/upgrade/keep.d/fips; do
|
||||
if printf '%s\n' "$RECON" | grep -qxF "$path"; then
|
||||
echo " PASS path: $path"
|
||||
else
|
||||
echo " FAIL path: missing $path"; fail=1
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$fail" -ne 0 ]; then
|
||||
echo "apk structural verification FAILED"
|
||||
exit 1
|
||||
fi
|
||||
echo "apk structural verification PASS"
|
||||
|
||||
- name: SHA-256 hashes
|
||||
run: |
|
||||
echo "==> Binaries:"
|
||||
sha256sum bins/fips bins/fipsctl bins/fipstop
|
||||
echo "==> Package:"
|
||||
sha256sum dist/${{ env.PACKAGE_FILENAME }}
|
||||
|
||||
- name: Upload artifact (GitHub only)
|
||||
if: ${{ env.ACT != 'true' }}
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: ${{ env.PACKAGE_FILENAME }}
|
||||
path: dist/${{ env.PACKAGE_FILENAME }}
|
||||
retention-days: 30
|
||||
|
||||
- name: Install nak
|
||||
shell: bash
|
||||
run: |
|
||||
NAK_VERSION="0.16.2"
|
||||
ARCH=$(uname -m)
|
||||
case "$ARCH" in
|
||||
x86_64|amd64) NAK_ARCH="amd64" ;;
|
||||
aarch64|arm64) NAK_ARCH="arm64" ;;
|
||||
*) echo "Unsupported architecture: $ARCH"; exit 1 ;;
|
||||
esac
|
||||
curl -fsSL "https://github.com/fiatjaf/nak/releases/download/v${NAK_VERSION}/nak-v${NAK_VERSION}-linux-${NAK_ARCH}" \
|
||||
-o /usr/local/bin/nak
|
||||
chmod +x /usr/local/bin/nak
|
||||
nak --version
|
||||
|
||||
- name: Install jq
|
||||
run: |
|
||||
if ! command -v jq &>/dev/null; then
|
||||
sudo apt-get update && sudo apt-get install -y jq
|
||||
fi
|
||||
|
||||
# Priority: HIVE_CI_NSEC from env (loom job) > repo secret > generate ephemeral
|
||||
- name: Resolve signing key
|
||||
id: keys
|
||||
shell: bash
|
||||
env:
|
||||
SECRET_NSEC: ${{ secrets.HIVE_CI_NSEC }}
|
||||
run: |
|
||||
: ${GITHUB_OUTPUT:=/tmp/github_output}
|
||||
if [ -n "${HIVE_CI_NSEC:-}" ]; then
|
||||
echo "Using HIVE_CI_NSEC from loom job environment"
|
||||
NSEC="$HIVE_CI_NSEC"
|
||||
elif [ -n "$SECRET_NSEC" ]; then
|
||||
echo "Using HIVE_CI_NSEC from repository secrets"
|
||||
NSEC="$SECRET_NSEC"
|
||||
else
|
||||
echo "No nsec provided -- generating ephemeral keypair"
|
||||
NSEC=$(nak key generate)
|
||||
fi
|
||||
|
||||
PUBKEY=$(echo "$NSEC" | nak key public)
|
||||
echo "::add-mask::$NSEC"
|
||||
echo "nsec=$NSEC" >> "$GITHUB_OUTPUT"
|
||||
echo "pubkey=$PUBKEY" >> "$GITHUB_OUTPUT"
|
||||
echo "Publisher pubkey (hex): $PUBKEY"
|
||||
|
||||
- name: Upload to Blossom
|
||||
id: blossom_upload
|
||||
shell: bash
|
||||
env:
|
||||
BLOSSOM_SERVER: "https://blossom.primal.net"
|
||||
NSEC: ${{ steps.keys.outputs.nsec }}
|
||||
run: |
|
||||
: ${GITHUB_OUTPUT:=/tmp/github_output}
|
||||
|
||||
UPLOAD_RESPONSE=$(nak blossom upload \
|
||||
--server "$BLOSSOM_SERVER" \
|
||||
--sec "$NSEC" \
|
||||
"dist/${{ env.PACKAGE_FILENAME }}" < /dev/null)
|
||||
|
||||
echo "Upload response:"
|
||||
echo "$UPLOAD_RESPONSE"
|
||||
|
||||
FILE_HASH=$(echo "$UPLOAD_RESPONSE" | jq -r '.sha256')
|
||||
if [ -z "$FILE_HASH" ] || [ "$FILE_HASH" = "null" ]; then
|
||||
echo "Failed to extract hash from upload response"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
BLOSSOM_URL="${BLOSSOM_SERVER}/${FILE_HASH}"
|
||||
echo "url=$BLOSSOM_URL" >> "$GITHUB_OUTPUT"
|
||||
echo "hash=$FILE_HASH" >> "$GITHUB_OUTPUT"
|
||||
echo "Uploaded to Blossom: $BLOSSOM_URL"
|
||||
|
||||
- name: Publish NIP-94 release event
|
||||
id: publish
|
||||
shell: bash
|
||||
env:
|
||||
RELAYS: "wss://relay.damus.io wss://nos.lol wss://nostr.mom wss://offchain.pub"
|
||||
NSEC: ${{ steps.keys.outputs.nsec }}
|
||||
run: |
|
||||
: ${GITHUB_OUTPUT:=/tmp/github_output}
|
||||
set -e
|
||||
|
||||
VERSION="${{ needs.determine-versioning.outputs.package_version }}"
|
||||
CHANNEL="${{ needs.determine-versioning.outputs.release_channel }}"
|
||||
|
||||
nak event --sec "$NSEC" -k 1063 \
|
||||
-c "FIPS Package: ${{ env.PACKAGE_NAME }} for ${{ matrix.openwrt_arch }} (apk)" \
|
||||
--tag url="${{ steps.blossom_upload.outputs.url }}" \
|
||||
--tag m="application/octet-stream" \
|
||||
--tag x="${{ steps.blossom_upload.outputs.hash }}" \
|
||||
--tag ox="${{ steps.blossom_upload.outputs.hash }}" \
|
||||
--tag filename="${{ env.PACKAGE_FILENAME }}" \
|
||||
--tag A="${{ matrix.openwrt_arch }}" \
|
||||
--tag v="$VERSION" \
|
||||
--tag n="${{ env.PACKAGE_NAME }}" \
|
||||
--tag format="apk" \
|
||||
--tag compression="none" \
|
||||
> event.json 2> event.err
|
||||
|
||||
if [ ! -s event.json ]; then
|
||||
echo "Failed to create event"
|
||||
cat event.err 2>/dev/null || true
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "=== Event JSON ==="
|
||||
cat event.json
|
||||
echo "=================="
|
||||
|
||||
EVENT_ID=$(jq -r '.id' event.json)
|
||||
if [ -z "$EVENT_ID" ] || [ "$EVENT_ID" = "null" ]; then
|
||||
echo "Failed to extract event ID"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Publish to relays
|
||||
cat event.json | nak event $RELAYS 2>&1
|
||||
|
||||
echo "eventId=$EVENT_ID" >> "$GITHUB_OUTPUT"
|
||||
echo "Published NIP-94 event: $EVENT_ID"
|
||||
|
||||
- name: Build Summary
|
||||
run: |
|
||||
echo "Build Summary for ${{ matrix.openwrt_arch }} (apk):"
|
||||
echo " Package: ${{ env.PACKAGE_FILENAME }}"
|
||||
echo " apk version: ${{ needs.determine-versioning.outputs.apk_version }}"
|
||||
echo " Release EventId: ${{ steps.publish.outputs.eventId }}"
|
||||
echo " Blossom URL: ${{ steps.blossom_upload.outputs.url }}"
|
||||
|
||||
release:
|
||||
name: Publish GitHub Release (github only)
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
needs: [build, build-apk]
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- name: Download all .ipk artifacts
|
||||
- name: Download package artifacts
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
# Only the .ipk/.apk packages (named fips_<ver>_<arch>.*), not the
|
||||
# fips-bins-* raw-binary artifacts shared between the build jobs.
|
||||
pattern: fips_*
|
||||
path: dist
|
||||
merge-multiple: true
|
||||
|
||||
- name: Generate OpenWrt release checksums
|
||||
run: |
|
||||
cd dist
|
||||
find . -maxdepth 1 -type f -name '*.ipk' -printf '%P\n' \
|
||||
find . -maxdepth 1 -type f \( -name '*.ipk' -o -name '*.apk' \) -printf '%P\n' \
|
||||
| LC_ALL=C sort \
|
||||
| xargs sha256sum \
|
||||
> checksums-openwrt.txt
|
||||
@@ -537,5 +897,6 @@ jobs:
|
||||
with:
|
||||
files: |
|
||||
dist/*.ipk
|
||||
dist/*.apk
|
||||
dist/checksums-openwrt.txt
|
||||
generate_release_notes: true
|
||||
|
||||
@@ -101,6 +101,16 @@ with v0.3.x peers.
|
||||
|
||||
### Added
|
||||
|
||||
- OpenWrt `.apk` packaging (`packaging/openwrt-apk/`, `make apk`) for
|
||||
OpenWrt 25+, where apk-tools is the mandatory package manager (the
|
||||
existing `.ipk` continues to cover OpenWrt 24.x and earlier). Built
|
||||
SDK-free: it reuses the `.ipk` cross-compile (`cargo-zigbuild`) and the
|
||||
shared installed-filesystem payload, and assembles the package with
|
||||
`apk mkpkg` from apk-tools 3.0.5 built from source — no OpenWrt SDK
|
||||
image. A `build-apk` CI job (aarch64, x86_64) builds and structurally
|
||||
verifies the package; releases now publish `.apk` artifacts and
|
||||
checksums alongside `.ipk`. Packages are unsigned, installed with
|
||||
`apk add --allow-untrusted`, matching the `.ipk` posture.
|
||||
- Nym mixnet transport (`transports.nym`) for outbound peer links
|
||||
tunneled through a local `nym-socks5-client` SOCKS5 proxy into the
|
||||
Nym mixnet, as a privacy transport alongside Tor. Outbound-only and
|
||||
|
||||
+6
-2
@@ -6,7 +6,8 @@
|
||||
# Usage:
|
||||
# make deb Build a Debian/Ubuntu .deb package
|
||||
# make tarball Build a systemd install tarball
|
||||
# make ipk Build an OpenWrt .ipk package
|
||||
# make ipk Build an OpenWrt .ipk package (opkg, OpenWrt 24.x and earlier)
|
||||
# make apk Build an OpenWrt .apk package (apk-tools, mandatory on OpenWrt 25+)
|
||||
# make aur Build fips-git AUR package and validate with namcap
|
||||
# make pkg Build a macOS .pkg installer
|
||||
# make zip Build a Windows .zip package
|
||||
@@ -17,7 +18,7 @@ SHELL := /bin/bash
|
||||
PACKAGING_DIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
|
||||
PROJECT_ROOT := $(abspath $(PACKAGING_DIR)/..)
|
||||
|
||||
.PHONY: all deb tarball ipk aur pkg zip clean
|
||||
.PHONY: all deb tarball ipk apk aur pkg zip clean
|
||||
|
||||
all: deb tarball
|
||||
|
||||
@@ -30,6 +31,9 @@ tarball:
|
||||
ipk:
|
||||
@bash $(PACKAGING_DIR)/openwrt-ipk/build-ipk.sh
|
||||
|
||||
apk:
|
||||
@bash $(PACKAGING_DIR)/openwrt-apk/build-apk.sh
|
||||
|
||||
aur:
|
||||
@bash $(PACKAGING_DIR)/aur/build-aur.sh
|
||||
|
||||
|
||||
+28
-5
@@ -8,7 +8,8 @@ All build outputs go to `deploy/` at the project root.
|
||||
```sh
|
||||
make deb # Debian/Ubuntu .deb
|
||||
make tarball # systemd install tarball
|
||||
make ipk # OpenWrt .ipk
|
||||
make ipk # OpenWrt .ipk (opkg, OpenWrt 24.x and earlier)
|
||||
make apk # OpenWrt .apk (apk-tools, mandatory on OpenWrt 25+)
|
||||
make aur # Arch Linux AUR package (fips-git, local build + namcap)
|
||||
make pkg # macOS .pkg installer
|
||||
make zip # Windows .zip package
|
||||
@@ -46,7 +47,8 @@ packaging/
|
||||
debian/ Debian/Ubuntu .deb packaging via cargo-deb
|
||||
macos/ macOS .pkg installer via pkgbuild
|
||||
systemd/ Generic Linux systemd tarball packaging
|
||||
openwrt/ OpenWrt .ipk packaging via cargo-zigbuild
|
||||
openwrt-ipk/ OpenWrt .ipk packaging via cargo-zigbuild (opkg)
|
||||
openwrt-apk/ OpenWrt .apk packaging via cargo-zigbuild + apk mkpkg
|
||||
windows/ Windows .zip package with service scripts
|
||||
```
|
||||
|
||||
@@ -100,7 +102,7 @@ sudo ./fips-<version>-linux-<arch>/install.sh
|
||||
See [systemd/README.install.md](systemd/README.install.md) for full
|
||||
installation and configuration instructions.
|
||||
|
||||
### OpenWrt (`.ipk`)
|
||||
### OpenWrt (`.ipk`, opkg — OpenWrt 24.x and earlier)
|
||||
|
||||
Cross-compiled with cargo-zigbuild and assembled as a standard `.ipk`
|
||||
archive. Supports aarch64, mipsel, mips, arm, and x86\_64 targets.
|
||||
@@ -110,12 +112,33 @@ archive. Supports aarch64, mipsel, mips, arm, and x86\_64 targets.
|
||||
make ipk
|
||||
|
||||
# Build for a specific architecture
|
||||
bash packaging/openwrt/build-ipk.sh --arch mipsel
|
||||
bash packaging/openwrt-ipk/build-ipk.sh --arch mipsel
|
||||
```
|
||||
|
||||
See [openwrt/README.md](openwrt/README.md) for router-specific
|
||||
See [openwrt-ipk/README.md](openwrt-ipk/README.md) for router-specific
|
||||
installation instructions.
|
||||
|
||||
### OpenWrt (`.apk`, apk-tools — mandatory on OpenWrt 25+)
|
||||
|
||||
OpenWrt 25 makes apk-tools the mandatory package manager (it is opt-in on
|
||||
24.10). Same SDK-free approach
|
||||
(cargo-zigbuild), but the `.apk` container is assembled by `apk mkpkg`
|
||||
rather than hand-rolled, so the build additionally needs an apk-tools v3
|
||||
`apk` binary built from source. The installed-filesystem payload is shared
|
||||
with the `.ipk` package.
|
||||
|
||||
```sh
|
||||
# Build (default: aarch64; also x86_64)
|
||||
make apk
|
||||
|
||||
# Build for a specific architecture
|
||||
bash packaging/openwrt-apk/build-apk.sh --arch x86_64
|
||||
```
|
||||
|
||||
Packages are unsigned; install with `apk add --allow-untrusted`. See
|
||||
[openwrt-apk/README.md](openwrt-apk/README.md) for building apk-tools and
|
||||
router-specific installation.
|
||||
|
||||
### macOS (`.pkg`)
|
||||
|
||||
Built with `pkgbuild` (included with Xcode command-line tools). Installs
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
# FIPS OpenWrt Package (apk)
|
||||
|
||||
Builds a FIPS `.apk` for **OpenWrt 25+**, where apk-tools is the mandatory
|
||||
package manager. apk is also available opt-in on **24.10** (where opkg remains
|
||||
the default). For OpenWrt 24.x and earlier, the `.ipk` package in
|
||||
[`../openwrt-ipk/`](../openwrt-ipk/) still works.
|
||||
|
||||
Like the `.ipk` build, this is **SDK-free**: it cross-compiles with
|
||||
`cargo-zigbuild` and assembles the package directly — no OpenWrt SDK image. The
|
||||
`.ipk` format is a plain tar.gz we can hand-roll, but the `.apk` (apk-tools v3
|
||||
ADB) container is not, so we drive the official `apk mkpkg` applet — the same
|
||||
tool OpenWrt's own [`include/package-pack.mk`](https://github.com/openwrt/openwrt/blob/main/include/package-pack.mk)
|
||||
calls. The only extra requirement over the `.ipk` build is the `apk` binary.
|
||||
|
||||
## Layout
|
||||
|
||||
| File | Purpose |
|
||||
|---|---|
|
||||
| `build-apk.sh` | Cross-compile + assemble the `.apk` via `apk mkpkg` |
|
||||
| `apk-version.sh` | Map a release tag / commit height to an apk-tools-valid version |
|
||||
| `apk-version.test.sh` | Case-table test for `apk-version.sh` (`sh apk-version.test.sh`) |
|
||||
|
||||
The installed-filesystem payload (init scripts, `fips.yaml`, sysctl drop-ins,
|
||||
hotplug, uci-defaults, …) is **shared** with the `.ipk` package — there is one
|
||||
canonical copy in [`../openwrt-ipk/files/`](../openwrt-ipk/files/). `build-apk.sh`
|
||||
stages from there, so the two packages always ship the same files. Keep the
|
||||
staging block in `build-apk.sh` in sync with `../openwrt-ipk/build-ipk.sh`.
|
||||
|
||||
## Versioning
|
||||
|
||||
apk-tools enforces a strict version grammar
|
||||
(`<digit>(.<digit>)*(_<suffix><digit>*)*(-r<N>)`). `apk-version.sh` builds a
|
||||
valid version from structured inputs rather than rewriting an already-flattened
|
||||
string:
|
||||
|
||||
| Input | apk version |
|
||||
|---|---|
|
||||
| `tag v1.2.3` | `1.2.3-r0` |
|
||||
| `tag v1.2.3-rc1` | `1.2.3_rc1-r0` |
|
||||
| `dev 1234` (commit height) | `0.0.0_git1234-r0` |
|
||||
|
||||
The human-readable version (`v1.2.3`, `master.123.abcdef0`) is still used for the
|
||||
artifact filename; only the metadata embedded in the package is normalized.
|
||||
|
||||
## Building
|
||||
|
||||
### Prerequisites
|
||||
|
||||
| Requirement | Notes |
|
||||
|---|---|
|
||||
| `cargo install cargo-zigbuild` + `zig` | Rust musl cross-compilation (as for `.ipk`) |
|
||||
| apk-tools v3 `apk` binary | Provides `apk mkpkg`; not packaged for most distros — build from source |
|
||||
| `fakeroot` | Optional; makes packaged files root-owned on an unprivileged build host |
|
||||
|
||||
apk-tools is not in Debian/Ubuntu repos, so build the pinned release from source.
|
||||
Pin the same commit the targeted OpenWrt release ships (see
|
||||
`package/system/apk/Makefile` upstream) so the `.apk` is readable by the device's
|
||||
`apk`. CI builds **3.0.5** (`b5a31c0d…`):
|
||||
|
||||
```bash
|
||||
sudo apt-get install -y build-essential meson ninja-build pkg-config \
|
||||
zlib1g-dev libssl-dev libzstd-dev liblzma-dev lua5.4-dev scdoc
|
||||
git clone https://gitlab.alpinelinux.org/alpine/apk-tools.git
|
||||
cd apk-tools && git checkout b5a31c0d865342ad80be10d68f1bb3d3ad9b0866
|
||||
meson setup build && ninja -C build src/apk
|
||||
export APK_BIN="$PWD/build/src/apk"
|
||||
```
|
||||
|
||||
### Build the package
|
||||
|
||||
```bash
|
||||
# from the repo root
|
||||
./packaging/openwrt-apk/build-apk.sh --arch aarch64 # or x86_64, mipsel, mips, arm
|
||||
```
|
||||
|
||||
Output: `dist/fips_<version>_<openwrt-arch>.apk`. Override the version with
|
||||
`PKG_VERSION` (filename) and `APK_VERSION` (embedded metadata); otherwise both are
|
||||
derived from git.
|
||||
|
||||
## Installing on the router
|
||||
|
||||
Packages are **unsigned** (the same posture as our `.ipk`), so install with
|
||||
`--allow-untrusted`:
|
||||
|
||||
```bash
|
||||
scp -O dist/fips_<version>_<arch>.apk root@192.168.1.1:/tmp/
|
||||
ssh root@192.168.1.1 apk add --allow-untrusted /tmp/fips_<version>_<arch>.apk
|
||||
```
|
||||
|
||||
On OpenWrt 25.x, installing from a *signed repository* requires the publisher's
|
||||
key; a single `--allow-untrusted` package install does not. If we ever publish an
|
||||
apk feed, add ECDSA (prime256v1) signing via `apk mkpkg --sign` and distribute the
|
||||
public key to `/etc/apk/keys/`.
|
||||
|
||||
`/etc/fips/fips.yaml` is marked as a config file (via
|
||||
`/lib/apk/packages/fips.conffiles`), so apk preserves local edits across upgrades,
|
||||
and `/lib/upgrade/keep.d/fips` preserves `/etc/fips/` across `sysupgrade` — the
|
||||
same guarantees as the `.ipk` package.
|
||||
Executable
+74
@@ -0,0 +1,74 @@
|
||||
#!/bin/sh
|
||||
# Emit an apk-tools-compatible version string for FIPS.
|
||||
#
|
||||
# apk-tools enforces a strict version grammar:
|
||||
# <digit>(.<digit>)*(_<suffix><digit>*)*(-r<N>)
|
||||
# where <suffix> is a recognised pre-release/post-release token
|
||||
# (alpha, beta, pre, rc, cvs, svn, git, hg, p).
|
||||
#
|
||||
# Unlike a regex rewrite of an already-flattened version string, this
|
||||
# helper builds the apk version directly from the *structured* inputs the
|
||||
# caller already has (a release tag, or a commit height). There is no
|
||||
# parsing-back-out of a "branch.height.hash" blob, so there is no fragile
|
||||
# reparse step to get wrong.
|
||||
#
|
||||
# Usage:
|
||||
# apk-version.sh tag <git-tag> # e.g. v1.2.3, v1.2.3-rc1
|
||||
# apk-version.sh dev <height> # e.g. 1234 (git rev-list --count HEAD)
|
||||
# apk-version.sh auto # derive from the current git checkout
|
||||
#
|
||||
# Examples:
|
||||
# apk-version.sh tag v1.2.3 -> 1.2.3-r0
|
||||
# apk-version.sh tag v1.2.3-rc1 -> 1.2.3_rc1-r0
|
||||
# apk-version.sh dev 1234 -> 0.0.0_git1234-r0
|
||||
set -eu
|
||||
|
||||
mode="${1:-auto}"
|
||||
|
||||
case "$mode" in
|
||||
tag) raw_tag="${2:?tag mode requires a tag argument}"; height="" ;;
|
||||
dev) raw_tag=""; height="${2:?dev mode requires a height argument}" ;;
|
||||
auto)
|
||||
if raw_tag="$(git describe --exact-match --tags 2>/dev/null)"; then
|
||||
height=""
|
||||
else
|
||||
raw_tag=""
|
||||
height="$(git rev-list --count HEAD 2>/dev/null || echo 0)"
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
echo "usage: $0 [auto | tag <git-tag> | dev <height>]" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ -n "$raw_tag" ]; then
|
||||
# Release tag: vX.Y.Z or vX.Y.Z-<pre>. Strip the leading 'v', split the
|
||||
# core (X.Y.Z) from the pre-release token, and map our hyphen separator
|
||||
# to apk's '_' pre-release marker.
|
||||
body="${raw_tag#v}"
|
||||
core="${body%%-*}"
|
||||
case "$body" in
|
||||
*-*) pre="${body#*-}" ;;
|
||||
*) pre="" ;;
|
||||
esac
|
||||
|
||||
case "$pre" in
|
||||
"") suffix="" ;;
|
||||
alpha*|beta*|pre*|rc*) suffix="_${pre}" ;;
|
||||
*)
|
||||
# Unknown pre-release token: apk would reject or misorder it, so
|
||||
# drop it rather than emit an invalid version. The human-readable
|
||||
# PACKAGE_VERSION (the raw tag) is still used for the filename.
|
||||
suffix=""
|
||||
;;
|
||||
esac
|
||||
|
||||
printf '%s%s-r0\n' "$core" "$suffix"
|
||||
else
|
||||
# Untagged build: no meaningful semver, so anchor at 0.0.0 and encode the
|
||||
# monotonic commit height as a _git pre-release component. This keeps apk's
|
||||
# ordering sane across dev builds without smuggling the hash/branch into a
|
||||
# field that cannot represent them.
|
||||
printf '0.0.0_git%s-r0\n' "${height:-0}"
|
||||
fi
|
||||
Executable
+45
@@ -0,0 +1,45 @@
|
||||
#!/bin/sh
|
||||
# Case-table test for apk-version.sh. Run: sh apk-version.test.sh
|
||||
set -eu
|
||||
|
||||
HERE="$(cd "$(dirname "$0")" && pwd)"
|
||||
SUT="$HERE/apk-version.sh"
|
||||
|
||||
fail=0
|
||||
check() {
|
||||
# check <expected> <args...>
|
||||
expected="$1"; shift
|
||||
actual="$(sh "$SUT" "$@")"
|
||||
if [ "$actual" = "$expected" ]; then
|
||||
printf ' PASS %-22s -> %s\n' "$*" "$actual"
|
||||
else
|
||||
printf ' FAIL %-22s -> %s (expected %s)\n' "$*" "$actual" "$expected"
|
||||
fail=1
|
||||
fi
|
||||
}
|
||||
|
||||
echo "== apk-version.sh =="
|
||||
|
||||
# Plain release tags.
|
||||
check "1.2.3-r0" tag v1.2.3
|
||||
check "0.4.0-r0" tag v0.4.0
|
||||
check "10.20.30-r0" tag v10.20.30
|
||||
|
||||
# Pre-release tags: hyphen separator becomes apk's '_' marker.
|
||||
check "1.2.3_rc1-r0" tag v1.2.3-rc1
|
||||
check "1.2.3_alpha1-r0" tag v1.2.3-alpha1
|
||||
check "1.2.3_beta2-r0" tag v1.2.3-beta2
|
||||
check "1.2.3_pre1-r0" tag v1.2.3-pre1
|
||||
|
||||
# Unknown pre-release token is dropped (apk cannot represent it).
|
||||
check "1.2.3-r0" tag v1.2.3-weird9
|
||||
|
||||
# Dev builds: monotonic commit height as a _git component.
|
||||
check "0.0.0_git1234-r0" dev 1234
|
||||
check "0.0.0_git0-r0" dev 0
|
||||
|
||||
if [ "$fail" -ne 0 ]; then
|
||||
echo "FAILED"
|
||||
exit 1
|
||||
fi
|
||||
echo "OK"
|
||||
Executable
+289
@@ -0,0 +1,289 @@
|
||||
#!/bin/bash
|
||||
# Build a FIPS .apk package for OpenWrt without the OpenWrt SDK.
|
||||
#
|
||||
# apk-tools (.apk) is the mandatory package manager from OpenWrt 25 onward; it
|
||||
# is also available opt-in on 24.10, where opkg (.ipk) remains the default. The
|
||||
# .ipk package in ../openwrt-ipk/ still covers OpenWrt 24.x and earlier; this
|
||||
# .apk package is what you need on 25+. Unlike the .ipk format (a plain tar.gz
|
||||
# of tarballs that we
|
||||
# assemble by hand in ../openwrt-ipk/build-ipk.sh), the .apk container is the
|
||||
# apk-tools v3 ADB format, which is impractical to hand-roll. Instead we drive
|
||||
# the official `apk mkpkg` applet — the same tool OpenWrt's build system calls
|
||||
# in include/package-pack.mk — so no SDK is required, only the `apk` binary.
|
||||
#
|
||||
# Usage:
|
||||
# ./packaging/openwrt-apk/build-apk.sh [--arch <name>]
|
||||
#
|
||||
# Architectures (--arch): aarch64 [default], x86_64, mipsel, mips, arm
|
||||
# (the apk CI matrix ships aarch64 + x86_64; the rest are buildable locally).
|
||||
#
|
||||
# Output: dist/fips_<version>_<openwrt-arch>.apk
|
||||
#
|
||||
# Prerequisites:
|
||||
# cargo install cargo-zigbuild (Rust musl cross-compilation)
|
||||
# apk-tools v3 `apk` binary on PATH, or pointed at via APK_BIN=/path/to/apk
|
||||
# (build from source — see README.md; CI builds apk-tools 3.0.5).
|
||||
# fakeroot (optional but recommended; makes packaged files root-owned).
|
||||
#
|
||||
# Install on a router (packages are unsigned, like our .ipk):
|
||||
# scp -O dist/fips_<version>_<arch>.apk root@192.168.1.1:/tmp/
|
||||
# ssh root@192.168.1.1 apk add --allow-untrusted /tmp/fips_<version>_<arch>.apk
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Arguments
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
ARCH="aarch64"
|
||||
BIN_DIR="" # if set, use prebuilt binaries from here instead of compiling
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--arch) ARCH="$2"; shift 2 ;;
|
||||
--arch=*) ARCH="${1#*=}"; shift ;;
|
||||
--bin-dir) BIN_DIR="$2"; shift 2 ;;
|
||||
--bin-dir=*) BIN_DIR="${1#*=}"; shift ;;
|
||||
*) echo "Unknown argument: $1" >&2; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Architecture mapping
|
||||
#
|
||||
# RUST_TARGET — passed to cargo --target
|
||||
# OPENWRT_ARCH — apk "arch:" field and the package filename
|
||||
#
|
||||
# Kept in sync with ../openwrt-ipk/build-ipk.sh (same target table).
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
case "$ARCH" in
|
||||
aarch64)
|
||||
RUST_TARGET="aarch64-unknown-linux-musl"
|
||||
OPENWRT_ARCH="aarch64_cortex-a53"
|
||||
;;
|
||||
mipsel)
|
||||
RUST_TARGET="mipsel-unknown-linux-musl"
|
||||
OPENWRT_ARCH="mipsel_24kc"
|
||||
;;
|
||||
mips)
|
||||
RUST_TARGET="mips-unknown-linux-musl"
|
||||
OPENWRT_ARCH="mips_24kc"
|
||||
;;
|
||||
arm)
|
||||
RUST_TARGET="arm-unknown-linux-musleabihf"
|
||||
OPENWRT_ARCH="arm_cortex-a7"
|
||||
;;
|
||||
x86_64)
|
||||
RUST_TARGET="x86_64-unknown-linux-musl"
|
||||
OPENWRT_ARCH="x86_64"
|
||||
;;
|
||||
*)
|
||||
echo "Unknown arch: $ARCH" >&2
|
||||
echo "Valid: aarch64, mipsel, mips, arm, x86_64" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Paths
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||||
# The installed-filesystem payload (init scripts, config, sysctl, etc.) is
|
||||
# shared with the .ipk package; there is one canonical copy in openwrt-ipk/.
|
||||
FILES_DIR="$PROJECT_ROOT/packaging/openwrt-ipk/files"
|
||||
DIST_DIR="$PROJECT_ROOT/dist"
|
||||
|
||||
PKG_NAME="fips"
|
||||
# Human-readable version for the filename (e.g. v0.4.0 or master.123.abcdef0),
|
||||
# mirroring the .ipk artifacts and the CI/NIP-94 plumbing.
|
||||
PKG_VERSION="${PKG_VERSION:-$(cd "$PROJECT_ROOT" && git describe --tags --always --dirty 2>/dev/null || echo "0.1.0")}"
|
||||
# apk-tools-compatible version embedded inside the package metadata.
|
||||
APK_VERSION="${APK_VERSION:-$(cd "$PROJECT_ROOT" && sh "$SCRIPT_DIR/apk-version.sh" auto)}"
|
||||
|
||||
APK_BIN="${APK_BIN:-apk}"
|
||||
if ! command -v "$APK_BIN" >/dev/null 2>&1; then
|
||||
echo "Error: apk-tools binary not found (looked for '$APK_BIN')." >&2
|
||||
echo " Build apk-tools v3 from source or set APK_BIN=/path/to/apk." >&2
|
||||
echo " See packaging/openwrt-apk/README.md." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "==> Building $PKG_NAME $PKG_VERSION (apk version $APK_VERSION) for $OPENWRT_ARCH ($RUST_TARGET)"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 1. Obtain binaries
|
||||
#
|
||||
# Either use a directory of prebuilt binaries (--bin-dir; CI cross-compiles
|
||||
# once in a shared job and hands them to both the .ipk and .apk packagers), or
|
||||
# compile from source here for a self-contained local build.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
if [ -n "$BIN_DIR" ]; then
|
||||
RELEASE_DIR="$BIN_DIR"
|
||||
echo "==> Using prebuilt binaries from $RELEASE_DIR"
|
||||
for bin in fips fipsctl fipstop fips-gateway; do
|
||||
[ -f "$RELEASE_DIR/$bin" ] || {
|
||||
echo "Error: prebuilt binary not found: $RELEASE_DIR/$bin" >&2
|
||||
exit 1
|
||||
}
|
||||
done
|
||||
else
|
||||
if ! command -v cargo-zigbuild &>/dev/null; then
|
||||
echo "Error: cargo-zigbuild not found." >&2
|
||||
echo " Install: cargo install cargo-zigbuild" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! rustup target list --installed | grep -q "^$RUST_TARGET$"; then
|
||||
echo "==> Adding Rust target $RUST_TARGET..."
|
||||
rustup target add "$RUST_TARGET"
|
||||
fi
|
||||
|
||||
echo "==> Compiling..."
|
||||
cd "$PROJECT_ROOT"
|
||||
cargo zigbuild \
|
||||
--release \
|
||||
--target "$RUST_TARGET" \
|
||||
--bin fips \
|
||||
--bin fipsctl \
|
||||
--bin fipstop \
|
||||
--bin fips-gateway
|
||||
|
||||
RELEASE_DIR="$PROJECT_ROOT/target/$RUST_TARGET/release"
|
||||
|
||||
echo "==> Stripping binaries..."
|
||||
STRIP="${LLVM_STRIP:-strip}"
|
||||
for bin in fips fipsctl fipstop fips-gateway; do
|
||||
"$STRIP" "$RELEASE_DIR/$bin" 2>/dev/null || true
|
||||
done
|
||||
fi
|
||||
|
||||
SIZE=$(du -sh "$RELEASE_DIR/fips" | cut -f1)
|
||||
echo " fips: $SIZE"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 2. Stage the installed filesystem tree (--files root for apk mkpkg)
|
||||
# ---------------------------------------------------------------------------
|
||||
# This block is the same payload as ../openwrt-ipk/build-ipk.sh; keep the two
|
||||
# in sync. The CI apk structural check asserts every path below is present.
|
||||
|
||||
WORK_DIR="$(mktemp -d)"
|
||||
trap 'rm -rf "$WORK_DIR"' EXIT
|
||||
|
||||
STAGE_DIR="$WORK_DIR/root" # becomes the package's filesystem
|
||||
SCRIPTS_DIR="$WORK_DIR/scripts" # maintainer scripts (metadata, not payload)
|
||||
mkdir -p "$STAGE_DIR" "$SCRIPTS_DIR"
|
||||
|
||||
install -d "$STAGE_DIR/usr/bin"
|
||||
install -m 0755 "$RELEASE_DIR/fips" "$STAGE_DIR/usr/bin/fips"
|
||||
install -m 0755 "$RELEASE_DIR/fipsctl" "$STAGE_DIR/usr/bin/fipsctl"
|
||||
install -m 0755 "$RELEASE_DIR/fipstop" "$STAGE_DIR/usr/bin/fipstop"
|
||||
install -m 0755 "$RELEASE_DIR/fips-gateway" "$STAGE_DIR/usr/bin/fips-gateway"
|
||||
|
||||
install -d "$STAGE_DIR/etc/init.d"
|
||||
install -m 0755 "$FILES_DIR/etc/init.d/fips" "$STAGE_DIR/etc/init.d/fips"
|
||||
install -m 0755 "$FILES_DIR/etc/init.d/fips-gateway" "$STAGE_DIR/etc/init.d/fips-gateway"
|
||||
|
||||
install -d "$STAGE_DIR/etc/fips"
|
||||
install -m 0600 "$FILES_DIR/etc/fips/fips.yaml" "$STAGE_DIR/etc/fips/fips.yaml"
|
||||
install -m 0755 "$FILES_DIR/etc/fips/firewall.sh" "$STAGE_DIR/etc/fips/firewall.sh"
|
||||
|
||||
install -d "$STAGE_DIR/etc/dnsmasq.d"
|
||||
install -m 0644 "$FILES_DIR/etc/dnsmasq.d/fips.conf" "$STAGE_DIR/etc/dnsmasq.d/fips.conf"
|
||||
|
||||
install -d "$STAGE_DIR/etc/sysctl.d"
|
||||
install -m 0644 "$FILES_DIR/etc/sysctl.d/fips-bridge.conf" "$STAGE_DIR/etc/sysctl.d/fips-bridge.conf"
|
||||
install -m 0644 "$FILES_DIR/etc/sysctl.d/fips-gateway.conf" "$STAGE_DIR/etc/sysctl.d/fips-gateway.conf"
|
||||
|
||||
install -d "$STAGE_DIR/etc/hotplug.d/net"
|
||||
install -m 0755 "$FILES_DIR/etc/hotplug.d/net/99-fips" "$STAGE_DIR/etc/hotplug.d/net/99-fips"
|
||||
|
||||
install -d "$STAGE_DIR/etc/uci-defaults"
|
||||
install -m 0755 "$FILES_DIR/etc/uci-defaults/90-fips-setup" "$STAGE_DIR/etc/uci-defaults/90-fips-setup"
|
||||
|
||||
install -d "$STAGE_DIR/lib/upgrade/keep.d"
|
||||
install -m 0644 "$FILES_DIR/lib/upgrade/keep.d/fips" "$STAGE_DIR/lib/upgrade/keep.d/fips"
|
||||
|
||||
# ---- conffiles ----
|
||||
# apk mkpkg discovers config files from /lib/apk/packages/<name>.conffiles
|
||||
# inside the --files tree (same mechanism OpenWrt's package-pack.mk uses).
|
||||
# Listing fips.yaml here makes apk preserve user edits across upgrades, the
|
||||
# apk equivalent of opkg's conffiles handling.
|
||||
install -d "$STAGE_DIR/lib/apk/packages"
|
||||
cat > "$STAGE_DIR/lib/apk/packages/${PKG_NAME}.conffiles" <<'EOF'
|
||||
/etc/fips/fips.yaml
|
||||
EOF
|
||||
|
||||
# ---- maintainer scripts ----
|
||||
# Map our opkg maintainer scripts onto apk's lifecycle phases:
|
||||
# opkg postinst -> apk post-install (enable + start services)
|
||||
# opkg prerm -> apk pre-deinstall (stop + disable services)
|
||||
|
||||
cat > "$SCRIPTS_DIR/post-install" <<'EOF'
|
||||
#!/bin/sh
|
||||
# Run first-boot UCI setup (the script deletes itself when done).
|
||||
if [ -x /etc/uci-defaults/90-fips-setup ]; then
|
||||
/etc/uci-defaults/90-fips-setup && rm -f /etc/uci-defaults/90-fips-setup
|
||||
fi
|
||||
|
||||
/etc/init.d/fips enable
|
||||
/etc/init.d/fips start
|
||||
/etc/init.d/fips-gateway enable
|
||||
/etc/init.d/fips-gateway start
|
||||
exit 0
|
||||
EOF
|
||||
|
||||
cat > "$SCRIPTS_DIR/pre-deinstall" <<'EOF'
|
||||
#!/bin/sh
|
||||
/etc/init.d/fips-gateway stop 2>/dev/null || true
|
||||
/etc/init.d/fips-gateway disable 2>/dev/null || true
|
||||
/etc/init.d/fips stop 2>/dev/null || true
|
||||
/etc/init.d/fips disable 2>/dev/null || true
|
||||
exit 0
|
||||
EOF
|
||||
|
||||
chmod 0755 "$SCRIPTS_DIR/post-install" "$SCRIPTS_DIR/pre-deinstall"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 3. Assemble the .apk via apk mkpkg
|
||||
# ---------------------------------------------------------------------------
|
||||
# fakeroot makes the packaged files root-owned even though CI runs unprivileged.
|
||||
|
||||
DESCRIPTION="FIPS Mesh Network Daemon. Distributed, decentralized mesh networking over UDP, TCP, and raw Ethernet, with a TUN interface (fips0), ULA IPv6 addressing, and a .fips DNS responder."
|
||||
DEPENDS="kmod-tun kmod-br-netfilter kmod-nft-nat kmod-nf-conntrack ip-full"
|
||||
|
||||
PKG_FILENAME="${PKG_NAME}_${PKG_VERSION}_${OPENWRT_ARCH}.apk"
|
||||
mkdir -p "$DIST_DIR"
|
||||
|
||||
FAKEROOT=""
|
||||
if command -v fakeroot >/dev/null 2>&1; then
|
||||
FAKEROOT="fakeroot"
|
||||
else
|
||||
echo "Warning: fakeroot not found — packaged files will be owned by the build user." >&2
|
||||
fi
|
||||
|
||||
$FAKEROOT "$APK_BIN" mkpkg \
|
||||
--info "name:$PKG_NAME" \
|
||||
--info "version:$APK_VERSION" \
|
||||
--info "description:$DESCRIPTION" \
|
||||
--info "arch:$OPENWRT_ARCH" \
|
||||
--info "license:MIT" \
|
||||
--info "origin:$PKG_NAME" \
|
||||
--info "url:https://github.com/jmcorgan/fips" \
|
||||
--info "maintainer:FIPS Network" \
|
||||
--info "depends:$DEPENDS" \
|
||||
--script "post-install:$SCRIPTS_DIR/post-install" \
|
||||
--script "pre-deinstall:$SCRIPTS_DIR/pre-deinstall" \
|
||||
--files "$STAGE_DIR" \
|
||||
--output "$DIST_DIR/$PKG_FILENAME"
|
||||
|
||||
echo ""
|
||||
echo "==> Done: dist/$PKG_FILENAME"
|
||||
echo " $(du -sh "$DIST_DIR/$PKG_FILENAME" | cut -f1)"
|
||||
echo ""
|
||||
echo "Install on router (OpenWrt 25+, or 24.10 with apk enabled):"
|
||||
echo " scp -O dist/$PKG_FILENAME root@192.168.1.1:/tmp/"
|
||||
echo " ssh root@192.168.1.1 apk add --allow-untrusted /tmp/$PKG_FILENAME"
|
||||
@@ -27,11 +27,14 @@ set -euo pipefail
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
ARCH="aarch64"
|
||||
BIN_DIR="" # if set, use prebuilt binaries from here instead of compiling
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--arch) ARCH="$2"; shift 2 ;;
|
||||
--arch=*) ARCH="${1#*=}"; shift ;;
|
||||
--bin-dir) BIN_DIR="$2"; shift 2 ;;
|
||||
--bin-dir=*) BIN_DIR="${1#*=}"; shift ;;
|
||||
*) echo "Unknown argument: $1" >&2; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
@@ -86,44 +89,55 @@ PKG_VERSION="${PKG_VERSION:-$(cd "$PROJECT_ROOT" && git describe --tags --always
|
||||
echo "==> Building $PKG_NAME $PKG_VERSION for $OPENWRT_ARCH ($RUST_TARGET)"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Prerequisites
|
||||
# 1. Obtain binaries
|
||||
#
|
||||
# Either use a directory of prebuilt binaries (--bin-dir; CI cross-compiles
|
||||
# once in a shared job and hands them to both the .ipk and .apk packagers), or
|
||||
# compile from source here for a self-contained local build.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
if ! command -v cargo-zigbuild &>/dev/null; then
|
||||
echo "Error: cargo-zigbuild not found." >&2
|
||||
echo " Install: cargo install cargo-zigbuild" >&2
|
||||
exit 1
|
||||
if [ -n "$BIN_DIR" ]; then
|
||||
RELEASE_DIR="$BIN_DIR"
|
||||
echo "==> Using prebuilt binaries from $RELEASE_DIR"
|
||||
for bin in fips fipsctl fipstop fips-gateway; do
|
||||
[ -f "$RELEASE_DIR/$bin" ] || {
|
||||
echo "Error: prebuilt binary not found: $RELEASE_DIR/$bin" >&2
|
||||
exit 1
|
||||
}
|
||||
done
|
||||
else
|
||||
if ! command -v cargo-zigbuild &>/dev/null; then
|
||||
echo "Error: cargo-zigbuild not found." >&2
|
||||
echo " Install: cargo install cargo-zigbuild" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! rustup target list --installed | grep -q "^$RUST_TARGET$"; then
|
||||
echo "==> Adding Rust target $RUST_TARGET..."
|
||||
rustup target add "$RUST_TARGET"
|
||||
fi
|
||||
|
||||
echo "==> Compiling..."
|
||||
cd "$PROJECT_ROOT"
|
||||
cargo zigbuild \
|
||||
--release \
|
||||
--target "$RUST_TARGET" \
|
||||
--bin fips \
|
||||
--bin fipsctl \
|
||||
--bin fipstop \
|
||||
--bin fips-gateway
|
||||
|
||||
RELEASE_DIR="$PROJECT_ROOT/target/$RUST_TARGET/release"
|
||||
|
||||
echo "==> Stripping binaries..."
|
||||
STRIP="${LLVM_STRIP:-strip}"
|
||||
for bin in fips fipsctl fipstop fips-gateway; do
|
||||
"$STRIP" "$RELEASE_DIR/$bin" 2>/dev/null || true
|
||||
done
|
||||
fi
|
||||
|
||||
if ! rustup target list --installed | grep -q "^$RUST_TARGET$"; then
|
||||
echo "==> Adding Rust target $RUST_TARGET..."
|
||||
rustup target add "$RUST_TARGET"
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 1. Build
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
echo "==> Compiling..."
|
||||
cd "$PROJECT_ROOT"
|
||||
cargo zigbuild \
|
||||
--release \
|
||||
--target "$RUST_TARGET" \
|
||||
--bin fips \
|
||||
--bin fipsctl \
|
||||
--bin fipstop \
|
||||
--bin fips-gateway
|
||||
|
||||
RELEASE_DIR="$PROJECT_ROOT/target/$RUST_TARGET/release"
|
||||
|
||||
echo "==> Stripping binaries..."
|
||||
STRIP="${LLVM_STRIP:-strip}"
|
||||
for bin in fips fipsctl fipstop fips-gateway; do
|
||||
"$STRIP" "$RELEASE_DIR/$bin" 2>/dev/null || true
|
||||
done
|
||||
|
||||
SIZE=$(du -sh "$RELEASE_DIR/fips" | cut -f1)
|
||||
echo " fips: $SIZE after strip"
|
||||
echo " fips: $SIZE"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 2. Assemble .ipk
|
||||
|
||||
@@ -12,6 +12,17 @@ start_service() {
|
||||
# Ensure TUN module is loaded before starting the daemon.
|
||||
modprobe tun 2>/dev/null || true
|
||||
|
||||
# Pre-create the control-socket runtime directory so the daemon binds the
|
||||
# canonical /run/fips/control.sock instead of falling back to /tmp. This is
|
||||
# the procd equivalent of the systemd unit's RuntimeDirectory=fips (and the
|
||||
# fips.tmpfiles "d /run/fips 0750 root fips" entry); OpenWrt was the only
|
||||
# platform missing it. Without it, fips-gateway — which creates /run/fips
|
||||
# for its own gateway.sock — makes fipsctl/fipstop resolve a control socket
|
||||
# under /run/fips that the daemon actually bound under /tmp.
|
||||
mkdir -p /run/fips
|
||||
chmod 0750 /run/fips
|
||||
chgrp fips /run/fips 2>/dev/null || true
|
||||
|
||||
procd_open_instance
|
||||
procd_set_param command "$PROG" --config "$CONFIG"
|
||||
# Respawn: restart after 5 s, give up after 5 consecutive failures within
|
||||
|
||||
+131
-38
@@ -83,6 +83,35 @@ fn fwd_value(data: &serde_json::Value, pkt_key: &str, byte_key: &str) -> String
|
||||
format!("{} pkts ({})", pkts, helpers::format_bytes(bytes))
|
||||
}
|
||||
|
||||
/// Read a raw forwarding counter as a u64 (0 if missing), for arithmetic
|
||||
/// (percentages, derived totals) that the string-returning helpers can't do.
|
||||
fn fwd_count(data: &serde_json::Value, key: &str) -> u64 {
|
||||
data.get("forwarding")
|
||||
.and_then(|f| f.get(key))
|
||||
.and_then(|v| v.as_u64())
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
/// Total mesh egress = locally-originated + transit-forwarded, formatted as
|
||||
/// "N pkts (B)". There is no single daemon counter for everything this node
|
||||
/// transmits to peers, so it is derived from its two contributors.
|
||||
fn mesh_tx_value(data: &serde_json::Value) -> String {
|
||||
let pkts = fwd_count(data, "originated_packets") + fwd_count(data, "forwarded_packets");
|
||||
let bytes = fwd_count(data, "originated_bytes") + fwd_count(data, "forwarded_bytes");
|
||||
format!("{} pkts ({})", pkts, helpers::format_bytes(bytes))
|
||||
}
|
||||
|
||||
/// Format a route-class count as "N (xx.x%)" where the percentage is the class's
|
||||
/// share of total forwarded (transit) packets. Zero forwarded yields "0.0%".
|
||||
fn route_class_value(count: u64, total_forwarded: u64) -> String {
|
||||
let pct = if total_forwarded > 0 {
|
||||
count as f64 / total_forwarded as f64 * 100.0
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
format!("{count} ({pct:.1}%)")
|
||||
}
|
||||
|
||||
/// Build a section: a styled header line followed by the kv pairs rendered
|
||||
/// through the group helper so the section's values share a left edge.
|
||||
fn section(title: &str, pairs: &[(&str, String)]) -> Vec<Line<'static>> {
|
||||
@@ -110,49 +139,39 @@ fn draw_routing_stats(
|
||||
let err = |key: &str| helpers::nested_u64(data, "error_signals", key);
|
||||
let cong = |key: &str| helpers::nested_u64(data, "congestion", key);
|
||||
|
||||
// Left column: Forwarding + Discovery. Each section's values share a left
|
||||
// edge via the kv_lines group helper.
|
||||
// The node is an interface adapter between the local host stack and the
|
||||
// mesh; the left column reads each side as a Transmitted/Received pair.
|
||||
//
|
||||
// Local Stack — traffic crossing the TUN / local-origination boundary:
|
||||
// Transmitted is what the host injects into the mesh (originated), Received
|
||||
// is what the mesh hands up to the host (delivered).
|
||||
let mut left = section(
|
||||
"Forwarding",
|
||||
"Local Stack",
|
||||
&[
|
||||
(
|
||||
"Transmitted",
|
||||
fwd_value(data, "originated_packets", "originated_bytes"),
|
||||
),
|
||||
(
|
||||
"Received",
|
||||
fwd_value(data, "delivered_packets", "delivered_bytes"),
|
||||
),
|
||||
],
|
||||
);
|
||||
left.push(Line::from(""));
|
||||
// Mesh — traffic crossing the peer-link boundary: Transmitted is everything
|
||||
// this node puts on the wire (originated + forwarded, derived), Received is
|
||||
// the ingress aggregate from peers (own-delivered + transit + drops).
|
||||
left.extend(section(
|
||||
"Mesh",
|
||||
&[
|
||||
("Transmitted", mesh_tx_value(data)),
|
||||
(
|
||||
"Received",
|
||||
fwd_value(data, "received_packets", "received_bytes"),
|
||||
),
|
||||
(
|
||||
"Delivered",
|
||||
fwd_value(data, "delivered_packets", "delivered_bytes"),
|
||||
),
|
||||
(
|
||||
"Forwarded",
|
||||
fwd_value(data, "forwarded_packets", "forwarded_bytes"),
|
||||
),
|
||||
(
|
||||
"Originated",
|
||||
fwd_value(data, "originated_packets", "originated_bytes"),
|
||||
),
|
||||
(
|
||||
"Decode Error",
|
||||
fwd_value(data, "decode_error_packets", "decode_error_bytes"),
|
||||
),
|
||||
(
|
||||
"TTL Exhausted",
|
||||
fwd_value(data, "ttl_exhausted_packets", "ttl_exhausted_bytes"),
|
||||
),
|
||||
(
|
||||
"No Route",
|
||||
fwd_value(data, "drop_no_route_packets", "drop_no_route_bytes"),
|
||||
),
|
||||
(
|
||||
"MTU Exceeded",
|
||||
fwd_value(data, "drop_mtu_exceeded_packets", "drop_mtu_exceeded_bytes"),
|
||||
),
|
||||
(
|
||||
"Send Error",
|
||||
fwd_value(data, "drop_send_error_packets", "drop_send_error_bytes"),
|
||||
),
|
||||
],
|
||||
);
|
||||
));
|
||||
left.push(Line::from(""));
|
||||
left.extend(section(
|
||||
"Discovery Requests",
|
||||
@@ -184,15 +203,89 @@ fn draw_routing_stats(
|
||||
],
|
||||
));
|
||||
|
||||
// Right column: Error Signals + Congestion
|
||||
// Right column — "Forwarded" (transit / routed through this node).
|
||||
// Forwarded total, then the route-class breakdown (a percentage partition
|
||||
// of the total), then the transit-path drop reasons.
|
||||
let fwd_total = fwd_count(data, "forwarded_packets");
|
||||
let mut right = section(
|
||||
"Forwarded",
|
||||
&[(
|
||||
"Forwarded",
|
||||
fwd_value(data, "forwarded_packets", "forwarded_bytes"),
|
||||
)],
|
||||
);
|
||||
// Blank separator after the Forwarded total, matching the spacing between
|
||||
// every other section pair; the total and its route-class breakdown read
|
||||
// as two distinct groups.
|
||||
right.push(Line::from(""));
|
||||
// Route-class breakdown: a partition of Forwarded, each line annotated with
|
||||
// its share of the total. Tree-down cross — the dive-to-tree-child
|
||||
// cut-through — is the last class; Tree-down + Tree-down cross sum to the
|
||||
// pre-split tree-down total.
|
||||
right.extend(section(
|
||||
"Route Class",
|
||||
&[
|
||||
(
|
||||
"Direct Peer",
|
||||
route_class_value(fwd_count(data, "route_direct_peer"), fwd_total),
|
||||
),
|
||||
(
|
||||
"Tree-down",
|
||||
route_class_value(fwd_count(data, "route_tree_down"), fwd_total),
|
||||
),
|
||||
(
|
||||
"Tree-up",
|
||||
route_class_value(fwd_count(data, "route_tree_up"), fwd_total),
|
||||
),
|
||||
(
|
||||
"Cross-link descend",
|
||||
route_class_value(fwd_count(data, "route_crosslink_descend"), fwd_total),
|
||||
),
|
||||
(
|
||||
"Cross-link ascend",
|
||||
route_class_value(fwd_count(data, "route_crosslink_ascend"), fwd_total),
|
||||
),
|
||||
(
|
||||
"Tree-down cross",
|
||||
route_class_value(fwd_count(data, "route_tree_down_cross"), fwd_total),
|
||||
),
|
||||
],
|
||||
));
|
||||
right.push(Line::from(""));
|
||||
right.extend(section(
|
||||
"Dropped",
|
||||
&[
|
||||
(
|
||||
"No Route",
|
||||
fwd_value(data, "drop_no_route_packets", "drop_no_route_bytes"),
|
||||
),
|
||||
(
|
||||
"TTL Exhausted",
|
||||
fwd_value(data, "ttl_exhausted_packets", "ttl_exhausted_bytes"),
|
||||
),
|
||||
(
|
||||
"Decode Error",
|
||||
fwd_value(data, "decode_error_packets", "decode_error_bytes"),
|
||||
),
|
||||
(
|
||||
"MTU Exceeded",
|
||||
fwd_value(data, "drop_mtu_exceeded_packets", "drop_mtu_exceeded_bytes"),
|
||||
),
|
||||
(
|
||||
"Send Error",
|
||||
fwd_value(data, "drop_send_error_packets", "drop_send_error_bytes"),
|
||||
),
|
||||
],
|
||||
));
|
||||
right.push(Line::from(""));
|
||||
right.extend(section(
|
||||
"Error Signals",
|
||||
&[
|
||||
("Coords Required", err("coords_required")),
|
||||
("Path Broken", err("path_broken")),
|
||||
("MTU Exceeded", err("mtu_exceeded")),
|
||||
],
|
||||
);
|
||||
));
|
||||
right.push(Line::from(""));
|
||||
right.extend(section(
|
||||
"Congestion",
|
||||
|
||||
@@ -1134,7 +1134,12 @@ fn routing_focused_pane_scrolls() {
|
||||
let mut app1 = app_with(Tab::Routing, data);
|
||||
app1.data.insert(Tab::Cache, json!({}));
|
||||
app1.focused_pane.insert(Tab::Routing, 2);
|
||||
app1.scroll_offsets.insert((Tab::Routing, 2), 6);
|
||||
// Congestion is the last section of the right ("Forwarded") column, below
|
||||
// the route-class breakdown, Dropped, and Error Signals groups. The left
|
||||
// column is the taller of the two, so scrolling fully to the bottom would
|
||||
// over-scroll the right column past Congestion; this offset lands the
|
||||
// Congestion region inside the short window instead.
|
||||
app1.scroll_offsets.insert((Tab::Routing, 2), 24);
|
||||
let buf1 = testkit::render(100, 20, |frame, area| {
|
||||
super::routing::draw(frame, &app1, area);
|
||||
});
|
||||
|
||||
@@ -53,6 +53,12 @@
|
||||
"originated_packets": 0,
|
||||
"received_bytes": 0,
|
||||
"received_packets": 0,
|
||||
"route_crosslink_ascend": 0,
|
||||
"route_crosslink_descend": 0,
|
||||
"route_direct_peer": 0,
|
||||
"route_tree_down": 0,
|
||||
"route_tree_down_cross": 0,
|
||||
"route_tree_up": 0,
|
||||
"ttl_exhausted_bytes": 0,
|
||||
"ttl_exhausted_packets": 0
|
||||
},
|
||||
|
||||
@@ -22,6 +22,12 @@
|
||||
"originated_packets": 0,
|
||||
"received_bytes": 0,
|
||||
"received_packets": 0,
|
||||
"route_crosslink_ascend": 0,
|
||||
"route_crosslink_descend": 0,
|
||||
"route_direct_peer": 0,
|
||||
"route_tree_down": 0,
|
||||
"route_tree_down_cross": 0,
|
||||
"route_tree_up": 0,
|
||||
"ttl_exhausted_bytes": 0,
|
||||
"ttl_exhausted_packets": 0
|
||||
},
|
||||
|
||||
@@ -151,6 +151,11 @@ impl Node {
|
||||
}
|
||||
} else {
|
||||
self.metrics().forwarding.record_forwarded(encoded.len());
|
||||
// Classify this transit forward by route class (partition of
|
||||
// forwarded_packets). Done here, at the data-plane chokepoint, so
|
||||
// the error-signal routing callers of find_next_hop are excluded.
|
||||
let class = self.classify_forward(&datagram.dest_addr, &next_hop_addr);
|
||||
self.metrics().forwarding.record_route_class(class);
|
||||
if outgoing_ce {
|
||||
self.metrics().congestion.ce_forwarded.inc();
|
||||
}
|
||||
|
||||
@@ -81,6 +81,51 @@ pub struct ForwardingMetrics {
|
||||
pub drop_send_error_bytes: Counter,
|
||||
pub originated_packets: Counter,
|
||||
pub originated_bytes: Counter,
|
||||
pub route_tree_up: Counter,
|
||||
pub route_tree_down: Counter,
|
||||
pub route_tree_down_cross: Counter,
|
||||
pub route_crosslink_descend: Counter,
|
||||
pub route_crosslink_ascend: Counter,
|
||||
pub route_direct_peer: Counter,
|
||||
}
|
||||
|
||||
/// Route class of a transit-forwarded packet, classified from tree
|
||||
/// coordinates at the forwarding decision point. The six variants
|
||||
/// partition `forwarded_packets` exactly.
|
||||
///
|
||||
/// Two variants are up-and-over forwards (destination not in the chosen
|
||||
/// peer's subtree); they differ in whether they depend on a child
|
||||
/// advertising cross-link reach *upward* to its parent:
|
||||
/// - `TreeDownCross`: the chosen peer is our tree descendant, but the
|
||||
/// destination is *not* in that child's subtree. The forward only fired
|
||||
/// because the child advertised cross-link reach upward to us, beyond its
|
||||
/// own subtree. If children advertised only their subtree upward, this
|
||||
/// forward would route up instead, so its count measures how much
|
||||
/// forwarding depends on the upward cross-link advertisement — the
|
||||
/// dive-to-tree-child cut-through.
|
||||
/// - `CrosslinkAscend`: the chosen peer is lateral (neither ancestor nor
|
||||
/// descendant) and the destination is not in its subtree. This is a node
|
||||
/// using its *own* cross-link, learned via the peer's split-horizon
|
||||
/// advertisement to its neighbors, so it does not depend on any upward
|
||||
/// advertisement. Tracked alongside `TreeDownCross` as the lateral
|
||||
/// up-and-over contrast.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum RouteClass {
|
||||
/// Chosen peer is our ancestor (tree-up).
|
||||
TreeUp,
|
||||
/// Chosen peer is our descendant and dest is in its subtree (canonical
|
||||
/// tree-down).
|
||||
TreeDown,
|
||||
/// Chosen peer is our descendant but dest is *not* in its subtree: the
|
||||
/// dive-to-tree-child cut-through enabled by upward cross-link
|
||||
/// advertisement.
|
||||
TreeDownCross,
|
||||
/// Chosen peer is lateral and dest is in its subtree (subtree entry).
|
||||
CrosslinkDescend,
|
||||
/// Chosen peer is lateral and dest is not in its subtree (up-and-over).
|
||||
CrosslinkAscend,
|
||||
/// Chosen peer is the destination itself (degenerate direct hop).
|
||||
DirectPeer,
|
||||
}
|
||||
|
||||
impl ForwardingMetrics {
|
||||
@@ -112,6 +157,21 @@ impl ForwardingMetrics {
|
||||
self.originated_bytes.add(bytes as u64);
|
||||
}
|
||||
|
||||
/// Record the route class of a transit-forwarded packet. The five
|
||||
/// classes partition `forwarded_packets`, so this is called exactly
|
||||
/// once per `record_forwarded` (transit chokepoint only).
|
||||
#[inline]
|
||||
pub fn record_route_class(&self, class: RouteClass) {
|
||||
match class {
|
||||
RouteClass::TreeUp => self.route_tree_up.inc(),
|
||||
RouteClass::TreeDown => self.route_tree_down.inc(),
|
||||
RouteClass::TreeDownCross => self.route_tree_down_cross.inc(),
|
||||
RouteClass::CrosslinkDescend => self.route_crosslink_descend.inc(),
|
||||
RouteClass::CrosslinkAscend => self.route_crosslink_ascend.inc(),
|
||||
RouteClass::DirectPeer => self.route_direct_peer.inc(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Mirror of `ForwardingStats::record_reject_bytes`: route a typed
|
||||
/// forwarding rejection of `bytes` payload to its packet and byte
|
||||
/// counters.
|
||||
@@ -163,6 +223,12 @@ impl ForwardingMetrics {
|
||||
drop_send_error_bytes: self.drop_send_error_bytes.get(),
|
||||
originated_packets: self.originated_packets.get(),
|
||||
originated_bytes: self.originated_bytes.get(),
|
||||
route_tree_up: self.route_tree_up.get(),
|
||||
route_tree_down: self.route_tree_down.get(),
|
||||
route_tree_down_cross: self.route_tree_down_cross.get(),
|
||||
route_crosslink_descend: self.route_crosslink_descend.get(),
|
||||
route_crosslink_ascend: self.route_crosslink_ascend.get(),
|
||||
route_direct_peer: self.route_direct_peer.get(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2706,6 +2706,86 @@ impl Node {
|
||||
self.peers.get(&next_hop_id).filter(|p| p.can_send())
|
||||
}
|
||||
|
||||
/// Classify a transit forward by route class from tree coordinates.
|
||||
///
|
||||
/// Called at the transit chokepoint after `find_next_hop` returns a peer,
|
||||
/// so the six classes partition `forwarded_packets` exactly. The branch
|
||||
/// that `find_next_hop` took (bloom vs greedy-tree) is *not* the route
|
||||
/// class: a peer can be selected by either, so the cut-through splits
|
||||
/// (`TreeDownCross`, `CrosslinkAscend`) are decided here from coordinates,
|
||||
/// not from which branch fired.
|
||||
///
|
||||
/// Inputs: our coords (`tree_state.my_coords`), the chosen peer's coords
|
||||
/// (`tree_state.peer_coords`), and the destination coords (re-read from the
|
||||
/// coord cache, which `find_next_hop` just touched). Both the tree-down and
|
||||
/// cross-link branches split on whether the destination is in the chosen
|
||||
/// peer's subtree; when the dest coords are unavailable that test defaults
|
||||
/// to "not in subtree", i.e. the up-and-over variant (`TreeDownCross` for a
|
||||
/// descendant peer, `CrosslinkAscend` for a lateral one).
|
||||
pub(crate) fn classify_forward(
|
||||
&self,
|
||||
dest: &NodeAddr,
|
||||
chosen_peer: &NodeAddr,
|
||||
) -> metrics::RouteClass {
|
||||
// Degenerate: the next hop is the destination itself (Branch 2).
|
||||
if chosen_peer == dest {
|
||||
return metrics::RouteClass::DirectPeer;
|
||||
}
|
||||
|
||||
let my_addr = self.node_addr();
|
||||
let my_coords = self.tree_state.my_coords();
|
||||
|
||||
// Tree-up: the chosen peer is our ancestor.
|
||||
if my_coords.has_ancestor(chosen_peer) {
|
||||
return metrics::RouteClass::TreeUp;
|
||||
}
|
||||
|
||||
// Whether the destination is in the chosen peer's subtree. Both the
|
||||
// tree-down and cross-link splits below turn on this same test, so it
|
||||
// is computed once. On the live transit path the dest coords are
|
||||
// always present here: `find_next_hop` looks them up with an early
|
||||
// return, so a coord-cache miss yields no next hop to classify (the
|
||||
// caller signals `CoordsRequired` instead of forwarding). The miss
|
||||
// branch below is therefore defensive — reachable only by direct
|
||||
// unit-test calls — and defaults the test to "not in subtree", i.e.
|
||||
// the up-and-over variant of whichever branch fires (TreeDownCross for
|
||||
// a descendant peer, CrosslinkAscend for a lateral one), matching the
|
||||
// original cross-link default-to-ascend.
|
||||
let now_ms = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map(|d| d.as_millis() as u64)
|
||||
.unwrap_or(0);
|
||||
let dest_in_peer_subtree = self
|
||||
.coord_cache
|
||||
.get(dest, now_ms)
|
||||
.is_some_and(|dest_coords| dest_coords.has_ancestor(chosen_peer));
|
||||
|
||||
// Tree-down: the chosen peer is our descendant (we are its ancestor).
|
||||
// Split by subtree membership: a dest genuinely below the child is the
|
||||
// canonical tree-down; a dest *not* below it means we only forwarded
|
||||
// down because the child advertised cross-link reach upward, beyond its
|
||||
// own subtree — the dive-to-tree-child cut-through (TreeDownCross).
|
||||
if let Some(peer_coords) = self.tree_state.peer_coords(chosen_peer)
|
||||
&& peer_coords.has_ancestor(my_addr)
|
||||
{
|
||||
return if dest_in_peer_subtree {
|
||||
metrics::RouteClass::TreeDown
|
||||
} else {
|
||||
metrics::RouteClass::TreeDownCross
|
||||
};
|
||||
}
|
||||
|
||||
// Cross-link (lateral): split by whether the destination is in the
|
||||
// chosen peer's subtree. Descend = subtree entry; ascend = up-and-over
|
||||
// via the node's own cross-link (learned from the peer's split-horizon
|
||||
// advertisement, independent of any upward advertisement).
|
||||
if dest_in_peer_subtree {
|
||||
return metrics::RouteClass::CrosslinkDescend;
|
||||
}
|
||||
|
||||
metrics::RouteClass::CrosslinkAscend
|
||||
}
|
||||
|
||||
/// Select the best peer from a set of bloom filter candidates.
|
||||
///
|
||||
/// Uses distance from each candidate's tree coordinates to the destination
|
||||
|
||||
@@ -229,6 +229,12 @@ pub struct ForwardingStatsSnapshot {
|
||||
pub drop_send_error_bytes: u64,
|
||||
pub originated_packets: u64,
|
||||
pub originated_bytes: u64,
|
||||
pub route_tree_up: u64,
|
||||
pub route_tree_down: u64,
|
||||
pub route_tree_down_cross: u64,
|
||||
pub route_crosslink_descend: u64,
|
||||
pub route_crosslink_ascend: u64,
|
||||
pub route_direct_peer: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize)]
|
||||
|
||||
@@ -1110,3 +1110,226 @@ async fn test_routing_source_only_coords_100_nodes() {
|
||||
|
||||
cleanup_nodes(&mut nodes).await;
|
||||
}
|
||||
|
||||
// === Route-class classification (transit-forward partition) ===
|
||||
|
||||
use crate::node::metrics::{ForwardingMetrics, RouteClass};
|
||||
|
||||
/// Current epoch millis, matching the cache-insert idiom used above.
|
||||
fn now_ms() -> u64 {
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map(|d| d.as_millis() as u64)
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_classify_forward_tree_up() {
|
||||
// my_coords = [me, parent, root]; the chosen peer is our parent (an
|
||||
// ancestor in our path) → tree-up.
|
||||
let mut node = make_node();
|
||||
let me = *node.node_addr();
|
||||
let parent = make_node_addr(10);
|
||||
let root = make_node_addr(1);
|
||||
node.tree_state_mut()
|
||||
.set_my_coords_for_test(TreeCoordinate::from_addrs(vec![me, parent, root]).unwrap());
|
||||
|
||||
// Destination somewhere above us; routed via the parent.
|
||||
let dest = make_node_addr(50);
|
||||
node.coord_cache_mut().insert(
|
||||
dest,
|
||||
TreeCoordinate::from_addrs(vec![dest, root]).unwrap(),
|
||||
now_ms(),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
node.classify_forward(&dest, &parent),
|
||||
RouteClass::TreeUp,
|
||||
"chosen peer is our ancestor"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_classify_forward_tree_down() {
|
||||
// Chosen peer is our descendant: its coords name us as an ancestor.
|
||||
let mut node = make_node();
|
||||
let me = *node.node_addr();
|
||||
let root = make_node_addr(1);
|
||||
node.tree_state_mut()
|
||||
.set_my_coords_for_test(TreeCoordinate::from_addrs(vec![me, root]).unwrap());
|
||||
|
||||
let child = make_node_addr(20);
|
||||
node.tree_state_mut().update_peer(
|
||||
ParentDeclaration::new(child, me, 1, 1000),
|
||||
TreeCoordinate::from_addrs(vec![child, me, root]).unwrap(),
|
||||
);
|
||||
|
||||
// Destination below the child; routed down to it.
|
||||
let dest = make_node_addr(60);
|
||||
node.coord_cache_mut().insert(
|
||||
dest,
|
||||
TreeCoordinate::from_addrs(vec![dest, child, me, root]).unwrap(),
|
||||
now_ms(),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
node.classify_forward(&dest, &child),
|
||||
RouteClass::TreeDown,
|
||||
"chosen peer is our descendant, dest in its subtree"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_classify_forward_tree_down_cross() {
|
||||
// Chosen peer is our descendant (a tree child), but the destination is NOT
|
||||
// in that child's subtree: we are diving down to the child only because it
|
||||
// advertised cross-link reach upward, beyond its own subtree. This is the
|
||||
// dive-to-tree-child cut-through.
|
||||
let mut node = make_node();
|
||||
let me = *node.node_addr();
|
||||
let root = make_node_addr(1);
|
||||
node.tree_state_mut()
|
||||
.set_my_coords_for_test(TreeCoordinate::from_addrs(vec![me, root]).unwrap());
|
||||
|
||||
let child = make_node_addr(20);
|
||||
node.tree_state_mut().update_peer(
|
||||
ParentDeclaration::new(child, me, 1, 1000),
|
||||
TreeCoordinate::from_addrs(vec![child, me, root]).unwrap(),
|
||||
);
|
||||
|
||||
// Destination lives elsewhere (directly under root), NOT under the child;
|
||||
// reachable from the child only via a cross-link.
|
||||
let dest = make_node_addr(60);
|
||||
node.coord_cache_mut().insert(
|
||||
dest,
|
||||
TreeCoordinate::from_addrs(vec![dest, root]).unwrap(),
|
||||
now_ms(),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
node.classify_forward(&dest, &child),
|
||||
RouteClass::TreeDownCross,
|
||||
"descendant peer, dest not in its subtree (dive-to-tree-child cut-through)"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_classify_forward_crosslink_descend() {
|
||||
// Chosen peer is lateral (not in our path, we are not in its path) and the
|
||||
// destination is inside the peer's subtree → cross-link descend.
|
||||
let mut node = make_node();
|
||||
let me = *node.node_addr();
|
||||
let root = make_node_addr(1);
|
||||
let sibling_parent = make_node_addr(2);
|
||||
node.tree_state_mut()
|
||||
.set_my_coords_for_test(TreeCoordinate::from_addrs(vec![me, root]).unwrap());
|
||||
|
||||
let peer = make_node_addr(30);
|
||||
node.tree_state_mut().update_peer(
|
||||
ParentDeclaration::new(peer, sibling_parent, 1, 1000),
|
||||
TreeCoordinate::from_addrs(vec![peer, sibling_parent, root]).unwrap(),
|
||||
);
|
||||
|
||||
// Destination is under the cross-link peer.
|
||||
let dest = make_node_addr(70);
|
||||
node.coord_cache_mut().insert(
|
||||
dest,
|
||||
TreeCoordinate::from_addrs(vec![dest, peer, sibling_parent, root]).unwrap(),
|
||||
now_ms(),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
node.classify_forward(&dest, &peer),
|
||||
RouteClass::CrosslinkDescend,
|
||||
"lateral peer, dest in its subtree"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_classify_forward_crosslink_ascend() {
|
||||
// Chosen peer is lateral and the destination is NOT in its subtree → the
|
||||
// up-and-over case (the Bloom v2 behavior delta).
|
||||
let mut node = make_node();
|
||||
let me = *node.node_addr();
|
||||
let root = make_node_addr(1);
|
||||
let sibling_parent = make_node_addr(2);
|
||||
node.tree_state_mut()
|
||||
.set_my_coords_for_test(TreeCoordinate::from_addrs(vec![me, root]).unwrap());
|
||||
|
||||
let peer = make_node_addr(40);
|
||||
node.tree_state_mut().update_peer(
|
||||
ParentDeclaration::new(peer, sibling_parent, 1, 1000),
|
||||
TreeCoordinate::from_addrs(vec![peer, sibling_parent, root]).unwrap(),
|
||||
);
|
||||
|
||||
// Destination lives elsewhere (under root directly), NOT under the peer.
|
||||
let dest = make_node_addr(80);
|
||||
node.coord_cache_mut().insert(
|
||||
dest,
|
||||
TreeCoordinate::from_addrs(vec![dest, root]).unwrap(),
|
||||
now_ms(),
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
node.classify_forward(&dest, &peer),
|
||||
RouteClass::CrosslinkAscend,
|
||||
"lateral peer, dest not in its subtree"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_classify_forward_direct_peer() {
|
||||
// Degenerate case: the next hop is the destination itself.
|
||||
let mut node = make_node();
|
||||
let me = *node.node_addr();
|
||||
let root = make_node_addr(1);
|
||||
node.tree_state_mut()
|
||||
.set_my_coords_for_test(TreeCoordinate::from_addrs(vec![me, root]).unwrap());
|
||||
|
||||
let dest = make_node_addr(90);
|
||||
assert_eq!(
|
||||
node.classify_forward(&dest, &dest),
|
||||
RouteClass::DirectPeer,
|
||||
"next hop is the destination"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_route_class_partition_sums_to_forwarded() {
|
||||
// The six route classes partition forwarded_packets: bumping
|
||||
// record_forwarded once per record_route_class keeps the sum of the class
|
||||
// counters equal to forwarded_packets.
|
||||
let m = ForwardingMetrics::default();
|
||||
let classes = [
|
||||
RouteClass::TreeUp,
|
||||
RouteClass::TreeUp,
|
||||
RouteClass::TreeDown,
|
||||
RouteClass::TreeDownCross,
|
||||
RouteClass::TreeDownCross,
|
||||
RouteClass::CrosslinkDescend,
|
||||
RouteClass::CrosslinkAscend,
|
||||
RouteClass::CrosslinkAscend,
|
||||
RouteClass::CrosslinkAscend,
|
||||
RouteClass::DirectPeer,
|
||||
];
|
||||
for &c in &classes {
|
||||
m.record_forwarded(100);
|
||||
m.record_route_class(c);
|
||||
}
|
||||
|
||||
let snap = m.snapshot();
|
||||
let class_sum = snap.route_tree_up
|
||||
+ snap.route_tree_down
|
||||
+ snap.route_tree_down_cross
|
||||
+ snap.route_crosslink_descend
|
||||
+ snap.route_crosslink_ascend
|
||||
+ snap.route_direct_peer;
|
||||
assert_eq!(
|
||||
class_sum, snap.forwarded_packets,
|
||||
"route classes must partition forwarded_packets"
|
||||
);
|
||||
assert_eq!(snap.route_tree_up, 2);
|
||||
assert_eq!(snap.route_tree_down_cross, 2);
|
||||
assert_eq!(snap.route_crosslink_ascend, 3);
|
||||
assert_eq!(snap.route_direct_peer, 1);
|
||||
}
|
||||
|
||||
@@ -92,6 +92,15 @@ impl TreeState {
|
||||
&self.my_coords
|
||||
}
|
||||
|
||||
/// Test-only override of this node's coordinates, bypassing the
|
||||
/// parent/declaration state machine. Lets routing tests place the node at
|
||||
/// an arbitrary tree position to exercise coordinate-based classification.
|
||||
#[cfg(test)]
|
||||
pub(crate) fn set_my_coords_for_test(&mut self, coords: TreeCoordinate) {
|
||||
self.root = *coords.root_id();
|
||||
self.my_coords = coords;
|
||||
}
|
||||
|
||||
/// Get the current root.
|
||||
pub fn root(&self) -> &NodeAddr {
|
||||
&self.root
|
||||
|
||||
Reference in New Issue
Block a user