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 <noreply@anthropic.com>
This commit is contained in:
UltradesuandClaude Opus 4.8 committed 2026-10-02 11:48:19 +01:00
1 parent da7ce1dca1
commit a826b9d40e
5 files changed
+339

No files matched your search

+148
View File
@@ -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
+62
View File
@@ -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 <network-name> <tsn1…secret>
```
## 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.
Vendored Executable
+57
View File
@@ -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
Done. Start the agent with:
sudo systemctl enable --now tsunagi
Control it as root, so the client shares the agent's state directory:
sudo tsunagi status
sudo tsunagi join <network-name> <tsn1…secret>
EOF
else
cat <<EOF
Installed the binary; no systemd found, so no service was set up. Either:
* run it directly as root:
sudo tsunagi up
* or grant the capability and run it as your own user (no root at runtime):
sudo setcap cap_net_admin,cap_net_bind_service+p ${PREFIX}/bin/tsunagi
tsunagi up
For the local DNS resolver as a non-root user, run \`tsunagi dns\` once; it
prints the polkit rule that lets your user configure systemd-resolved.
* or skip the interface entirely with \`tsunagi up --no-tun\`: tunnels still
form, they just do not reach the operating system.
EOF
fi
+26
View File
@@ -0,0 +1,26 @@
[Unit]
Description=tsunagi mesh agent
Documentation=https://github.com/Ultradesu/tsunagi
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
ExecStart=/usr/local/bin/tsunagi up
Restart=on-failure
RestartSec=5
# This unit runs the agent as root, which covers everything it needs: creating
# the TUN interface (CAP_NET_ADMIN), binding low ports, and configuring
# systemd-resolved — root is not subject to polkit, so no polkit rule is needed.
#
# To run as a non-root user instead, uncomment the three lines below, set a
# User=, and install the polkit rule that `tsunagi dns` prints (resolved decides
# by user id, not capability, so the rule is what lets a non-root agent set the
# resolver):
#User=tsunagi
#AmbientCapabilities=CAP_NET_ADMIN CAP_NET_BIND_SERVICE
#CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_BIND_SERVICE
[Install]
WantedBy=multi-user.target
+46
View File
@@ -0,0 +1,46 @@
# Running tsunagi on Windows
This archive holds `tsunagi.exe`. The overlay interface is a Wintun adapter, so
the agent needs two things to carry real traffic:
1. **`wintun.dll` beside the executable.** Download it from
<https://www.wintun.net>, 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.