Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 85 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,91 @@ jobs:
with:
name: tenebra-windows-installer
path: ui-desktop/src-tauri/target/release/bundle/nsis/*-setup.exe
linux:
# Proves the Linux desktop shell builds: the unix-socket backend that drives
# the systemd daemon, and the deb bundle itself. The `core` job above already
# runs the Go suite on Linux — including the linux-tagged control socket and
# the SO_PEERCRED peer check — so this job covers the Rust and bundler half.
#
# Pinned to 22.04 rather than ubuntu-latest deliberately. The bundled
# binaries link against the runner's glibc, and anything built on 24.04
# refuses to start on an older distribution; the rolling Arch target would
# not care, but the deb is precisely what Debian-family users install.
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
with:
go-version: '1.26'
cache: false
- uses: actions/setup-node@v6
with:
node-version: 24
- uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt, clippy
- name: Install the GTK and WebKit build dependencies
# Tauri v2 links against webkit2gtk-4.1; the appindicator package backs
# the tray, and patchelf is what the bundler uses to rewrite rpaths.
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev \
librsvg2-dev patchelf file
- name: Check Rust formatting
working-directory: ui-desktop/src-tauri
run: cargo fmt --check
- name: Fetch sing-box and the rule-sets
run: bash scripts/fetch-resources.sh --arch amd64
- name: Build core sidecar
# tauri-build validates the externalBin at compile time, so the sidecar
# has to exist under its target triple before cargo runs at all.
run: go build -o ui-desktop/src-tauri/binaries/tenebra-core-x86_64-unknown-linux-gnu ./cmd/tenebra-core
- name: Lint the Rust backend
working-directory: ui-desktop/src-tauri
run: cargo clippy --all-targets -- -D warnings
- name: Test the Rust backend
working-directory: ui-desktop/src-tauri
run: cargo test --all-targets
- name: Install front-end dependencies
working-directory: ui-desktop
run: npm ci
- name: Build the Debian package
working-directory: ui-desktop
# Same reasoning as the Windows job: CI only proves the bundle builds, so
# it overrides the config to skip updater artifacts and never touches the
# signing key. AppImage is left to the release workflow — it downloads a
# linuxdeploy toolchain on every run and would double this job's time for
# no extra coverage of our own code.
run: npm run tauri build -- --bundles deb --config src-tauri/tauri.ci.conf.json
- uses: actions/upload-artifact@v6
with:
name: tenebra-linux-deb
path: ui-desktop/src-tauri/target/release/bundle/deb/*.deb
arch-package:
# Lints the Arch packaging on every push. It stops short of a full makepkg on
# purpose: the PKGBUILD takes its source from a git tag, so building it only
# works on a tagged commit — that is the release workflow's job. What is
# cheap to catch here is the rest: broken shell, a malformed PKGBUILD, and
# the namcap complaints that would otherwise surface only at release time.
runs-on: ubuntu-latest
container: archlinux:base-devel
steps:
- name: Install the packaging tools
run: pacman -Syu --noconfirm --needed namcap git
- uses: actions/checkout@v6
- name: Parse the PKGBUILD
# makepkg refuses to run as root, so the checks run as a normal user.
# --printsrcinfo parses the whole file without fetching any source.
run: |
useradd -m builder
chown -R builder .
su builder -c 'cd packaging/arch && makepkg --printsrcinfo' > /tmp/SRCINFO
head -30 /tmp/SRCINFO
- name: Lint the PKGBUILD
run: |
cd packaging/arch
namcap PKGBUILD
macos:
# Proves the Go core plus the darwin-tagged macOS adapter (adapters/macos)
# compile and pass their tests on a real Apple Silicon runner — the macOS
Expand Down
165 changes: 163 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ jobs:
# resolving new versions, so a signed release always matches the lockfile.
args: --bundles nsis -- --locked
releaseBody: |
Desktop builds of Tenebra for Windows and macOS.
Desktop builds of Tenebra for Windows, macOS and Linux.

**Windows** is user-ready: run the installer (SmartScreen warns on an
unsigned build -> More info -> Run anyway); it sets up the background
Expand All @@ -87,6 +87,16 @@ jobs:
the "macOS note" in the README before downloading. A signed,
click-to-run macOS build is planned.

**Linux**: the Arch package (.pkg.tar.zst) is the click-to-run one
— `pacman -U` installs the app, the core and the systemd unit, and
`systemctl enable --now tenebra` starts the tunnel service. The .deb
and the AppImage install only the app and the core: the privileged
service still has to be set up once by hand with
scripts/linux/install-daemon.sh, because the Debian bundle cannot run
a post-install script. Updates for a packaged install come from your
package manager, not the in-app updater, which only works for the
AppImage.

Updates are delivered in-app and verified against the project's
minisign key before they install (on macOS the updater refreshes the
app, not the hand-installed daemon).
Expand Down Expand Up @@ -169,7 +179,7 @@ jobs:
includeUpdaterJson: true
args: --target universal-apple-darwin --bundles app,dmg -- --locked
releaseBody: |
Desktop builds of Tenebra for Windows and macOS.
Desktop builds of Tenebra for Windows, macOS and Linux.

**Windows** is user-ready: run the installer (SmartScreen warns on an
unsigned build -> More info -> Run anyway); it sets up the background
Expand All @@ -183,6 +193,16 @@ jobs:
the "macOS note" in the README before downloading. A signed,
click-to-run macOS build is planned.

**Linux**: the Arch package (.pkg.tar.zst) is the click-to-run one
— `pacman -U` installs the app, the core and the systemd unit, and
`systemctl enable --now tenebra` starts the tunnel service. The .deb
and the AppImage install only the app and the core: the privileged
service still has to be set up once by hand with
scripts/linux/install-daemon.sh, because the Debian bundle cannot run
a post-install script. Updates for a packaged install come from your
package manager, not the in-app updater, which only works for the
AppImage.

Updates are delivered in-app and verified against the project's
minisign key before they install (on macOS the updater refreshes the
app, not the hand-installed daemon).
Expand All @@ -194,3 +214,144 @@ jobs:
# latest.json carrying both platforms; the Windows job already published
# a Windows-only interim copy, which this overwrite supersedes.
run: node scripts/publish-beta-manifest.mjs "$GITHUB_REF_NAME" "${{ steps.channel.outputs.prerelease }}"
linux:
# Third in the chain for the same reason macOS is second: all three jobs
# upload to one release and tauri-action merges its platform entries into
# that release's latest.json, so running them in sequence keeps the
# read-modify-write deterministic. A failure here leaves the Windows and
# macOS assets standing; Linux follows in a fix-forward tag.
#
# 22.04 rather than ubuntu-latest: the AppImage and the deb link against the
# runner's glibc, and a 24.04 build refuses to start on anything older.
needs: [ci, version-check, macos]
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
with:
go-version: '1.26'
cache: false
- uses: actions/setup-node@v6
with:
node-version: 24
- uses: dtolnay/rust-toolchain@stable
- name: Install the GTK and WebKit build dependencies
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev \
librsvg2-dev patchelf file
- name: Fetch sing-box and the rule-sets
run: bash scripts/fetch-resources.sh --arch amd64
- name: Build core sidecar
run: go build -o ui-desktop/src-tauri/binaries/tenebra-core-x86_64-unknown-linux-gnu ./cmd/tenebra-core
- name: Install front-end dependencies
working-directory: ui-desktop
run: npm ci
- name: Resolve the release channel from the tag
id: channel
shell: bash
run: |
# Same rule as the other two jobs: a SemVer prerelease suffix marks the
# GitHub release prerelease, keeping /releases/latest/ on the last full
# release so stable clients are unaffected.
if [[ "$GITHUB_REF_NAME" == *-* ]]; then
echo "prerelease=true" >> "$GITHUB_OUTPUT"
else
echo "prerelease=false" >> "$GITHUB_OUTPUT"
fi
- name: Build, sign, and publish the release
uses: tauri-apps/tauri-action@v0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
with:
projectPath: ui-desktop
tagName: ${{ github.ref_name }}
releaseName: Tenebra ${{ github.ref_name }}
releaseDraft: false
prerelease: ${{ steps.channel.outputs.prerelease }}
# Only the AppImage carries an updater artifact — the Tauri updater
# cannot replace a package-managed install, which is why the app hides
# its update controls when it was installed from a package.
includeUpdaterJson: true
args: --bundles deb,appimage -- --locked
releaseBody: |
Desktop builds of Tenebra for Windows, macOS and Linux.

**Windows** is user-ready: run the installer (SmartScreen warns on an
unsigned build -> More info -> Run anyway); it sets up the background
service, and the in-app updater keeps both app and service current.

**macOS is for advanced users, not click-to-run yet.** The DMG alone
will NOT give you a working tunnel: macOS needs a root helper daemon
that is currently installed by hand with a sudo script
(scripts/macos/install-daemon.sh), and the build is unsigned (first
launch: System Settings -> Privacy & Security -> Open Anyway). Read
the "macOS note" in the README before downloading. A signed,
click-to-run macOS build is planned.

**Linux**: the Arch package (.pkg.tar.zst) is the click-to-run one
— `pacman -U` installs the app, the core and the systemd unit, and
`systemctl enable --now tenebra` starts the tunnel service. The .deb
and the AppImage install only the app and the core: the privileged
service still has to be set up once by hand with
scripts/linux/install-daemon.sh, because the Debian bundle cannot run
a post-install script. Updates for a packaged install come from your
package manager, not the in-app updater, which only works for the
AppImage.

Updates are delivered in-app and verified against the project's
minisign key before they install (on macOS the updater refreshes the
app, not the hand-installed daemon).
- name: Publish the beta channel manifest
shell: bash
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Final overwrite of beta.json, now that latest.json carries all three
# platforms; the Windows and macOS jobs published interim copies.
run: node scripts/publish-beta-manifest.mjs "$GITHUB_REF_NAME" "${{ steps.channel.outputs.prerelease }}"
arch-package:
# Builds the pacman package the Arch users actually want, so they do not have
# to run makepkg themselves. It is a separate job from the Tauri bundles
# because it builds inside an Arch container: the package links against
# Arch's own glibc and webkit, which is the whole point — a deb built on
# Ubuntu would drag the wrong library names along.
#
# The PKGBUILD takes its source from the git tag, so this can only run once
# the tag exists; it also means the package is built from exactly what was
# tagged rather than from the runner's working copy.
needs: [ci, version-check, linux]
runs-on: ubuntu-latest
container: archlinux:base-devel
steps:
- name: Install the packaging tools
run: pacman -Syu --noconfirm --needed git github-cli
- uses: actions/checkout@v6
- name: Build the package
# The revision comes from the PKGBUILD's own pkgver, which
# scripts/set-version.mjs keeps in step with the tag; version-check has
# already asserted they agree before this job runs.
run: |
set -euo pipefail
# makepkg refuses to run as root, so build as an unprivileged user that
# is allowed to install the build dependencies it resolves.
useradd -m builder
echo 'builder ALL=(ALL) NOPASSWD: /usr/bin/pacman' > /etc/sudoers.d/builder
chown -R builder .
su builder -c 'cd packaging/arch && makepkg --syncdeps --noconfirm --needed'
ls -la packaging/arch/*.pkg.tar.zst
- name: Verify the package installs
run: |
set -euo pipefail
pacman -U --noconfirm packaging/arch/*.pkg.tar.zst
# Prove the layout the daemon and the UI resolve against is really there.
test -x /usr/bin/tenebra-core
test -x /usr/lib/tenebra/sing-box
test -f /usr/lib/systemd/system/tenebra.service
- name: Attach the package to the release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: ${{ github.ref_name }}
run: gh release upload "$TAG" packaging/arch/*.pkg.tar.zst --clobber
17 changes: 15 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,27 @@ target/
/ui-desktop/src-tauri/target/
/ui-desktop/src-tauri/gen/

# makepkg builds in place, so running packaging/arch/PKGBUILD from a checkout
# drops a source clone, a build tree and the finished package next to it
/packaging/arch/src/
/packaging/arch/pkg/
/packaging/arch/tenebra/
/packaging/arch/*.pkg.tar.*
/packaging/arch/*.srs
/packaging/arch/sing-box-*.tar.gz

# sing-box and tunnel binaries are fetched at build time, never committed
/bin/
sing-box
sing-box.exe
wintun.dll

# gomobile bind leftover: the ARM core binary produced during Android bind work
tenebra-core
# gomobile bind leftover: the ARM core binary produced during Android bind work.
# Anchored to the repository root on purpose — an unanchored pattern also matches
# the cmd/tenebra-core directory, and git then silently refuses to add anything
# new inside it (existing files stay tracked, so the hole is invisible until a
# fresh source file goes missing from a commit).
/tenebra-core

# sing-box runtime cache, written next to wherever the core runs
cache.db
Expand Down
42 changes: 41 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,45 @@ All notable changes to Tenebra are documented here. The format follows
> [project status](README.md#project-status). Expect breaking changes between
> 0.x releases.

## [0.4.6] - 2026-07-27

### Added

- **Linux is a supported desktop platform.** Until now Linux only compiled: the
core was built and tested in CI, but it could not host a daemon there — the
socket transport was rejected off macOS — and no installable artifact was
produced. It now runs the same way Windows and macOS do: one privileged core
under systemd owns the tunnel and serves the control protocol on
`/run/tenebra.sock`, and the unprivileged desktop app attaches to it. The
connecting user is authenticated from the kernel's own socket credentials,
and the profile store under `/var/lib/tenebra/data` is created root-only and
re-checked on every start.
- **An Arch package.** `packaging/arch/PKGBUILD` builds the core, the desktop
app and the systemd unit from source; the release workflow builds it in an
Arch container, verifies it installs, and attaches the resulting
`.pkg.tar.zst` to the release. sing-box is bundled with a pinned digest
rather than declared as a dependency: there is no such package in Arch's own
repositories, only in the AUR, and it goes into a private directory so an
existing AUR install is untouched.
- **Debian and AppImage bundles** for everything else, plus install and
uninstall scripts for setting the service up by hand. Only the Arch package
installs the systemd unit for you; the Debian bundle cannot run a
post-install script, so there the service is one manual step.

### Fixed

- **Linux: the crash and core logs no longer live in `/tmp`.** They now follow
the XDG data directory. `/tmp` is shared and world-writable, so a symlink
planted at the old path could have redirected an append-only writer.
- **Linux: `tenebra://` links now work in a release build.** Deep-link
registration was compiled in only for development builds, and an AppImage has
no installer to claim the scheme on its behalf, so a released build never
registered as a handler at all.
- **Linux: the app no longer offers an update it cannot install.** The bundled
updater can only replace an AppImage, so an install that came from a package
manager now says where its updates come from instead of showing a button that
would fail.

## [0.4.5] - 2026-07-25

### Fixed
Expand Down Expand Up @@ -568,7 +607,8 @@ Initial tagged release.
first run. Updates delivered in-app are minisign-verified against the bundled
key; only the initial download is unsigned.

[Unreleased]: https://github.com/Divaaaan/tenebra/compare/v0.4.5...HEAD
[Unreleased]: https://github.com/Divaaaan/tenebra/compare/v0.4.6...HEAD
[0.4.6]: https://github.com/Divaaaan/tenebra/compare/v0.4.5...v0.4.6
[0.4.5]: https://github.com/Divaaaan/tenebra/compare/v0.4.4...v0.4.5
[0.4.4]: https://github.com/Divaaaan/tenebra/compare/v0.4.3...v0.4.4
[0.4.3]: https://github.com/Divaaaan/tenebra/compare/v0.4.2...v0.4.3
Expand Down
Loading
Loading