Running on Windows needed wintun.dll by hand, an elevated console and a
manually started agent; without the DLL the agent silently ran with no
interface (and so no exit node or routing at all).
`tsng service` (hidden) runs the same `up` under the Windows service manager:
src/winservice.rs registers with the SCM through the windows-service crate,
reports Running/StopPending/Stopped, and maps a stop request onto the existing
shutdown path (stop_signal). The only unsafe is the extern "system" entry point
that define_windows_service! expands to, allowed in that module alone. A
service has no console, so it logs to %ProgramData%\Tsunagi\logs\tsng.log
(one older file kept past 8 MiB). Its state defaults to
%ProgramData%\Tsunagi\{data,cache} and its control socket to the system-wide
default every client already uses, so an unelevated `tsng status` and the tray
find it.
dist/windows/tsunagi.iss (Inno Setup) installs tsng.exe, the tray and Wintun
0.14.1 (pinned by SHA-256 in CI, licence text shipped), registers the
"Tsunagi" service as LocalSystem with auto start and restart-on-failure, locks
the data/cache/log directories to SYSTEM and Administrators by SID, and by
tasks adds tsng to the PATH, an inbound firewall rule for the agent and a tray
autostart. It stops the service, the tray and any hand-started agent before
replacing files; uninstall removes the service, rule and PATH entry and asks
before deleting the data (never on a silent uninstall). The release workflows
(GitHub and Gitea) build it on the Windows job and publish
tsunagi-setup-<version>-x86_64.exe.
.cargo/config.toml links the MSVC C runtime statically so nothing needs a
Visual C++ redistributable on a clean machine.
Compatibility: an agent run by hand keeps its data in the user profile; the
service has its own, so networks must be joined again once.
Checks: cargo fmt --check, clippy -D warnings, cargo test --workspace passed on
macOS (one unrelated wireguard restart test failed once under load and passed
on three reruns). winservice.rs was cargo-checked for x86_64-pc-windows-msvc in
a scratch crate; the whole tsunagi crate cannot be built for Windows from
macOS (aws-lc needs the Windows SDK). The .iss script and the CI step were NOT
compiled or run: no Inno Setup or Windows here.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
323 lines
12 KiB
YAML
323 lines
12 KiB
YAML
name: Release
|
|
|
|
# GitHub version of the release pipeline. The build half mirrors the Gitea one
|
|
# in .gitea/workflows/release.yml (static musl Linux via cargo-zigbuild, native
|
|
# macOS and Windows); only the publish half differs: it uses the current
|
|
# artifact actions (Node 24) and fills the release with `gh`.
|
|
#
|
|
# Every job is gated on `github.server_url == 'https://github.com'`, so this file
|
|
# runs only on GitHub. Gitea runs its own copy in .gitea/workflows (which pins
|
|
# the older @v3 artifact actions it supports); should a Gitea instance also read
|
|
# .github/workflows, the guard skips these jobs there so they never fail on the
|
|
# artifact backend or `gh`.
|
|
on:
|
|
push:
|
|
tags: ["v*"]
|
|
|
|
permissions:
|
|
contents: write
|
|
|
|
env:
|
|
CARGO_TERM_COLOR: always
|
|
CARGO_NET_RETRY: "10"
|
|
|
|
jobs:
|
|
build:
|
|
name: ${{ matrix.asset }}
|
|
if: ${{ github.server_url == 'https://github.com' }}
|
|
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
|
|
deb: true
|
|
deb_arch: amd64
|
|
pkg_arch: x86_64
|
|
gnu_target: x86_64-unknown-linux-gnu
|
|
- os: ubuntu-latest
|
|
target: aarch64-unknown-linux-musl
|
|
asset: tsunagi-linux-aarch64
|
|
archive: tar.gz
|
|
zig: true
|
|
deb: true
|
|
deb_arch: arm64
|
|
pkg_arch: aarch64
|
|
# 32-bit ARM with hardware float (ARMv7, `v7l`): single-board and edge
|
|
# devices. Statically linked like the others.
|
|
- os: ubuntu-latest
|
|
target: armv7-unknown-linux-musleabihf
|
|
asset: tsunagi-linux-armv7
|
|
archive: tar.gz
|
|
zig: true
|
|
deb: true
|
|
deb_arch: armhf
|
|
pkg_arch: armv7h
|
|
- 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@v5
|
|
|
|
- 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 and packaging tooling
|
|
if: matrix.zig
|
|
run: sudo apt-get update && sudo apt-get install -y --no-install-recommends cmake clang libclang-dev libarchive-tools zstd
|
|
- name: Install zig
|
|
if: matrix.zig
|
|
uses: mlugg/setup-zig@v2
|
|
with:
|
|
# Pin a real, archived Zig release. Left unpinned the action resolves
|
|
# to the newest version it knows of (a yet-unreleased 0.16.0-dev),
|
|
# whose tarball 404s on ziglang.org while the community mirrors flake.
|
|
version: 0.14.1
|
|
- 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
|
|
# zig's linker ignores the `-O1` GNU-ld optimization flag Rust passes
|
|
# and says so on stderr, which rustc now surfaces as a linker_messages
|
|
# warning. It is cosmetic — the binary is correct — so quiet just that
|
|
# one lint to keep the musl build log clean. A real link error still
|
|
# fails the build.
|
|
export RUSTFLAGS="${RUSTFLAGS:-} -A linker_messages"
|
|
cargo zigbuild --release --locked --target "${{ matrix.target }}" --bin tsng
|
|
else
|
|
cargo build --release --locked --target "${{ matrix.target }}" --bin tsng
|
|
fi
|
|
|
|
# The tray GUI. On Linux it is built against glibc (an old baseline for
|
|
# portability), not musl: a GUI must dlopen libGL/X11/Wayland at runtime,
|
|
# which a static musl binary cannot. macOS and Windows build it natively.
|
|
- name: Build the tray GUI
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
if [[ "${{ runner.os }}" == "Linux" ]]; then
|
|
if [[ -n "${{ matrix.gnu_target }}" ]]; then
|
|
# tray-icon links GTK and libappindicator through pkg-config, which
|
|
# has no cross story under zig, so the Linux GUI is built natively
|
|
# (x86_64 only) against the runner's glibc.
|
|
sudo apt-get update
|
|
sudo apt-get install -y --no-install-recommends \
|
|
libgtk-3-dev libayatana-appindicator3-dev libxdo-dev
|
|
cargo build --release --locked \
|
|
--target "${{ matrix.gnu_target }}" --bin tsunagi-tray
|
|
else
|
|
echo "no GUI build for ${{ matrix.target }}"
|
|
fi
|
|
else
|
|
cargo build --release --locked --target "${{ matrix.target }}" --bin tsunagi-tray
|
|
fi
|
|
|
|
- name: Package
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
version="${GITHUB_REF_NAME#v}"
|
|
stage="dist/pkg/${{ matrix.asset }}"
|
|
mkdir -p "$stage"
|
|
|
|
bin="tsng"
|
|
[[ "${{ runner.os }}" == "Windows" ]] && bin="tsng.exe"
|
|
cp "target/${{ matrix.target }}/release/${bin}" "$stage/"
|
|
|
|
# The tray GUI alongside it, where it was built (Linux: x86_64 only).
|
|
tray="tsunagi-tray"
|
|
[[ "${{ runner.os }}" == "Windows" ]] && tray="tsunagi-tray.exe"
|
|
if [[ "${{ runner.os }}" == "Linux" ]]; then
|
|
if [[ -n "${{ matrix.gnu_target }}" ]]; then
|
|
cp "target/${{ matrix.gnu_target }}/release/${tray}" "$stage/"
|
|
fi
|
|
else
|
|
cp "target/${{ matrix.target }}/release/${tray}" "$stage/"
|
|
fi
|
|
|
|
# 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 \
|
|
dist/linux/50-tsunagi-resolved.rules "$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"
|
|
# The tray as an application bundle, for the Homebrew cask.
|
|
bash dist/macos/build-app.sh \
|
|
"target/${{ matrix.target }}/release/tsunagi-tray" \
|
|
"$version" "$stage/Tsunagi.app"
|
|
;;
|
|
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
|
|
|
|
# The Windows installer: the agent as a service, Wintun, the tray and the
|
|
# PATH entry in one setup program. Wintun is the signed library from
|
|
# wintun.net, pinned by hash so a changed download fails the build.
|
|
- name: Build the Windows installer
|
|
if: runner.os == 'Windows'
|
|
shell: pwsh
|
|
run: |
|
|
$ErrorActionPreference = 'Stop'
|
|
$version = $env:GITHUB_REF_NAME.TrimStart('v')
|
|
Invoke-WebRequest -Uri https://www.wintun.net/builds/wintun-0.14.1.zip -OutFile wintun.zip
|
|
$hash = (Get-FileHash wintun.zip -Algorithm SHA256).Hash
|
|
if ($hash -ne '07C256185D6EE3652E09FA55C0B673E2624B565E02C4B9091C79CA7D2F24EF51') {
|
|
throw "wintun.zip has an unexpected hash: $hash"
|
|
}
|
|
Expand-Archive wintun.zip -DestinationPath wintun-dl
|
|
choco install innosetup -y --no-progress
|
|
$iscc = Join-Path ${env:ProgramFiles(x86)} 'Inno Setup 6\ISCC.exe'
|
|
$stage = Join-Path $PWD 'dist/pkg/${{ matrix.asset }}'
|
|
& $iscc "/DAppVersion=$version" "/DSourceDir=$stage" `
|
|
"/DWintunDir=$(Join-Path $PWD 'wintun-dl/wintun/bin/amd64')" `
|
|
"/O$PWD" dist/windows/tsunagi.iss
|
|
if ($LASTEXITCODE -ne 0) { throw "ISCC failed with $LASTEXITCODE" }
|
|
|
|
# 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/tsng" \
|
|
"${{ matrix.deb_arch }}" \
|
|
"$version" \
|
|
"tsunagi_${version}_${{ matrix.deb_arch }}.deb"
|
|
|
|
# A dependency-free Arch package (.pkg.tar.zst), assembled with bsdtar and
|
|
# zstd (no makepkg); same binary, service and polkit rule. See
|
|
# dist/arch/build-pkg.sh.
|
|
- name: Build the Arch package
|
|
if: matrix.pkg_arch
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
version="${GITHUB_REF_NAME#v}"
|
|
pkgver="${version//-/_}"
|
|
bash dist/arch/build-pkg.sh \
|
|
"target/${{ matrix.target }}/release/tsng" \
|
|
"${{ matrix.pkg_arch }}" \
|
|
"$version" \
|
|
"tsunagi-${pkgver}-1-${{ matrix.pkg_arch }}.pkg.tar.zst"
|
|
|
|
# The GUI variants: the same agent plus the tray binary and a desktop
|
|
# launcher, named tsunagi-gui (conflicting with the headless packages).
|
|
- name: Build the GUI packages
|
|
if: matrix.gnu_target
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
version="${GITHUB_REF_NAME#v}"
|
|
pkgver="${version//-/_}"
|
|
tray="target/${{ matrix.gnu_target }}/release/tsunagi-tray"
|
|
bash dist/deb/build-deb.sh \
|
|
"target/${{ matrix.target }}/release/tsng" \
|
|
"${{ matrix.deb_arch }}" "$version" \
|
|
"tsunagi-gui_${version}_${{ matrix.deb_arch }}.deb" "$tray"
|
|
bash dist/arch/build-pkg.sh \
|
|
"target/${{ matrix.target }}/release/tsng" \
|
|
"${{ matrix.pkg_arch }}" "$version" \
|
|
"tsunagi-gui-${pkgver}-1-${{ matrix.pkg_arch }}.pkg.tar.zst" "$tray"
|
|
|
|
- name: Upload the build outputs as run artifacts
|
|
uses: actions/upload-artifact@v7
|
|
with:
|
|
name: ${{ matrix.asset }}
|
|
path: |
|
|
${{ matrix.asset }}-*.*
|
|
*.deb
|
|
*.pkg.tar.zst
|
|
tsunagi-setup-*.exe
|
|
if-no-files-found: error
|
|
|
|
publish:
|
|
name: Publish release
|
|
needs: build
|
|
if: ${{ github.server_url == 'https://github.com' }}
|
|
runs-on: ubuntu-latest
|
|
permissions:
|
|
contents: write
|
|
steps:
|
|
- name: Download every build artifact
|
|
uses: actions/download-artifact@v8
|
|
with:
|
|
path: artifacts
|
|
|
|
- name: Show what was collected
|
|
run: find artifacts -type f
|
|
|
|
- name: Create the release and upload the archives
|
|
shell: bash
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
TAG: ${{ github.ref_name }}
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
# download-artifact nests each artifact under its own directory, so
|
|
# the archives are artifacts/<asset>/<file>.
|
|
mapfile -t files < <(find artifacts -type f \( -name '*.tar.gz' -o -name '*.zip' -o -name '*.deb' -o -name '*.pkg.tar.zst' -o -name 'tsunagi-setup-*.exe' \))
|
|
printf 'asset: %s\n' "${files[@]}"
|
|
|
|
prerelease=()
|
|
case "$TAG" in *-*) prerelease=(--prerelease) ;; esac
|
|
|
|
# Create the release on the first run; on a re-run, add/replace assets
|
|
# on the release that is already there.
|
|
if gh release view "$TAG" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
|
|
gh release upload "$TAG" "${files[@]}" --repo "$GITHUB_REPOSITORY" --clobber
|
|
else
|
|
gh release create "$TAG" "${files[@]}" \
|
|
--repo "$GITHUB_REPOSITORY" \
|
|
--title "tsunagi $TAG" \
|
|
--generate-notes \
|
|
--verify-tag \
|
|
"${prerelease[@]}"
|
|
fi
|