From a3856eed3b4f636f0fde2729c1fb7e6dadbd8603 Mon Sep 17 00:00:00 2001 From: Ultradesu Date: Fri, 2 Oct 2026 13:34:33 +0100 Subject: [PATCH] Build a dependency-free .deb and attach it to the release MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a Debian package to the release for the x86_64 and aarch64 Linux builds, so `apt install ./tsunagi_*.deb` is a convenient path on the Ubuntu hosts. Because the binary is static musl, the package declares no Depends — it is built with plain dpkg-deb on the runner (no cargo-deb or nfpm), from dist/deb/build-deb.sh. The package installs: - the static binary at /usr/bin/tsunagi; - a systemd service at /lib/systemd/system/tsunagi.service that runs the agent as root (ExecStart rewritten to /usr/bin; the source unit targets /usr/local/bin for the tarball installer); - a polkit rule at /usr/share/polkit-1/rules.d granting the resolve1 actions to a "tsunagi" group, for running the agent as a non-root user instead of via the root service. postinst creates that system group and runs daemon-reload; prerm disables the service on removal. Both release workflows gain a "Build the Debian package" step on the two Linux matrix entries (deb/deb_arch), include the .deb in the uploaded run artifacts, and attach it to the release (Gitea via the API loop, GitHub via gh). macOS and Windows are unchanged. Service runs as root by default; the polkit rule/group is the documented opt-in for the non-root path — say if you'd rather the service itself run as the tsunagi user. YAML and shell syntax validated; the .deb build itself runs only on the Linux runner (no dpkg-deb locally) and has not been exercised in this session. Co-Authored-By: Claude Opus 4.8 --- .gitea/workflows/release.yml | 30 ++++++++-- .github/workflows/release.yml | 27 ++++++++- dist/deb/50-tsunagi-resolved.rules | 25 ++++++++ dist/deb/build-deb.sh | 92 ++++++++++++++++++++++++++++++ 4 files changed, 167 insertions(+), 7 deletions(-) create mode 100644 dist/deb/50-tsunagi-resolved.rules create mode 100755 dist/deb/build-deb.sh diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml index c41675e..39aa0d5 100644 --- a/.gitea/workflows/release.yml +++ b/.gitea/workflows/release.yml @@ -33,11 +33,15 @@ jobs: asset: tsunagi-linux-x86_64 archive: tar.gz zig: true + deb: true + deb_arch: amd64 - os: ubuntu-latest target: aarch64-unknown-linux-musl asset: tsunagi-linux-aarch64 archive: tar.gz zig: true + deb: true + deb_arch: arm64 - os: macos-latest target: aarch64-apple-darwin asset: tsunagi-macos-aarch64 @@ -133,13 +137,31 @@ jobs: tar -C dist/pkg -czf "${archive}" "${{ matrix.asset }}" fi + # A dependency-free .deb for the x86_64 and aarch64 Linux builds: it drops + # the static binary, the systemd service and the polkit rule. Built with + # dpkg-deb (present on the runner); see dist/deb/build-deb.sh. + - name: Build the Debian package + if: matrix.deb + shell: bash + run: | + set -euo pipefail + version="${GITHUB_REF_NAME#v}" + bash dist/deb/build-deb.sh \ + "target/${{ matrix.target }}/release/tsunagi" \ + "${{ matrix.deb_arch }}" \ + "$version" \ + "tsunagi_${version}_${{ matrix.deb_arch }}.deb" + # @v3 on purpose: Gitea does not support the v4 artifact backend. This is - # what makes the archive show up under the run and hands it to `publish`. - - name: Upload the archive as a run artifact + # what makes the archive (and the .deb, where built) show up under the run + # and hands it to `publish`. + - name: Upload the build outputs as run artifacts uses: actions/upload-artifact@v3 with: name: ${{ matrix.asset }} - path: ${{ matrix.asset }}-*.* + path: | + ${{ matrix.asset }}-*.* + *.deb if-no-files-found: error publish: @@ -211,5 +233,5 @@ jobs: cat up.json >&2 || true rc=1 fi - done < <(find artifacts -type f \( -name '*.tar.gz' -o -name '*.zip' \) -print0) + done < <(find artifacts -type f \( -name '*.tar.gz' -o -name '*.zip' -o -name '*.deb' \) -print0) exit "$rc" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 945111b..6cd1c38 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -34,11 +34,15 @@ jobs: asset: tsunagi-linux-x86_64 archive: tar.gz zig: true + deb: true + deb_arch: amd64 - os: ubuntu-latest target: aarch64-unknown-linux-musl asset: tsunagi-linux-aarch64 archive: tar.gz zig: true + deb: true + deb_arch: arm64 - os: macos-latest target: aarch64-apple-darwin asset: tsunagi-macos-aarch64 @@ -134,11 +138,28 @@ jobs: tar -C dist/pkg -czf "${archive}" "${{ matrix.asset }}" fi - - name: Upload the archive as a run artifact + # A dependency-free .deb for the x86_64 and aarch64 Linux builds: it drops + # the static binary, the systemd service and the polkit rule. Built with + # dpkg-deb (present on the runner); see dist/deb/build-deb.sh. + - name: Build the Debian package + if: matrix.deb + shell: bash + run: | + set -euo pipefail + version="${GITHUB_REF_NAME#v}" + bash dist/deb/build-deb.sh \ + "target/${{ matrix.target }}/release/tsunagi" \ + "${{ matrix.deb_arch }}" \ + "$version" \ + "tsunagi_${version}_${{ matrix.deb_arch }}.deb" + + - name: Upload the build outputs as run artifacts uses: actions/upload-artifact@v4 with: name: ${{ matrix.asset }} - path: ${{ matrix.asset }}-*.* + path: | + ${{ matrix.asset }}-*.* + *.deb if-no-files-found: error publish: @@ -167,7 +188,7 @@ jobs: # download-artifact@v4 nests each artifact under its own directory, so # the archives are artifacts//. - mapfile -t files < <(find artifacts -type f \( -name '*.tar.gz' -o -name '*.zip' \)) + mapfile -t files < <(find artifacts -type f \( -name '*.tar.gz' -o -name '*.zip' -o -name '*.deb' \)) printf 'asset: %s\n' "${files[@]}" prerelease=() diff --git a/dist/deb/50-tsunagi-resolved.rules b/dist/deb/50-tsunagi-resolved.rules new file mode 100644 index 0000000..00ae4d7 --- /dev/null +++ b/dist/deb/50-tsunagi-resolved.rules @@ -0,0 +1,25 @@ +// Installed by the tsunagi package. +// +// Lets a non-root agent configure systemd-resolved for its overlay zones — the +// four resolve1 actions it calls, and nothing else — for any user in the +// "tsunagi" group (created by the package). The packaged systemd service runs +// as root and does not need this rule; it is for running the agent as your own +// user instead: +// +// sudo usermod -aG tsunagi "$USER" +// sudo setcap cap_net_admin,cap_net_bind_service+p /usr/bin/tsunagi +// tsunagi up +// +// polkit decides by user/group id, not by capability, which is why the resolver +// needs this rule even once the binary has the capability. +polkit.addRule(function(action, subject) { + var allowed = [ + "org.freedesktop.resolve1.set-dns-servers", + "org.freedesktop.resolve1.set-domains", + "org.freedesktop.resolve1.set-default-route", + "org.freedesktop.resolve1.revert" + ]; + if (allowed.indexOf(action.id) >= 0 && subject.isInGroup("tsunagi")) { + return polkit.Result.YES; + } +}); diff --git a/dist/deb/build-deb.sh b/dist/deb/build-deb.sh new file mode 100755 index 0000000..a38a2bc --- /dev/null +++ b/dist/deb/build-deb.sh @@ -0,0 +1,92 @@ +#!/usr/bin/env bash +# Assemble a dependency-free Debian package from a prebuilt static tsunagi +# binary, using only dpkg-deb (no cargo-deb, no nfpm). The binary is statically +# linked, so the package declares no Depends and runs on any Debian/Ubuntu. +# +# Usage: build-deb.sh +# path to the built tsunagi executable +# Debian architecture: amd64 or arm64 +# package version (the tag without its leading v) +# path to write the .deb to +set -euo pipefail + +binary="$1" +arch="$2" +version="$3" +output="$4" + +here="$(cd "$(dirname "$0")" && pwd)" +root="$(mktemp -d)" +trap 'rm -rf "$root"' EXIT + +# Payload. Debian installs to /usr/bin (not /usr/local), so the service's +# ExecStart is rewritten to match; the source unit targets /usr/local/bin for +# the tarball installer. +install -D -m 0755 "$binary" "$root/usr/bin/tsunagi" + +mkdir -p "$root/lib/systemd/system" +sed 's#/usr/local/bin/tsunagi#/usr/bin/tsunagi#' \ + "$here/../linux/tsunagi.service" > "$root/lib/systemd/system/tsunagi.service" +chmod 0644 "$root/lib/systemd/system/tsunagi.service" + +install -D -m 0644 "$here/50-tsunagi-resolved.rules" \ + "$root/usr/share/polkit-1/rules.d/50-tsunagi-resolved.rules" +install -D -m 0644 "$here/../../README.md" "$root/usr/share/doc/tsunagi/README.md" +install -D -m 0644 "$here/../linux/README.md" "$root/usr/share/doc/tsunagi/INSTALL.md" + +mkdir -p "$root/DEBIAN" + +cat > "$root/DEBIAN/control" < +Section: net +Priority: optional +Homepage: https://github.com/Ultradesu/tsunagi +Description: Serverless private mesh networking agent + tsunagi forms small private mesh IP networks between peers, with NAT + traversal and no central server. This package installs a static, + dependency-free binary, a systemd service that runs the agent as root, and a + polkit rule that lets a non-root agent configure systemd-resolved. +CTRL + +# A system group for the optional non-root run (the polkit rule grants it the +# resolve1 actions). The packaged service runs as root and does not need it. +cat > "$root/DEBIAN/postinst" <<'POSTINST' +#!/bin/sh +set -e +if ! getent group tsunagi >/dev/null 2>&1; then + addgroup --system tsunagi >/dev/null 2>&1 || true +fi +if [ -d /run/systemd/system ]; then + systemctl daemon-reload >/dev/null 2>&1 || true +fi +exit 0 +POSTINST + +cat > "$root/DEBIAN/prerm" <<'PRERM' +#!/bin/sh +set -e +if [ "$1" = remove ] || [ "$1" = purge ]; then + if [ -d /run/systemd/system ]; then + systemctl disable --now tsunagi.service >/dev/null 2>&1 || true + fi +fi +exit 0 +PRERM + +cat > "$root/DEBIAN/postrm" <<'POSTRM' +#!/bin/sh +set -e +if [ -d /run/systemd/system ]; then + systemctl daemon-reload >/dev/null 2>&1 || true +fi +exit 0 +POSTRM + +chmod 0755 "$root/DEBIAN/postinst" "$root/DEBIAN/prerm" "$root/DEBIAN/postrm" + +# --root-owner-group writes the payload as root:root without needing fakeroot. +dpkg-deb --root-owner-group --build "$root" "$output" +echo "built $output"