From 63178574cecc684f5754732acd6889212e68b2e6 Mon Sep 17 00:00:00 2001 From: Ultradesu Date: Fri, 2 Oct 2026 13:46:27 +0100 Subject: [PATCH] Package as a dedicated tsunagi user; add Arch packages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .gitea/workflows/release.yml | 28 ++++++-- .github/workflows/release.yml | 28 ++++++-- crates/tsunagi-cli/src/main.rs | 11 ++++ dist/arch/README.md | 52 +++++++++++++++ dist/arch/build-pkg.sh | 95 ++++++++++++++++++++++++++++ dist/deb/50-tsunagi-resolved.rules | 25 -------- dist/deb/build-deb.sh | 16 +++-- dist/linux/50-tsunagi-resolved.rules | 23 +++++++ dist/linux/README.md | 29 +++++---- dist/linux/install.sh | 46 ++++++++------ dist/linux/tsunagi.service | 34 ++++++---- 11 files changed, 306 insertions(+), 81 deletions(-) create mode 100644 dist/arch/README.md create mode 100755 dist/arch/build-pkg.sh delete mode 100644 dist/deb/50-tsunagi-resolved.rules create mode 100644 dist/linux/50-tsunagi-resolved.rules diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml index 39aa0d5..f7c69fd 100644 --- a/.gitea/workflows/release.yml +++ b/.gitea/workflows/release.yml @@ -35,6 +35,7 @@ jobs: zig: true deb: true deb_arch: amd64 + pkg_arch: x86_64 - os: ubuntu-latest target: aarch64-unknown-linux-musl asset: tsunagi-linux-aarch64 @@ -42,6 +43,7 @@ jobs: zig: true deb: true deb_arch: arm64 + pkg_arch: aarch64 - os: macos-latest target: aarch64-apple-darwin asset: tsunagi-macos-aarch64 @@ -67,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 @@ -118,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) @@ -152,6 +155,22 @@ jobs: "$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 (and the .deb, where built) show up under the run # and hands it to `publish`. @@ -162,6 +181,7 @@ jobs: path: | ${{ matrix.asset }}-*.* *.deb + *.pkg.tar.zst if-no-files-found: error publish: @@ -233,5 +253,5 @@ jobs: cat up.json >&2 || true rc=1 fi - done < <(find artifacts -type f \( -name '*.tar.gz' -o -name '*.zip' -o -name '*.deb' \) -print0) + done < <(find artifacts -type f \( -name '*.tar.gz' -o -name '*.zip' -o -name '*.deb' -o -name '*.pkg.tar.zst' \) -print0) exit "$rc" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6cd1c38..4bcb676 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -36,6 +36,7 @@ jobs: zig: true deb: true deb_arch: amd64 + pkg_arch: x86_64 - os: ubuntu-latest target: aarch64-unknown-linux-musl asset: tsunagi-linux-aarch64 @@ -43,6 +44,7 @@ jobs: zig: true deb: true deb_arch: arm64 + pkg_arch: aarch64 - os: macos-latest target: aarch64-apple-darwin asset: tsunagi-macos-aarch64 @@ -68,9 +70,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 @@ -119,7 +121,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) @@ -153,6 +156,22 @@ jobs: "$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@v4 with: @@ -160,6 +179,7 @@ jobs: path: | ${{ matrix.asset }}-*.* *.deb + *.pkg.tar.zst if-no-files-found: error publish: @@ -188,7 +208,7 @@ jobs: # download-artifact@v4 nests each artifact under its own directory, so # the archives are artifacts//. - mapfile -t files < <(find artifacts -type f \( -name '*.tar.gz' -o -name '*.zip' -o -name '*.deb' \)) + 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=() diff --git a/crates/tsunagi-cli/src/main.rs b/crates/tsunagi-cli/src/main.rs index f926293..e8f4e4b 100644 --- a/crates/tsunagi-cli/src/main.rs +++ b/crates/tsunagi-cli/src/main.rs @@ -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) } diff --git a/dist/arch/README.md b/dist/arch/README.md new file mode 100644 index 0000000..b67edd1 --- /dev/null +++ b/dist/arch/README.md @@ -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 +``` + +## 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. diff --git a/dist/arch/build-pkg.sh b/dist/arch/build-pkg.sh new file mode 100755 index 0000000..8c0153c --- /dev/null +++ b/dist/arch/build-pkg.sh @@ -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 +# path to the built tsunagi executable +# pacman architecture: x86_64 or aarch64 +# package version (the tag without its leading v) +# 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" < +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" diff --git a/dist/deb/50-tsunagi-resolved.rules b/dist/deb/50-tsunagi-resolved.rules deleted file mode 100644 index 00ae4d7..0000000 --- a/dist/deb/50-tsunagi-resolved.rules +++ /dev/null @@ -1,25 +0,0 @@ -// Installed by the tsunagi package. -// -// Lets a non-root agent configure systemd-resolved for its overlay zones — the -// four resolve1 actions it calls, and nothing else — for any user in the -// "tsunagi" group (created by the package). The packaged systemd service runs -// as root and does not need this rule; it is for running the agent as your own -// user instead: -// -// sudo usermod -aG tsunagi "$USER" -// sudo setcap cap_net_admin,cap_net_bind_service+p /usr/bin/tsunagi -// tsunagi up -// -// polkit decides by user/group id, not by capability, which is why the resolver -// needs this rule even once the binary has the capability. -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; - } -}); diff --git a/dist/deb/build-deb.sh b/dist/deb/build-deb.sh index a38a2bc..9e44b51 100755 --- a/dist/deb/build-deb.sh +++ b/dist/deb/build-deb.sh @@ -29,7 +29,7 @@ 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/50-tsunagi-resolved.rules" \ +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" @@ -47,18 +47,24 @@ 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 root, and a - polkit rule that lets a non-root agent configure systemd-resolved. + 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 -# A system group for the optional non-root run (the polkit rule grants it the -# resolve1 actions). The packaged service runs as root and does not need it. +# 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 diff --git a/dist/linux/50-tsunagi-resolved.rules b/dist/linux/50-tsunagi-resolved.rules new file mode 100644 index 0000000..0168255 --- /dev/null +++ b/dist/linux/50-tsunagi-resolved.rules @@ -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; + } +}); diff --git a/dist/linux/README.md b/dist/linux/README.md index 02a0c8b..2a77bb6 100644 --- a/dist/linux/README.md +++ b/dist/linux/README.md @@ -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 ``` -## 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 diff --git a/dist/linux/install.sh b/dist/linux/install.sh index 90749a3..f60a8c9 100755 --- a/dist/linux/install.sh +++ b/dist/linux/install.sh @@ -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 < @@ -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 diff --git a/dist/linux/tsunagi.service b/dist/linux/tsunagi.service index d8c02f2..57b5a8c 100644 --- a/dist/linux/tsunagi.service +++ b/dist/linux/tsunagi.service @@ -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