Compare commits

..
Author SHA1 Message Date
UltradesuandClaude Opus 4.8 8c812e6874 Bump the GitHub workflow actions off deprecated Node 20
Release / tsunagi-linux-aarch64 (push) Successful in 5m25s
Release / tsunagi-linux-x86_64 (push) Failing after 3h12m37s
Release / tsunagi-macos-aarch64 (push) Canceled after 0s
Release / tsunagi-windows-x86_64 (push) Canceled after 0s
Release / Publish release (push) Canceled after 0s
GitHub now forces actions/checkout@v4, actions/upload-artifact@v4 and
actions/download-artifact@v4 (all Node 20) onto Node 24 with a deprecation
warning. Move them to the current Node 24 majors — checkout@v5,
upload-artifact@v7, download-artifact@v8 — which keep the same inputs and the
per-artifact subdirectory layout (merge-multiple defaults to false), so the
publish step is unchanged.

Only the .github workflow is touched. The .gitea workflow stays on
checkout@v4 and the @v3 artifact actions, because Gitea does not support the v4+
artifact backend and may not have Node 24 on its runner image; it also does not
emit these deprecation warnings. mlugg/setup-zig has no Node 24 release yet
(latest v2.2.1 is still Node 20), so that one warning remains until upstream
updates — nothing to change on our side.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-02 14:01:33 +01:00
UltradesuandClaude Opus 4.8 63178574ce Package as a dedicated tsunagi user; add Arch packages
Release / tsunagi-linux-aarch64 (push) Successful in 2m39s
Release / tsunagi-linux-x86_64 (push) Successful in 2m32s
Release / tsunagi-macos-aarch64 (push) Canceled after 0s
Release / tsunagi-windows-x86_64 (push) Canceled after 0s
Release / Publish release (push) Canceled after 0s
Reworks the Linux packaging so the service runs as an unprivileged, dedicated
"tsunagi" user instead of root — the point of a native package is to set things
up properly for that OS's users — and adds Arch Linux packages beside the deb.
Both package families are built from the already-compiled static binary; nothing
is recompiled.

Service model (shared systemd unit, dist/linux/tsunagi.service):
- User=tsunagi with AmbientCapabilities=CAP_NET_ADMIN CAP_NET_BIND_SERVICE, so
  it creates the TUN and binds low ports without root.
- StateDirectory/CacheDirectory/RuntimeDirectory give it /var/lib/tsunagi,
  /var/cache/tsunagi and /run/tsunagi, and the agent is pointed at them (and the
  socket at /run/tsunagi/agent.sock) through TSUNAGI_* env in the unit.
- The polkit rule now grants the resolve1 actions to the "tsunagi" group; the
  service user is in it, and admins can add themselves.

