Linux support: systemd daemon, deb/AppImage bundles and an Arch package - #8
Merged
Conversation
Linux built and tested in CI but could not host a detached daemon: --socket was rejected everywhere but macOS, and the peer check admitted every caller unconditionally because no listener could exist to authenticate against. Give Linux the same shape macOS has. The unix-socket listener and the symlink-safe, fail-closed data-directory clamp are now shared between the two rather than copied, with only the paths differing: /run/tenebra.sock and /var/lib/tenebra/data. Peer credentials come from SO_PEERCRED, and the console user is resolved from logind's seat state, refusing to guess when two seats disagree and failing open the way darwin does. Resolving the bundled sing-box needed its own answer here. Windows and macOS ship one self-contained directory; a distribution package spreads the payload across the hierarchy, so the search walks the install prefix and then /usr, under both the lowercase package name and the product name the Debian bundle uses, with and without a resources/ subdirectory. The rule-sets follow the same list, so the binary and its .srs can never drift apart. The tunnel supervisor is its own adapter instead of the generic one: reusing the Windows adapter meant every diagnostic on an Arch desktop was prefixed 'windows:', and the privilege hints pointed at a service that does not exist there.
Adds the pieces a Linux install actually needs: the sing-box fetch generalised to linux/amd64 and arm64 behind the same SHA-256 pins the other two platforms use, a systemd unit, install and uninstall scripts, and an Arch package. The unit runs the core as root with a capability set of exactly six, an explicit DeviceAllow for /dev/net/tun and a syscall filter. The sandboxing stops where it would break a VPN: ProtectSystem is full rather than strict so /run stays writable, PrivateDevices is off because it would take the tun device with it, and the kernel-tunable protections are off because routing writes to /proc/sys. TENEBRA_CONFIG_DIR is deliberately left unset so the core still pins and clamps its own root-only store instead of quietly inheriting one. Arch gets a PKGBUILD rather than a bundler target because Tauri has none: its config schema knows deb, rpm and appimage and nothing else. sing-box is bundled with a pinned digest instead of declared as a dependency — there is no sing-box in core or extra, only in the AUR, and a package may not depend on one. It goes into the package's own directory so an existing AUR install is left alone. pkgver is wired into set-version.mjs: it doubles as the git tag the source is taken from, so a stale copy would not merely mislabel the package, it would build the wrong revision or fail on a tag that does not exist.
CI gains a job that builds the deb and runs the Rust suite on Linux, and a cheap one that parses and lints the PKGBUILD on every push — a full makepkg only works on a tagged commit, since the package takes its source from the tag. The release workflow gains Linux as the third link in the existing chain. The sequencing is not cosmetic: all the jobs upload to one release and tauri-action merges its platform entries into that release's latest.json, so running them in order keeps that read-modify-write deterministic. A separate job then builds the pacman package inside an Arch container, proves it installs and that the layout the daemon resolves against is really there, and attaches it to the release. Both Linux jobs pin ubuntu-22.04 rather than ubuntu-latest: the artifacts link against the runner's glibc, and anything built on 24.04 refuses to start on an older distribution. The release notes are identical in all three jobs now — each one rewrites the body, so unsynchronised copies meant the published text depended on which job finished last. They say plainly that only the Arch package installs the service, and that a packaged install updates through the package manager rather than the in-app updater, which can only replace an AppImage.
The unix-socket backend now covers Linux as well as macOS, carrying over the patient first dial and the late-daemon watch added in 0.4.5 rather than reverting to the naive version: on Linux the service and the desktop session start concurrently, which is exactly the race that shape exists for. The liveness probe reads /proc/net/unix instead of dialling, because a dial would displace whichever client currently holds the session. Bundles: deb and appimage, with a platform config so the Linux build carries sing-box and the rule-sets under their real names and never the wintun payload. The desktop entry is the bundler's own template plus a %u field code on Exec — the MimeType line is filled in from the deep-link schemes, so without it the desktop environment would hand a tenebra:// link to a launcher that drops it. Deep-link registration also had to stop being debug-only: an AppImage has no installer to claim the scheme, so a release build never registered as a handler. Two Linux-only holes closed on the way. The crash and core logs were written to /tmp, which is world-writable — a pre-planted symlink there would have redirected an append-only writer; they now follow XDG. And the updater is gated: a package-managed install skips the check and says where updates come from, instead of offering a button that cannot work, since the Tauri updater can only replace an AppImage.
The pattern for the gomobile leftover binary was unanchored, so it matched the cmd/tenebra-core directory as well. Files already tracked stayed tracked, which hid the problem completely: every new source file added there since was quietly dropped from its commit, and only CI noticed, with the linux and darwin builds failing on an undefined function whose definition was sitting in the working tree. Anchor the pattern and add the files it swallowed.
Run the bundle build from the front-end directory. Without it npm looked for a package.json in the repository root and the Linux job failed after five minutes of work that had already succeeded.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Linux was previously a compile target only: the Go core built and was tested in
CI, but
--socketwas rejected off macOS, so there was no way to run a detacheddaemon, and no bundle was produced for any distribution.
This branch makes Linux a first-class desktop platform, with Arch as the primary
target.
What is here
symlink-safe data-directory clamp are now shared with macOS instead of copied;
Linux serves on
/run/tenebra.sockand stores under/var/lib/tenebra/data.Peer credentials come from
SO_PEERCRED, and the console user is resolved fromlogind's seat state, failing open the way darwin does.
the Windows adapter meant every diagnostic on a Linux desktop was prefixed
windows:and the privilege hints named a service that does not exist there..srsrule-sets arefound across the filesystem hierarchy rather than "next to the executable",
under both the package name and the product name the Debian bundle uses.
install does not offer a button that cannot work, crash/core logs moved off
/tmp, and deep-link registration fixed for release builds.uninstall scripts, and an Arch PKGBUILD.
sing-boxis bundled with a pinneddigest rather than declared as a dependency: it is not in core or extra, only
in the AUR, and a package may not depend on one.
workflow adds Linux as a third link in the existing chain and builds the pacman
package in an Arch container, proving it installs before attaching it.
Verified
gofmt/vetclean; cross-builds for linux, darwin, windows; full suitegreen on macOS and, with
-raceplus staticcheck, in a Linux container.tsc, eslint, 542 front-end tests,cargo fmt, clippy-D warnings,157 Rust tests plus the e2e on macOS;
cargo check --all-targetsclean in botha Debian and an Arch container.
systemd-analyze verifyclean and a fullinstall/upgrade/rollback/uninstall lifecycle exercised in a live systemd
container (
systemd-analyze security3.9);makepkgbuildstenebra-0.4.5-1-x86_64.pkg.tar.zst,pacman -Uinstalls it, namcap clean..debwas built and unpacked — the generated desktop entry reallycarries
Exec=... %uand thetenebra://MimeType, and the dependency list iscorrect.
version, admits an unprivileged client, clamps its store to root-owned 0700 and
resolves sing-box from the packaged path.
Not verified
No live tunnel on Linux yet — nothing here proves traffic flows, routes install
or the kill switch holds. DNS on systemd-resolved hosts is the most likely leak
source and is documented as the top open question. The GUI binary compiles but
has never been launched (no display in a container), and on a desktop with no
tray host the close-to-tray behaviour would leave the window unreachable.