> Project status — early development. The desktop client is the current
> focus. The core, the control protocol and the UI are in good shape and well
> tested, and the Windows tunnel path (wintun + sing-box under the service) is
> exercised against real servers rather than only in tests — but no automated
> test stands up a real tunnel on any platform, and the macOS and Linux tunnels
> have had no privileged live run signed off. Treat this as pre-release: not
> yet "production-ready",
> and expect things to move around. See
> Project status for the honest breakdown.
Why another client
Most clients either lock you into a single protocol or are vague about what they
do with your traffic. Tenebra:
- speaks the protocols sing-box supports — VLESS/REALITY, Hysteria2, AmneziaWG,
Shadowsocks, Trojan, VMess;
- routes Russian destinations directly and sends everything else through the
tunnel, so latency-sensitive local traffic stays local;
- falls back between protocols when one gets throttled or blocked, and remembers
what worked;
- ships no telemetry, no accounts and no bundled servers — you import your own
subscription.
What it does
Everything below is implemented in this repo today (the UI features are desktop):
- Many protocols, one model. Import VLESS (incl. REALITY), Hysteria2,
AmneziaWG, Shadowsocks, Trojan and VMess. A single normalized node model feeds
a from-scratch sing-box config generator. (AmneziaWG links import and connect,
but the bundled stock sing-box applies none of the AWG obfuscation parameters —
the tunnel runs as plain WireGuard; full AmneziaWG obfuscation is
planned and needs a build that links a fork.)
- Import the way you have it. Subscription URL, a raw share link, a
.txt
file of links, clipboard paste, or a QR code (image file or pasted image).
Subscription bodies handle a Clash/Mihomo YAML config, base64, or plaintext
link lists and read the Subscription-Userinfo header for traffic used / total
and expiry.
- Smart RU routing. Smart keeps Russian domains and IPs (and your LAN)
direct and tunnels the rest; Global tunnels everything; Direct is the
proxy off. Geodata comes from the official public sing-geoip / sing-geosite
rule-sets, shipped in the build as local
.srs binaries and loaded from disk —
never downloaded while you are connecting. If a build is missing them, Smart
routes like Global and says so in the log rather than failing to connect.
- DPI bypass that works on the first connect. Windows only: the client drives
zapret so censored services work at their
own latency instead of through an exit node. One bundle release is compiled
into the build so a censored network cannot leave a fresh install with no
bypass at all; newer releases are downloaded as they are published — see
DPI bypass.
- Protocol fallback. A pure state machine walks the last known-good node
first, then by protocol preference (REALITY → Hysteria2 → AmneziaWG), so a
blocked or throttled protocol is retried as another. The last good node leads
the next launch.
- Per-app split tunnelling. Exclude sends chosen apps around the tunnel;
Include sends only chosen apps through it. Matched by executable name and
persisted across restarts.
- Honest leak check. Observes the machine's public IP from redundant echo
services and runs a best-effort DNS probe, then reports a verdict that never
fakes a pass — it tells you what it could not measure rather than claiming
"safe". See docs/control-protocol.md.
- Desktop niceties. System tray that reflects the connection state (with quick
connect/disconnect), desktop notifications on state changes,
tenebra:// deep
links (import a subscription or connect a profile), launch at login (optionally
minimized to the tray), single-instance, live traffic graphs, light/dark themes,
and English / Russian UI.
The kill-switch (drop proxied traffic instead of leaking when the tunnel drops) is a
UI toggle — best-effort by design, with the exact guarantee described in the
changelog; LAN bypass is a core routing option.
DPI bypass
Blocking here is done by inspecting traffic, not by address: YouTube can be
unwatchable on a connection that is otherwise fine, and a tunnel handshake can be
dropped for looking like a tunnel handshake. On Windows Tenebra answers that
with zapret — a separate program that edits
packets on the way out (splitting the TLS ClientHello, sending decoys, and so on)
until the filter stops matching them. It runs beside the tunnel rather than
inside it, so a service the bypass can reach directly stays direct at its own
latency instead of taking the round trip through an exit node. There is no
equivalent on macOS or Linux; the tunnel there carries everything.
Shipped as a floor, downloaded to stay current. What the bypass needs is the
Flowseal/zapret-discord-youtube
bundle: zapret's Windows build (winws.exe), the
WinDivert packet-interception driver it
attaches to, the Cygwin runtime that build needs, and the strategy and host lists
around them. Strategies are a moving target — a set that worked in March is a
set the filter has since learned — so the current release is fetched from
upstream. But a client that can only download one is a client with no bypass on
exactly the networks it exists for, so one release is also compiled into the
Windows core: the archive upstream published, byte for byte, checked against the
checksum this build pins for it. It is the floor, never the ceiling — a newer
release replaces it as soon as one is published and pinned. The macOS and Linux
binaries carry none of it; there is nothing there that could run a Windows
packet filter.
When it happens and where it lands. On the first connect with no bundle
present, the core downloads the latest published release and unpacks it into its
own data directory — %ProgramData%\Tenebra\data\zapret under the Windows
service — then re-checks for a newer one every twelve hours. It is a plain
download from the upstream release page; nothing about you goes with the request.
When that download cannot deliver a bundle at all — no network, GitHub blocked,
a release newer than any checksum this build carries, or an archive that did not
match the checksum it does — the compiled-in copy is unpacked into the same
place instead, and the next successful check upgrades past it.
How to decline the download. Settings → Censorship bypass → Update the
bundle automatically governs what Tenebra fetches: the first-connect download
and the twelve-hour re-check alike. Turned off, it asks the release page for
nothing — press Update when you want a newer one, or unpack one into the
zapret directory above yourself. It does not govern the copy compiled into the
build: those bytes need no
network and no update, so a first connect with no bundle present still unpacks
them and a fresh install is never left with the bypass missing. Deleting the
zapret directory removes what is installed; a later connect lays the
compiled-in copy back down. Running with no bypass at all means the tunnel
carries every service, censored ones included, through the exit node instead of
around the filter.
Everything in the bundle, with its license and copyright holder, is listed in
THIRD-PARTY-NOTICES.md.
Installing
Windows — from the Windows Package Manager:
winget install Divaaaan.Tenebra
or grab Tenebra_x.y.z_x64-setup.exe from the
latest release. Either
way the installer sets up the background service and the in-app updater keeps
everything current.
macOS — download the universal DMG from the
latest release, then
read the macOS note first —
the build currently needs a hand-installed root daemon.
Getting a server
Tenebra is a client — it ships no servers and hard-codes nothing. You bring
your own endpoint and import it as a subscription or a share link. Two ways to
get one:
- Run your own. Any sing-box or Xray
server works; point Tenebra at its subscription URL.
- Use a provider. Any service that hands you a subscription or a share link
will do. I run one at vpsxd.pro.
Project status
| Area | State |
|---|
| Go core (parsing, profiles, routing, config gen, fallback, leak logic) | Implemented, unit-tested, no third-party deps |
| Control protocol (core ↔ UI) | Implemented; covered by Go tests and a real-binary e2e |
| Desktop UI (Tauri 2 + React) | Implemented: all screens, reactive tray, notifications, deep links, autostart, i18n, themes |
| Windows tunnel (wintun + sing-box) | Implemented — a background service runs the tunnel, so the app connects without an elevated GUI; installer sets it up, the in-app updater refreshes both app and service |
| macOS tunnel (utun + sing-box) | Builds and runs — universal .app/DMG — but see the macOS note below: it needs a hand-installed root daemon and is not yet a click-to-run product. No live-tunnel sign-off yet |
Linux tunnel (/dev/net/tun + sing-box) | Builds and runs — a root systemd service owns the tunnel, installed by an Arch package or a sudo script; see the Linux note below. No live-tunnel sign-off yet |
Android (VpnService + libbox) | Alpha, hand-installed — a Kotlin / Compose client in ui-android/ builds and runs on a device: subscription import, node list with latency badges, an AUTO exit, switching the live exit without a reconnect, connect-on-boot, a Quick Settings tile, in-app logs and crash reports. Routing is Global only and there is no DPI bypass. CI builds a debug APK; a tagged release carries a signed one only once the signing key is in CI secrets |
| iOS (Network Extension) | Scaffold only — none of the Swift under ui-ios/ has been compiled and no framework has been built; the plan is docs/porting/ios.md |
| Release pipeline | Tag-triggered release workflow builds the Windows, macOS and Linux bundles plus the Arch package, minisign-signs the in-app updater artifacts, and publishes the GitHub release once every expected asset is on it; Android is a separate workflow on the same tag |
| Code-signing | Not set up — the Windows installer is Authenticode-unsigned (SmartScreen warns), the macOS build is unsigned/un-notarized (Gatekeeper needs a manual "Open Anyway"), and the Android release APK has no keystore in CI yet |
macOS note — read before downloading the DMG
The macOS build is for advanced users right now, not a finished product. Two
things are not yet in place, so a plain "download the DMG and drag to
Applications" will not give you a working tunnel:
- The tunnel needs a privileged helper. macOS only lets root open the
utun
device, so the app talks to a small root LaunchDaemon that owns the tunnel.
That daemon is currently installed by hand with a sudo script
(scripts/macos/install-daemon.sh) — there
is no in-app installer for it yet. Without it, the app runs but cannot connect.
- The build is unsigned and un-notarized. First launch needs
System Settings → Privacy & Security → Open Anyway, and updates to the
daemon are a manual step (the in-app updater refreshes only the app, not the
root daemon). Since 0.4.4 the app warns with a banner when the daemon has
fallen behind it; re-run the install script from your checkout to update:
sudo bash scripts/macos/install-daemon.sh --from-app /Applications/Tenebra.app --allow-unsigned.
The click-to-run macOS path — a signed, notarized build with an SMAppService
daemon bundled inside the app (so it installs and updates like the Windows
service) — needs an Apple Developer ID and is planned, not done. Until then,
use the DMG only if you're comfortable running the install script yourself.
Windows users are unaffected — the Windows installer sets up the service and
the updater keeps everything current automatically.
Linux note — the tunnel needs a root service
Linux is the same shape as macOS: only a privileged process may open
/dev/net/tun and install routes, so the app talks to a small root systemd
service that owns the tunnel and serves the control protocol on
/run/tenebra.sock. The app alone cannot connect. Two ways to set it up:
-
Arch Linux — build the package. packaging/arch/PKGBUILD
builds the core, the desktop app and the unit from source and installs them
with pacman:
cd packaging/arch && makepkg -si
sudo systemctl enable --now tenebra.service
Updates come from pacman, not the in-app updater — it can only replace an
AppImage, never files a package manager owns.
-
Any other distribution — the install script. Fetch the bundled resources,
then install the daemon from your checkout:
bash scripts/fetch-resources.sh
sudo bash scripts/linux/install-daemon.sh --dev
It is safe to re-run to upgrade, rolls back if an upgrade fails, and
scripts/linux/uninstall-daemon.sh
removes it. The GUI is a separate .deb/AppImage build.
Two limits worth knowing before you install: system-proxy mode does nothing on
Linux (it needs per-desktop settings a root daemon cannot reach, so it stays
quietly disarmed — tun mode, the default, is unaffected), and the bundled
sing-box binaries are glibc-linked, so musl distributions need their own.
Full detail, including the systemd sandbox and what is deliberately left out of
it, is in docs/porting/linux.md.
If you want to help close the gap, the macOS SMAppService path and the
non-desktop adapters are the highest-leverage places — see
CONTRIBUTING.md.
Repository layout
tenebra/
├── core/ Go. Platform-agnostic, stdlib-only, fully unit-tested.
│ ├── model/ Normalized proxy node + config types.
│ ├── subscription/ Parse vless/hysteria2/ss/trojan/vmess links + sub bodies.
│ ├── profile/ Named profiles and their atomic on-disk store.
│ ├── routing/ smart/global/direct + per-app split -> sing-box route/dns.
│ ├── singbox/ Build a full sing-box config as plain JSON (no sing-box dep).
│ ├── fallback/ Pure REALITY->Hysteria2->AmneziaWG fallback state machine.
│ ├── zapret/ Drive the DPI-bypass bundle, embedded + downloaded (Windows).
│ └── control/ The line-delimited JSON protocol + the daemon.
├── core-bridge/ The same generator as a mobile-facing library (no sing-box).
├── mobile/ gomobile wrapper: binds core-bridge + libbox into one artifact.
├── adapters/
│ ├── windows/ Spawn & supervise sing-box; traffic via its clash API.
│ ├── macos/ The same over utun, under the root LaunchDaemon.
│ └── linux/ The same over /dev/net/tun, under the root systemd service.
├── cmd/
│ └── tenebra-core/ The sidecar entry point (talks the protocol on stdin/stdout).
├── ui-desktop/ Tauri 2 app: Rust shell (src-tauri) + React/TS front end (src).
├── ui-android/ Kotlin/Compose client: VpnService + libbox (alpha).
├── ui-ios/ SwiftUI + Network Extension scaffold; never compiled.
├── deploy/ The privileged daemon's service definitions per platform.
├── packaging/
│ └── arch/ PKGBUILD building the whole thing for Arch Linux.
├── scripts/
│ ├── fetch-resources.ps1 Download pinned sing-box + wintun (Windows).
│ ├── fetch-resources.sh The same for macOS and Linux.
│ ├── build-libbox-android.sh One gomobile bind -> the fused tenebra.aar.
│ ├── build-libbox.sh The same bind for Apple (xcframework).
│ ├── macos/ Install/remove the root LaunchDaemon.
│ └── linux/ Install/remove the root systemd service.
└── docs/ Architecture, control protocol, the dev guide, porting notes.
Building
Requirements: Go 1.24+, Node 22+, and the Rust toolchain (for the
desktop UI). Full walkthrough and troubleshooting in
docs/development.md.
Core tests:
go test ./...
Desktop app (Windows):
# fetch the sing-box binary and wintun.dll into src-tauri/resources
powershell -File scripts/fetch-resources.ps1
# build the core sidecar where Tauri bundles it
go build -o ui-desktop/src-tauri/binaries/tenebra-core-x86_64-pc-windows-msvc.exe ./cmd/tenebra-core
# build the bundle
cd ui-desktop
npm install
npm run tauri build
Desktop app (Linux):
# fetch the sing-box binary and the rule-sets into src-tauri/resources
bash scripts/fetch-resources.sh
# build the core sidecar where Tauri bundles it
go build -o ui-desktop/src-tauri/binaries/tenebra-core-x86_64-unknown-linux-gnu ./cmd/tenebra-core
# build the .deb and AppImage
cd ui-desktop
npm install
npm run tauri build
On Arch, cd packaging/arch && makepkg -si does all of the above and installs
the result — see the Linux note.
Documentation
Support
Tenebra is maintained by one person in their spare time, so please keep support
low-friction:
- Questions or help — start a thread in
Discussions.
- Bugs — file a report through the
issue form; it asks
for your version, Windows build and logs.
- Security problems — follow SECURITY.md; please don't open a
public issue.
Response times vary — this is a side project, not a supported product. Thanks for
your patience.
License
GPLv3 — see LICENSE. sing-box is GPLv3, so Tenebra is too. Bundled
third-party components and their licenses are listed in
THIRD-PARTY-NOTICES.md.