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.