The Windscribe VPN client, packaged for NixOS. Unofficial.
Every connection mode works: WireGuard, OpenVPN over UDP and TCP, Stealth and WSTunnel. So do the kill switch, DNS leak protection and Connected DNS. The package ships both the Qt desktop client and a headless build for servers.
This is a repack of upstream's own Debian build rather than a source build.
Upstream's build system downloads a prebuilt libwsnet.so, so compiling from
source still lands a vendored binary while costing a full Qt and
OpenSSL-with-ECH toolchain. Since 2.2x upstream links Qt statically, which
means the repack needs no Qt at all.
Add the flake and turn the module on.
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
windscribe = {
url = "github:ParkerrDev/nixpkgs-windscribe";
inputs.nixpkgs.follows = "nixpkgs";
};
};
outputs = { nixpkgs, windscribe, ... }: {
nixosConfigurations.mymachine = nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
modules = [
windscribe.nixosModules.windscribe
{ services.windscribe.enable = true; }
];
};
};
}Rebuild, then launch Windscribe from your application menu or run windscribe.
The helper daemon starts on boot; you do not need to run anything by hand.
On a headless machine, install the smaller build instead:
services.windscribe = {
enable = true;
variant = "cli";
autoStart = true;
};Then loginctl enable-linger <user> so the engine survives logout, and drive it
with windscribe-cli login, windscribe-cli connect, windscribe-cli status.
Without flakes:
{
imports = [
"${builtins.fetchTarball "https://github.com/ParkerrDev/nixpkgs-windscribe/archive/refs/heads/master.tar.gz"}/modules/nixos.nix"
];
services.windscribe.enable = true;
}| Option | Type | Default | What it does |
|---|---|---|---|
services.windscribe.enable |
bool | false |
Install the client and run the helper daemon |
services.windscribe.variant |
"gui" / "cli" |
"gui" |
Qt desktop client, or the headless engine |
services.windscribe.package |
package | matching variant | Override the package |
services.windscribe.autoStart |
bool | false |
Start the engine with the user session |
services.windscribe.extraHelperPackages |
list | [ ] |
Extra tools on the helper's PATH |
A Home Manager module is exposed as homeManagerModules.windscribe for per-user
installs. It cannot replace the NixOS module: the helper daemon, the setgid
wrapper and the windscribe system user are privileged and have to be declared
system-wide.
| Mode | Backed by | Verified |
|---|---|---|
| WireGuard | kernel module, falling back to bundled amneziawg-go |
yes (both paths) |
| UDP | bundled OpenVPN 2.7.5 | yes |
| TCP | bundled OpenVPN 2.7.5 | yes |
| Stealth | OpenVPN wrapped in TLS by the bundled wstunnel |
yes |
| WSTunnel | bundled Windscribe wstunnel |
yes |
Each was checked by connecting from a NixOS VM and confirming the public address changed, DNS still resolved and the kill switch engaged. See docs/verification.md for the transcript.
Connected DNS works too, through the bundled ctrld. Split tunnelling does
not: it needs the cgroup v1 net_cls controller, which modern NixOS does
not mount.
Upstream's Linux build assumes a filesystem hierarchy NixOS does not have, in four separate places. Each is dealt with explicitly, and each is covered by an assertion in the VM tests so a version bump cannot quietly undo it.
The helper only execs binaries out of /opt/windscribe. Before it starts
OpenVPN, wstunnel, ctrld or amneziawg-go it compares realpath("/opt/windscribe")
against its compile-time install directory and refuses anything else. realpath
sees through symlinks, so pointing that path at the Nix store does not work. The
module bind-mounts the package tree into the helper's mount namespace instead,
which realpath cannot see through.
The helper hardcodes its own PATH. main.cpp runs
setenv("PATH", "/usr/sbin:/usr/bin:/sbin:/bin", 1) before anything can spawn a
child, which overrides whatever systemd puts in the unit environment and leaves
ip, nmcli, modprobe, pkill and resolvectl unreachable. The package
rewrites that string literal in place, NUL-padded so it is never lengthened, to
point at a directory the module fills with exactly the tools upstream calls.
The engine needs a group it can only inherit. It opens the helper socket
while holding the windscribe group and drops it immediately afterwards, so a
file capability is no help: the process never raises into the group. The module
installs a setgid wrapper, which is what upstream's postinst achieves with
chgrp windscribe && chmod 2755.
Three of the shipped binaries are Go. patchelf writing an RPATH into a Go
executable moves its program headers and the runtime then dies with SIGSEGV
before main, so wstunnel, amneziawg-go and ctrld are kept away from
autoPatchelfHook and given only an interpreter. ctrld needs one extra step:
it is UPX-packed and has no section headers at all, so it is unpacked first.
Skipping that is what forces other packagings to depend on nix-ld.
Two smaller ones: the firewall ruleset is built around
getpwnam("windscribe") and fails the whole apply when that user is missing,
which takes Stealth and WSTunnel down with it, so the module declares the user;
and install-update shells out to apt, dnf, pacman or zypper through pkexec,
so it is replaced with a script that tells you to bump the flake.
./pkgs/windscribe/update.sh # or: ./pkgs/windscribe/update.sh 2.25.0
nix build .#windscribe .#windscribe-cli
nix flake check # runs the VM testsThe update script fetches each .deb and records the hash it actually got,
rather than trusting the table in the release notes.
Do not use the in-app update button. It is wired to a script that refuses, because on NixOS there is no package manager for it to call.
nix flake check builds both variants and runs two NixOS VM tests that assert
the helper's PATH was rewritten, /opt/windscribe resolves to itself inside the
helper's namespace, every protocol binary runs, the helper socket is group-gated,
a plain user reaches it only through the setgid wrapper, and the engine comes up
and talks to the helper. The VM tests need KVM.
Windscribe for the client. Earlier NixOS attempts by adrianmgg, syntheit, ItzDerock and Varmisanth mapped out the setgid and bind-mount problems before this repackaging.
Contributions welcome.
