From d1ab993dd2c86a49f47d881d6826adf400dfc081 Mon Sep 17 00:00:00 2001 From: Ultradesu Date: Fri, 2 Oct 2026 11:48:19 +0100 Subject: [PATCH] Add a tagged-release CI workflow and Linux/Windows packaging Pushing a `v*` tag now builds the agent for every supported target and attaches the archives to a GitHub release. Modelled on the furumi release workflow (a build matrix, then a publish job that runs `gh release create`), adapted to this project's two differences: it needs system integration (capabilities/polkit on Linux, elevation+wintun on Windows, a root daemon on macOS), and its users run varied distros, not just Ubuntu. Linux is built as a fully **static musl** binary with cargo-zigbuild, so one artifact runs on Arch, Alpine, NixOS and old-glibc systems alike, and can be repackaged into whatever a distro prefers later. musl has no prebuilt aws-lc-sys bindings, so the Linux jobs install cmake + libclang (bindgen) and zig (the cross C compiler zigbuild drives). macOS (aarch64) and Windows (x86_64) build natively; Linux covers x86_64 and aarch64. Targets: x86_64/aarch64 linux-musl, aarch64 macOS, x86_64 Windows. Each archive carries the binary, the top-level README, and the platform's install notes plus integration files: - dist/linux/: a static-friendly README, a systemd unit (runs as root, so no capability grant or polkit rule is needed; a commented non-root variant is included), and an install.sh that drops the binary and the unit or prints the setcap/polkit alternative. - dist/windows/: install notes covering wintun.dll and Run-as-administrator. - dist/macos/: the existing LaunchDaemon plist and notes, now also shipped in the archive. Not exercised yet: the workflow only runs on a real tag push, and the aws-lc-sys-under-musl build has not been run locally in this session (no zig toolchain here). YAML and install.sh syntax are validated. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/release.yml | 148 ++++++++++++++++++++++++++++++++++ dist/linux/README.md | 62 ++++++++++++++ dist/linux/install.sh | 57 +++++++++++++ dist/linux/tsunagi.service | 26 ++++++ dist/windows/README.md | 46 +++++++++++ 5 files changed, 339 insertions(+) create mode 100644 .github/workflows/release.yml create mode 100644 dist/linux/README.md create mode 100755 dist/linux/install.sh create mode 100644 dist/linux/tsunagi.service create mode 100644 dist/windows/README.md diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..6abde92 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,148 @@ +name: Release + +# Build the agent for every supported target on a version tag and attach the +# archives to a GitHub release. Linux is a fully static musl binary (built with +# cargo-zigbuild) so one artifact runs on any distro — Arch, Alpine, NixOS, old +# glibc systems alike — and can then be repackaged however a distro prefers. +on: + push: + tags: ["v*"] + +permissions: + contents: read + +env: + CARGO_TERM_COLOR: always + CARGO_NET_RETRY: "10" + +jobs: + build: + name: ${{ matrix.asset }} + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-latest + target: x86_64-unknown-linux-musl + asset: tsunagi-linux-x86_64 + archive: tar.gz + zig: true + - os: ubuntu-latest + target: aarch64-unknown-linux-musl + asset: tsunagi-linux-aarch64 + archive: tar.gz + zig: true + - os: macos-latest + target: aarch64-apple-darwin + asset: tsunagi-macos-aarch64 + archive: tar.gz + zig: false + - os: windows-latest + target: x86_64-pc-windows-msvc + asset: tsunagi-windows-x86_64 + archive: zip + zig: false + + steps: + - uses: actions/checkout@v4 + + - uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ matrix.target }} + + - uses: Swatinem/rust-cache@v2 + with: + key: ${{ matrix.target }} + + # aws-lc-sys (iroh's rustls backend) and the bundled sqlite are C, and the + # musl targets have no prebuilt bindings, so bindgen needs libclang and the + # C build needs cmake. zig is the cross C compiler cargo-zigbuild drives. + - name: Install musl build tooling + if: matrix.zig + run: sudo apt-get update && sudo apt-get install -y --no-install-recommends cmake clang libclang-dev + - name: Install zig + if: matrix.zig + uses: mlugg/setup-zig@v1 + - name: Install cargo-zigbuild + if: matrix.zig + uses: taiki-e/install-action@v2 + with: + tool: cargo-zigbuild + + - name: Build + shell: bash + run: | + set -euo pipefail + if [[ "${{ matrix.zig }}" == "true" ]]; then + cargo zigbuild --release --locked --target "${{ matrix.target }}" --bin tsunagi + else + cargo build --release --locked --target "${{ matrix.target }}" --bin tsunagi + fi + + - name: Package + shell: bash + run: | + set -euo pipefail + + version="${GITHUB_REF_NAME#v}" + stage="dist/pkg/${{ matrix.asset }}" + mkdir -p "$stage" + + bin="tsunagi" + [[ "${{ runner.os }}" == "Windows" ]] && bin="tsunagi.exe" + cp "target/${{ matrix.target }}/release/${bin}" "$stage/" + + # The top-level README, plus the per-OS install notes and integration + # files, so the archive carries what the platform needs to set up. + cp README.md "$stage/" + case "${{ runner.os }}" in + Linux) + cp dist/linux/tsunagi.service dist/linux/install.sh "$stage/" + cp dist/linux/README.md "$stage/INSTALL.md" + ;; + macOS) + cp dist/macos/cy.hexor.tsunagi.plist "$stage/" + cp dist/macos/README.md "$stage/INSTALL.md" + ;; + Windows) + cp dist/windows/README.md "$stage/INSTALL.md" + ;; + esac + + archive="${{ matrix.asset }}-${version}.${{ matrix.archive }}" + if [[ "${{ matrix.archive }}" == "zip" ]]; then + (cd dist/pkg && 7z a "../../${archive}" "${{ matrix.asset }}") + else + tar -C dist/pkg -czf "${archive}" "${{ matrix.asset }}" + fi + + - uses: actions/upload-artifact@v4 + with: + name: ${{ matrix.asset }} + path: ${{ matrix.asset }}-*.* + if-no-files-found: error + + publish: + name: Publish release + needs: build + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/checkout@v4 + + - uses: actions/download-artifact@v4 + with: + path: artifacts + + - name: Create release and upload assets + env: + GH_TOKEN: ${{ github.token }} + RELEASE_TAG: ${{ github.ref_name }} + run: | + set -euo pipefail + gh release create "$RELEASE_TAG" artifacts/*/* \ + --title "tsunagi ${RELEASE_TAG}" \ + --generate-notes \ + --verify-tag diff --git a/dist/linux/README.md b/dist/linux/README.md new file mode 100644 index 0000000..02a0c8b --- /dev/null +++ b/dist/linux/README.md @@ -0,0 +1,62 @@ +# Running tsunagi on Linux + +This archive holds a **static** `tsunagi` binary (musl), so it runs on any +distribution — Arch, Alpine, NixOS, old glibc systems — with no shared-library +dependencies. Alongside it are a systemd unit and an installer. + +Creating the TUN interface needs `CAP_NET_ADMIN`, and configuring +systemd-resolved is decided by polkit (by user id, not capability). There are +three ways to satisfy that; pick one. + +## Quick install — systemd service as root + +```sh +sudo ./install.sh +sudo systemctl enable --now tsunagi +``` + +The service runs the agent as root, so the interface, low ports and the +resolver all just work (root is not subject to polkit). Control it **as root**, +so the client resolves the same state directory the daemon uses: + +```sh +sudo tsunagi status +sudo tsunagi join +``` + +## As your own user — one capability, no root at runtime + +```sh +sudo install -m 0755 tsunagi /usr/local/bin/tsunagi +sudo setcap cap_net_admin,cap_net_bind_service+p /usr/local/bin/tsunagi +tsunagi up +``` + +Nothing is left behind and nothing runs as root. The capability is lost on every +rebuild or copy of the binary, so re-run `setcap` after replacing it. For the +local DNS resolver, run `tsunagi dns` once: it prints the exact polkit rule that +lets your user configure systemd-resolved, ready to paste. + +## Without touching the OS + +```sh +tsunagi up --no-tun +``` + +Tunnels form and handshake between agents, but no interface, address or route is +created, so traffic never reaches the operating system. Needs no privileges. + +## What the agent installs + +Everything is tagged to its own interface, computed from local state, and +reversible — removed when a network leaves, broadcast is turned off, or the +agent stops: + +- a TUN interface configured over netlink (addresses, MTU, up); +- while broadcast is on, a `255.255.255.255` route through the interface and an + inbound-UDP firewall allowance from the overlay range (`iptables`, tagged); +- the systemd-resolved setting that sends the overlay zones' questions to the + local resolver. + +Nothing taken from a remote peer ever becomes a path, a command argument or an +OS setting. diff --git a/dist/linux/install.sh b/dist/linux/install.sh new file mode 100755 index 0000000..90749a3 --- /dev/null +++ b/dist/linux/install.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env sh +# Installs the tsunagi binary and, where systemd is present, a root service. +# Run it as root (it writes under /usr/local and /etc): +# +# sudo ./install.sh +# +# PREFIX overrides the install prefix (default /usr/local). +set -eu + +PREFIX="${PREFIX:-/usr/local}" +here="$(cd "$(dirname "$0")" && pwd)" + +if [ "$(id -u)" -ne 0 ]; then + echo "This installer writes to ${PREFIX}/bin and /etc; re-run it as root (sudo)." >&2 + exit 1 +fi + +echo "Installing tsunagi to ${PREFIX}/bin" +install -d "${PREFIX}/bin" +install -m 0755 "${here}/tsunagi" "${PREFIX}/bin/tsunagi" + +if command -v systemctl >/dev/null 2>&1; then + echo "Installing the systemd service (runs the agent as root)" + install -m 0644 "${here}/tsunagi.service" /etc/systemd/system/tsunagi.service + systemctl daemon-reload + cat < +EOF +else + cat <, take the DLL for your architecture (`amd64`), and + put it in the same folder as `tsunagi.exe`. +2. **An elevated process.** Creating the adapter requires Administrator rights. + +Open PowerShell or Command Prompt with **Run as administrator** and start the +agent there: + +```powershell +.\tsunagi.exe up +``` + +Run the commands that control it (`join`, `status`, `dns`, `network`) as the +same user with the same elevation. The agent serves its control interface on a +named pipe under `%ProgramData%\tsunagi`, so an elevated client finds it +regardless of which account started the agent. + +## Without touching the OS + +```powershell +.\tsunagi.exe up --no-tun +``` + +Tunnels form and handshake between agents, but no adapter, address or route is +created, so traffic never reaches the operating system. Needs no Wintun and no +elevation. + +## What the agent installs + +Everything is tagged to its own adapter, computed from local state, and +reversible — removed when a network leaves, broadcast is turned off, or the +agent stops: + +- a Wintun adapter configured with `netsh` (addresses, MTU); +- while broadcast is on, a `255.255.255.255` route through the adapter and an + inbound-UDP firewall allowance from the overlay range; +- the Name Resolution Policy Table entry that sends the overlay zones' questions + to the local resolver. + +Nothing taken from a remote peer ever becomes a path, a command argument or an +OS setting.