CLI: when no state dir is chosen, the control commands prefer the system
service's socket at /run/tsunagi/agent.sock when it exists, so `sudo tsunagi
status`/`join` reach the packaged daemon with no flags. A per-user agent (its
socket absent from /run) still uses its own.

Packaging:
- deb (build-deb.sh): postinst now creates the tsunagi system user+group; unit
  installed to /lib/systemd/system, binary to /usr/bin.
- Arch (new dist/arch/build-pkg.sh): a dependency-free .pkg.tar.zst assembled
  with bsdtar + zstd (no makepkg, no Arch host) — .PKGINFO, a .INSTALL scriptlet
  that creates the user and reloads systemd, and a gzipped .MTREE; unit to
  /usr/lib/systemd/system.
- The polkit rule moved to dist/linux (shared by deb, Arch and the tarball), and
  the tarball install.sh now creates the user and installs the unit + rule.
- Both release workflows build the deb and the Arch package on the two Linux
  matrix entries (deb_arch/pkg_arch), install libarchive-tools+zstd for it, and
  attach both to the release (Gitea API loop / gh).

Not exercised in this session: the .deb and .pkg.tar.zst build and install only
on Linux (no dpkg-deb/bsdtar+pacman locally); CLI build, clippy, fmt and tests
pass, and all shell/YAML is syntax-checked.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-02 13:46:27 +01:00
UltradesuandClaude Opus 4.8 a3856eed3b Build a dependency-free .deb and attach it to the release
Release / tsunagi-linux-aarch64 (push) Successful in 2m21s
Release / tsunagi-linux-x86_64 (push) Successful in 2m29s
Release / tsunagi-macos-aarch64 (push) Canceled after 0s
Release / tsunagi-windows-x86_64 (push) Canceled after 0s
Release / Publish release (push) Canceled after 0s
Adds a Debian package to the release for the x86_64 and aarch64 Linux builds, so
`apt install ./tsunagi_*.deb` is a convenient path on the Ubuntu hosts. Because
the binary is static musl, the package declares no Depends — it is built with
plain dpkg-deb on the runner (no cargo-deb or nfpm), from dist/deb/build-deb.sh.

The package installs:
- the static binary at /usr/bin/tsunagi;
- a systemd service at /lib/systemd/system/tsunagi.service that runs the agent
  as root (ExecStart rewritten to /usr/bin; the source unit targets
  /usr/local/bin for the tarball installer);
- a polkit rule at /usr/share/polkit-1/rules.d granting the resolve1 actions to
  a "tsunagi" group, for running the agent as a non-root user instead of via the
  root service. postinst creates that system group and runs daemon-reload; prerm
  disables the service on removal.

Both release workflows gain a "Build the Debian package" step on the two Linux
matrix entries (deb/deb_arch), include the .deb in the uploaded run artifacts,
and attach it to the release (Gitea via the API loop, GitHub via gh). macOS and
Windows are unchanged. Service runs as root by default; the polkit rule/group is
the documented opt-in for the non-root path — say if you'd rather the service
itself run as the tsunagi user.

YAML and shell syntax validated; the .deb build itself runs only on the Linux
runner (no dpkg-deb locally) and has not been exercised in this session.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-02 13:34:33 +01:00
UltradesuandClaude Opus 4.8 eac54f8c79 Revert the GitHub workflow guard to server_url
Release / tsunagi-linux-aarch64 (push) Canceled after 15s
Release / tsunagi-linux-x86_64 (push) Canceled after 0s
Release / tsunagi-macos-aarch64 (push) Canceled after 0s
Release / tsunagi-windows-x86_64 (push) Canceled after 0s
Release / Publish release (push) Canceled after 0s
Misdiagnosed the earlier report: Gitea was correctly running its own
.gitea/workflows copy, not this .github one, so the server_url guard was fine.
The switch to a GITHUB_RELEASE repo variable then made the workflow skip on
GitHub itself, since that variable is not set there.

Put both job guards back to `github.server_url == 'https://github.com'`, which
runs on GitHub with no manual setup and skips on Gitea (its .gitea copy is the
one that runs). No repository variable is needed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-02 12:53:26 +01:00
10 changed files with 454 additions and 73 deletions

No files matched your search

+49 -7
View File
@@ -33,11 +33,17 @@ jobs:
asset: tsunagi-linux-x86_64
archive: tar.gz
zig: true
deb: true
deb_arch: amd64
pkg_arch: x86_64
- 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
- os: macos-latest
target: aarch64-apple-darwin
asset: tsunagi-macos-aarch64
@@ -63,9 +69,9 @@ jobs:
# 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
- 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
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
@@ -114,7 +120,8 @@ jobs:
cp README.md "$stage/"
case "${{ runner.os }}" in
Linux)
cp dist/linux/tsunagi.service dist/linux/install.sh "$stage/"
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)
@@ -133,13 +140,48 @@ 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"
# 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/tsunagi" \
"${{ matrix.pkg_arch }}" \
"$version" \
"tsunagi-${pkgver}-1-${{ matrix.pkg_arch }}.pkg.tar.zst"
# @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
*.pkg.tar.zst
if-no-files-found: error
publish:
@@ -211,5 +253,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' -o -name '*.pkg.tar.zst' \) -print0)
exit "$rc"
+60 -23
View File
@@ -2,18 +2,14 @@ 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: GitHub needs the v4
# artifact backend and fills the release with `gh`.
# macOS and Windows); only the publish half differs: it uses the current
# artifact actions (Node 24) and fills the release with `gh`.
#
# Gitea reads both .gitea/workflows and .github/workflows, so without a guard it
# would also try to run this file and fail on @v4 and `gh`. (Gitea reports
# github.server_url as https://github.com, so that cannot tell the two apart.)
# Instead every job is gated on a repository variable GITHUB_RELEASE=true, which
# is set only in the GitHub repository's settings — so on Gitea, where it is
# absent, these jobs are skipped and the .gitea file is the one that runs.
#
# To enable on GitHub: Settings → Secrets and variables → Actions → Variables →
# New variable, name GITHUB_RELEASE, value true.
# 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*"]
@@ -28,7 +24,7 @@ env:
jobs:
build:
name: ${{ matrix.asset }}
if: ${{ vars.GITHUB_RELEASE == 'true' }}
if: ${{ github.server_url == 'https://github.com' }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
@@ -39,11 +35,17 @@ jobs:
asset: tsunagi-linux-x86_64
archive: tar.gz
zig: true
deb: true
deb_arch: amd64
pkg_arch: x86_64
- 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
- os: macos-latest
target: aarch64-apple-darwin
asset: tsunagi-macos-aarch64
@@ -56,7 +58,7 @@ jobs:
zig: false
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- uses: dtolnay/rust-toolchain@stable
with:
@@ -69,9 +71,9 @@ jobs:
# 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
- 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
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
@@ -120,7 +122,8 @@ jobs:
cp README.md "$stage/"
case "${{ runner.os }}" in
Linux)
cp dist/linux/tsunagi.service dist/linux/install.sh "$stage/"
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)
@@ -139,23 +142,57 @@ jobs:
tar -C dist/pkg -czf "${archive}" "${{ matrix.asset }}"
fi
- name: Upload the archive as a run artifact
uses: actions/upload-artifact@v4
# 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"
# 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/tsunagi" \
"${{ matrix.pkg_arch }}" \
"$version" \
"tsunagi-${pkgver}-1-${{ matrix.pkg_arch }}.pkg.tar.zst"
- name: Upload the build outputs as run artifacts
uses: actions/upload-artifact@v7
with:
name: ${{ matrix.asset }}
path: ${{ matrix.asset }}-*.*
path: |
${{ matrix.asset }}-*.*
*.deb
*.pkg.tar.zst
if-no-files-found: error
publish:
name: Publish release
needs: build
if: ${{ vars.GITHUB_RELEASE == 'true' }}
if: ${{ github.server_url == 'https://github.com' }}
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Download every build artifact
uses: actions/download-artifact@v4
uses: actions/download-artifact@v8
with:
path: artifacts
@@ -170,9 +207,9 @@ jobs:
run: |
set -euo pipefail
# download-artifact@v4 nests each artifact under its own directory, so
# 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' \))
mapfile -t files < <(find artifacts -type f \( -name '*.tar.gz' -o -name '*.zip' -o -name '*.deb' -o -name '*.pkg.tar.zst' \))
printf 'asset: %s\n' "${files[@]}"
prerelease=()
+11
View File
@@ -761,6 +761,17 @@ fn control_socket(
if !_is_custom_state_dir {
return tsunagi::ipc::default_control_socket_path();
}
// When the user did not choose a state directory, prefer a running system
// service's socket, so `sudo tsunagi status` reaches the packaged daemon —
// which binds there via TSUNAGI_CONTROL_SOCKET — with no flags. Only when it
// is actually present, so a per-user agent still uses its own socket.
#[cfg(all(unix, not(target_os = "macos")))]
if !_is_custom_state_dir {
let system = std::path::Path::new("/run/tsunagi/agent.sock");
if system.exists() {
return system.to_path_buf();
}
}
tsunagi::ipc::control_socket_path(&paths.state_dir)
}
+52
View File
@@ -0,0 +1,52 @@
# Running tsunagi on Arch Linux
Install the package built for your architecture:
```sh
sudo pacman -U tsunagi-*-x86_64.pkg.tar.zst
```
It drops a static, dependency-free binary at `/usr/bin/tsunagi`, a systemd
service, and a polkit rule, and creates a dedicated system user `tsunagi` the
service runs as.
Start it:
```sh
sudo systemctl enable --now tsunagi
```
Control it with `sudo` (the agent's control socket is private to the service
user; `sudo` reaches it with no extra flags, because the CLI finds the running
service's socket at `/run/tsunagi/agent.sock`):
```sh
sudo tsunagi status
sudo tsunagi join <network-name> <tsn1…secret>
```
## How it runs
The service runs as the unprivileged `tsunagi` user with just two capabilities
(`CAP_NET_ADMIN`, `CAP_NET_BIND_SERVICE`) granted by systemd — no root. systemd
creates and owns its directories:
| what | path |
|---|---|
| state | `/var/lib/tsunagi` |
| cache | `/var/cache/tsunagi` |
| control socket | `/run/tsunagi/agent.sock` |
The polkit rule lets anyone in the `tsunagi` group configure systemd-resolved
for the overlay zones (the service user is in that group). To run the agent as
your own user as well, add yourself to the group: `sudo usermod -aG tsunagi
"$USER"`.
## Removal
```sh
sudo pacman -R tsunagi
```
stops and disables the service. The `tsunagi` user and `/var/lib/tsunagi` are
left in place; remove them by hand if you want them gone.
Vendored Executable
+95
View File
@@ -0,0 +1,95 @@
#!/usr/bin/env bash
# Assemble an Arch Linux package (.pkg.tar.zst) from a prebuilt static tsunagi
# binary, using only bsdtar + zstd on a non-Arch host — no makepkg, no Arch
# container. The binary is static, so the package has no dependencies.
#
# Usage: build-pkg.sh <binary> <arch> <version> <output.pkg.tar.zst>
# <binary> path to the built tsunagi executable
# <arch> pacman architecture: x86_64 or aarch64
# <version> package version (the tag without its leading v)
# <output> path to write the package to
#
# Needs bsdtar (libarchive-tools) and zstd on the builder.
set -euo pipefail
binary="$1"
arch="$2"
version="$3"
output="$4"
here="$(cd "$(dirname "$0")" && pwd)"
root="$(mktemp -d)"
trap 'rm -rf "$root"' EXIT
# pacman pkgver may not contain '-', so a prerelease tag like 0.1.0-rc.9 becomes
# 0.1.0_rc.9.
pkgver="$(printf '%s' "$version" | tr '-' '_')"
pkgrel=1
# Payload. Arch keeps units under /usr/lib/systemd/system and installs to
# /usr/bin, so the service's ExecStart is rewritten to match.
install -D -m 0755 "$binary" "$root/usr/bin/tsunagi"
mkdir -p "$root/usr/lib/systemd/system"
sed 's#/usr/local/bin/tsunagi#/usr/bin/tsunagi#' \
"$here/../linux/tsunagi.service" > "$root/usr/lib/systemd/system/tsunagi.service"
chmod 0644 "$root/usr/lib/systemd/system/tsunagi.service"
install -D -m 0644 "$here/../linux/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"
# Installed size (payload only), before the metadata files are written.
size="$(du -sb "$root" | cut -f1)"
cat > "$root/.PKGINFO" <<PKGINFO
pkgname = tsunagi
pkgver = ${pkgver}-${pkgrel}
pkgdesc = Serverless private mesh networking agent
url = https://github.com/Ultradesu/tsunagi
builddate = $(date +%s)
packager = AB <ab@hexor.cy>
size = ${size}
arch = ${arch}
license = MIT
license = Apache
PKGINFO
# Scriptlet: create the dedicated system user the service runs as, and keep
# systemd in step. The "tsunagi" user is in the "tsunagi" group, which the
# polkit rule grants the resolver actions.
cat > "$root/.INSTALL" <<'INSTALL'
post_install() {
getent group tsunagi >/dev/null 2>&1 || groupadd -r tsunagi
getent passwd tsunagi >/dev/null 2>&1 || \
useradd -r -g tsunagi -d /var/lib/tsunagi -s /usr/bin/nologin -c "tsunagi agent" tsunagi
systemctl daemon-reload >/dev/null 2>&1 || true
}
post_upgrade() {
post_install
}
pre_remove() {
systemctl disable --now tsunagi.service >/dev/null 2>&1 || true
}
post_remove() {
systemctl daemon-reload >/dev/null 2>&1 || true
}
INSTALL
# .MTREE over the metadata and payload (not over itself), gzip-compressed, as
# pacman expects.
(
cd "$root"
LANG=C bsdtar -czf .MTREE --format=mtree \
--options='!all,use-set,type,uid,gid,mode,time,size,md5,sha256,link' \
.PKGINFO .INSTALL usr
)
# Pack with .PKGINFO first, then the rest, and zstd-compress.
(
cd "$root"
bsdtar -cf - .PKGINFO .MTREE .INSTALL usr
) | zstd -c -19 -T0 > "$output"
echo "built $output"
Vendored Executable
+98
View File
@@ -0,0 +1,98 @@
#!/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 <binary> <deb_arch> <version> <output.deb>
# <binary> path to the built tsunagi executable
# <deb_arch> Debian architecture: amd64 or arm64
# <version> package version (the tag without its leading v)
# <output> 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/../linux/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" <<CTRL
Package: tsunagi
Version: ${version}
Architecture: ${arch}
Maintainer: AB <ab@hexor.cy>
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 a dedicated
unprivileged "tsunagi" user with only CAP_NET_ADMIN and CAP_NET_BIND_SERVICE,
and a polkit rule for the local DNS resolver.
CTRL
# The dedicated system user the service runs as. It is in the "tsunagi" group,
# which the polkit rule grants the resolve1 actions. systemd creates the state,
# cache and runtime directories for it on first start.
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 ! getent passwd tsunagi >/dev/null 2>&1; then
adduser --system --ingroup tsunagi --home /var/lib/tsunagi --no-create-home \
--gecos "tsunagi agent" --disabled-login 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"
+23
View File
@@ -0,0 +1,23 @@
// Installed by the tsunagi packages.
//
// polkit decides who may configure systemd-resolved by user/group id, not by
// capability, so the agent needs this even though it holds CAP_NET_ADMIN. It
// grants exactly the four resolve1 actions the agent calls, and nothing else,
// to anyone in the "tsunagi" group.
//
// The packaged service runs as the "tsunagi" user (which is in that group), so
// its resolver configuration is allowed. To also run the agent as your own
// user, add yourself to the group:
//
// sudo usermod -aG tsunagi "$USER"
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;
}
});
+17 -12
View File
@@ -2,40 +2,45 @@
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.
dependencies. Alongside it are a systemd unit, a polkit rule, 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.
systemd-resolved is decided by polkit. There are three ways to run it; pick one.
## Quick install — systemd service as root
> On Debian/Ubuntu and Arch, prefer the native package (`.deb` / `.pkg.tar.zst`)
> from the release — it does all of the below for you.
## Quick install — systemd service as a dedicated user
```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:
The service runs as an unprivileged `tsunagi` user with just `CAP_NET_ADMIN`
and `CAP_NET_BIND_SERVICE` (granted by systemd), not root. systemd creates its
state (`/var/lib/tsunagi`), cache (`/var/cache/tsunagi`) and runtime
(`/run/tsunagi`) directories, and the polkit rule lets it configure the
resolver. Control it with `sudo` — the CLI finds the running service's socket at
`/run/tsunagi/agent.sock` with no flags:
```sh
sudo tsunagi status
sudo tsunagi join <network-name> <tsn1…secret>
```
## As your own user — one capability, no root at runtime
## As your own user — one capability, no service
```sh
sudo install -m 0755 tsunagi /usr/local/bin/tsunagi
sudo setcap cap_net_admin,cap_net_bind_service+p /usr/local/bin/tsunagi
sudo usermod -aG tsunagi "$USER" # so the resolver works; log in again after
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.
Nothing runs as root. The capability is lost on every rebuild or copy of the
binary, so re-run `setcap` after replacing it. The agent uses your per-user
state directory and socket, so plain `tsunagi status` (no sudo) reaches it.
## Without touching the OS
+27 -19
View File
@@ -1,10 +1,11 @@
#!/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):
# Installs the static tsunagi binary and, where systemd is present, a service
# that runs the agent as a dedicated unprivileged "tsunagi" user. Run as root:
#
# sudo ./install.sh
#
# PREFIX overrides the install prefix (default /usr/local).
# PREFIX overrides the install prefix (default /usr/local); if you change it,
# edit ExecStart in tsunagi.service to match.
set -eu
PREFIX="${PREFIX:-/usr/local}"
@@ -19,8 +20,23 @@ echo "Installing tsunagi to ${PREFIX}/bin"
install -d "${PREFIX}/bin"
install -m 0755 "${here}/tsunagi" "${PREFIX}/bin/tsunagi"
# The dedicated system user the service runs as (works with either shadow's
# useradd or Debian's adduser).
if ! getent group tsunagi >/dev/null 2>&1; then
groupadd -r tsunagi 2>/dev/null || addgroup --system tsunagi 2>/dev/null || true
fi
if ! getent passwd tsunagi >/dev/null 2>&1; then
useradd -r -g tsunagi -d /var/lib/tsunagi -s /usr/sbin/nologin -c "tsunagi agent" tsunagi 2>/dev/null \
|| adduser --system --ingroup tsunagi --home /var/lib/tsunagi --no-create-home \
--gecos "tsunagi agent" --disabled-login tsunagi 2>/dev/null || true
fi
# polkit rule so the agent (as the tsunagi user) may configure systemd-resolved.
install -d /etc/polkit-1/rules.d
install -m 0644 "${here}/50-tsunagi-resolved.rules" /etc/polkit-1/rules.d/50-tsunagi-resolved.rules
if command -v systemctl >/dev/null 2>&1; then
echo "Installing the systemd service (runs the agent as root)"
echo "Installing the systemd service (runs as the tsunagi user)"
install -m 0644 "${here}/tsunagi.service" /etc/systemd/system/tsunagi.service
systemctl daemon-reload
cat <<EOF
@@ -29,7 +45,7 @@ Done. Start the agent with:
sudo systemctl enable --now tsunagi
Control it as root, so the client shares the agent's state directory:
Control it with sudo (the CLI finds the service's socket automatically):
sudo tsunagi status
sudo tsunagi join <network-name> <tsn1…secret>
@@ -39,19 +55,11 @@ else
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.
* run it as root: sudo ${PREFIX}/bin/tsunagi up
* or as your own user: sudo setcap cap_net_admin,cap_net_bind_service+p ${PREFIX}/bin/tsunagi
tsunagi up
(add yourself to the tsunagi group so the resolver works:
sudo usermod -aG tsunagi "\$USER")
* or without touching the OS: tsunagi up --no-tun
EOF
fi
+22 -12
View File
@@ -6,21 +6,31 @@ Wants=network-online.target
[Service]
Type=simple
# Runs as a dedicated unprivileged user, not root. The two capabilities are all
# it needs: CAP_NET_ADMIN to create and configure the TUN interface, and
# CAP_NET_BIND_SERVICE for low ports. The packages create the `tsunagi` user.
User=tsunagi
Group=tsunagi
AmbientCapabilities=CAP_NET_ADMIN CAP_NET_BIND_SERVICE
CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_BIND_SERVICE
# systemd creates and owns these for the service user, and the agent is pointed
# at them: state in /var/lib/tsunagi, cache in /var/cache/tsunagi, and the
# control socket in /run/tsunagi, which stays private to the service user.
StateDirectory=tsunagi
StateDirectoryMode=0700
CacheDirectory=tsunagi
RuntimeDirectory=tsunagi
RuntimeDirectoryMode=0755
Environment=TSUNAGI_STATE_DIR=%S/tsunagi
Environment=TSUNAGI_CACHE_DIR=%C/tsunagi
Environment=TSUNAGI_CONTROL_SOCKET=%t/tsunagi/agent.sock
# The tarball installer puts the binary here; the deb and the Arch package
# rewrite this to /usr/bin where they install it.
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