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"