Skip to content

feat: embed wireguard-go as a library, removing client-side external binary dependencies#477

Open
sechmann wants to merge 7 commits intomainfrom
embed-wireguard-go
Open

feat: embed wireguard-go as a library, removing client-side external binary dependencies#477
sechmann wants to merge 7 commits intomainfrom
embed-wireguard-go

Conversation

@sechmann
Copy link
Copy Markdown
Contributor

@sechmann sechmann commented Feb 25, 2026

Summary

Replaces all external WireGuard binary dependencies (wireguard-go, wg, wireguard.exe) with in-process wireguard-go and wgctrl on client platforms. The naisdevice-helper now manages WireGuard interfaces, configuration, and routes entirely as a Go library — no subprocesses needed.

Note — server-side components (apiserver, gateway-agent, prometheus-agent) still use the wg CLI and kernel WireGuard via network_configurer.go. That is out of scope for this PR.

Changes by platform

macOS

  • SetupInterface: tun.CreateTUN + device.NewDevice (userspace wireguard-go) + UAPI socket
  • SyncConf: wgctrl via new shared wgconfig.ApplyConfig()
  • SetupRoutes: BSD routing sockets (x/net/route) instead of route command
  • TeardownInterface: closes UAPI + wgDevice in-process

Linux

  • SetupInterface: netlink.LinkAdd(&netlink.Wireguard{}) + netlink.AddrAdd + netlink.LinkSetUp
  • SyncConf: wgctrl via wgconfig.ApplyConfig()
  • SetupRoutes: netlink.RouteAdd instead of ip route
  • TeardownInterface: netlink.LinkDel

Why kernel WireGuard on Linux, not embedded wireguard-go?

Linux uses the kernel WireGuard module (netlink.Wireguard{}) instead of an embedded userspace wireguard-go device. This is intentional:

  1. Performance: Kernel WireGuard processes packets in-kernel without context switches to userspace. For a VPN carrying all corporate traffic, this matters.
  2. Maturity: The kernel module has been in mainline Linux since 5.6 (March 2020) and is backported to all major LTS and enterprise kernels. Every distribution that naisdevice targets ships it.
  3. No external binaries removed on Linux regardless: The old code already used ip link add type wireguard (shelling out to ip) and wg syncconf (shelling out to wg). This PR replaces those CLI calls with the equivalent Go libraries (vishvananda/netlink for interface management, wgctrl for configuration via netlink), eliminating the dependency on iproute2 and wireguard-tools binaries. The kernel module itself was always required.
  4. Consistency with wgctrl: wgctrl auto-detects kernel WireGuard on Linux and uses the netlink configuration API, which is the same codepath whether you created the interface via ip link or netlink.LinkAdd. No UAPI socket needed.

If kernel WireGuard is unavailable (e.g., very old kernels or restricted containers), netlink.LinkAdd will fail with a clear error. A future fallback to userspace wireguard-go could be added if needed, but there is currently no evidence this is required for the target deployment environments.

Windows

  • SetupInterface: tun.CreateTUN (wintun) + device.NewDevice (userspace wireguard-go) + UAPI named pipe
  • SyncConf: wgctrl via wgconfig.ApplyConfig()
  • SetupRoutes: winipcfg.LUID.AddIPAddress + LUID.AddRoute instead of wireguard.exe AllowedIPs routing
  • Ships wintun.dll alongside binary instead of bundling WireGuard MSI

Shared / cleanup

  • New internal/wgconfig package — shared wgctrl-based config builder and applier (with tests)
  • Consolidated duplicate key generation code in internal/wireguard/keys.go
  • Removed all client-side INI config pipeline dead code (Marshal, MarshalHeader, writeConfigFile, WireGuardConfigPath)
  • Removed wireguard-go/wireguard-tools from Homebrew cask depends_on
  • Removed wireguard-tools/iproute2 from Nix client service PATH
  • NSIS installer: removed WireGuard MSI install, bundles wintun.dll instead
  • Fixed pre-existing bug in Configure() where wrong error variable was logged

Testing

  • mise run check — all passing (govet, golangci-lint, staticcheck, govulncheck, ratchet)
  • mise run test — all passing
  • Needs manual end-to-end testing on macOS, Linux, and Windows
  • Needs CI validation of Windows NSIS build with wintun.dll

@github-actions
Copy link
Copy Markdown

github-actions bot commented Feb 25, 2026

📝 Changelog preview

Below is a preview of the Changelog that will be added to the next release. Only commit messages that follow the Conventional Commits specification will be included in the Changelog.

v1.15.0 - 2026-03-27

Full Changelog: v1.14.4...v1.15.0

🚀 Features

  • Embed wireguard-go as a library, removing external binary dependencies (d991c6b)
  • Validate and normalize CIDR prefixes and WireGuard public keys (e4bc647)
  • Add post-install smoke tests for all platforms (7b915d2)

🐛 Bug Fixes

  • Force-kill naisdevice-helper.exe during installer upgrade (3e971cf)
  • (ci) Let action-gh-release handle tag creation for pre-releases (24de3bd)

⚙️ Miscellaneous Changes

  • Update CI configuration (02ab256)
  • Bump go version in go.mod as well (e3c598b)
  • Bump pinned action versions via ratchet (7d746f0)

@sechmann sechmann changed the title feat: embed wireguard-go as a library, removing external binary dependencies feat: embed wireguard-go as a library, removing client-side external binary dependencies Feb 25, 2026
sechmann added a commit that referenced this pull request Feb 25, 2026
- Remove custom wireguard.PrivateKey type; use wgtypes.Key everywhere
- Fix ReadOrCreatePrivateKey to write/read base64 format (not raw bytes)
- Fix double base64 encoding in enrollgateway.go
- Replace fragile manual base64+copy with wgtypes.ParseKey in EnsurePrivateKey
- Add strings.TrimSpace when reading key files to handle trailing newlines
- Fix error wrapping: %v -> %w for proper errors.Is/As support
- Make Configure retry loop context-aware (select on ctx.Done vs time.Sleep)
- Clean up Linux interface on partial SetupInterface failure
- Make IPv6 address configuration conditional on all platforms
- Change GatewayRequest.WireGuardPublicKey from []byte to string
sechmann added a commit that referenced this pull request Mar 16, 2026
- Remove custom wireguard.PrivateKey type; use wgtypes.Key everywhere
- Fix ReadOrCreatePrivateKey to write/read base64 format (not raw bytes)
- Fix double base64 encoding in enrollgateway.go
- Replace fragile manual base64+copy with wgtypes.ParseKey in EnsurePrivateKey
- Add strings.TrimSpace when reading key files to handle trailing newlines
- Fix error wrapping: %v -> %w for proper errors.Is/As support
- Make Configure retry loop context-aware (select on ctx.Done vs time.Sleep)
- Clean up Linux interface on partial SetupInterface failure
- Make IPv6 address configuration conditional on all platforms
- Change GatewayRequest.WireGuardPublicKey from []byte to string
@sechmann sechmann force-pushed the embed-wireguard-go branch from cb6fb80 to 75eb38d Compare March 16, 2026 14:14
sechmann added a commit that referenced this pull request Mar 17, 2026
- Remove custom wireguard.PrivateKey type; use wgtypes.Key everywhere
- Fix ReadOrCreatePrivateKey to write/read base64 format (not raw bytes)
- Fix double base64 encoding in enrollgateway.go
- Replace fragile manual base64+copy with wgtypes.ParseKey in EnsurePrivateKey
- Add strings.TrimSpace when reading key files to handle trailing newlines
- Fix error wrapping: %v -> %w for proper errors.Is/As support
- Make Configure retry loop context-aware (select on ctx.Done vs time.Sleep)
- Clean up Linux interface on partial SetupInterface failure
- Make IPv6 address configuration conditional on all platforms
- Change GatewayRequest.WireGuardPublicKey from []byte to string
@sechmann sechmann force-pushed the embed-wireguard-go branch from 75eb38d to 5384124 Compare March 17, 2026 10:26
…dencies

Replace the external WireGuard binary/MSI with wireguard-go embedded as
a Go library. The helper now manages the WireGuard tunnel directly via
wgctrl and the wireguard-go userspace implementation.

Key changes:
- Helper creates and manages the WireGuard interface directly using
  wireguard-go's TUN device and UAPI configuration
- New internal/wgconfig package for WireGuard configuration generation
- Migrate key handling to wgtypes.Key, removing custom crypto wrappers
- Remove external WireGuard MSI bundles, ship wintun.dll instead
- Add wintun update/verify tasks for maintaining the bundled DLLs
- Simplify platform-specific helper code and remove dead functions
- Add build constraints for Linux-only server binaries (gateway-agent,
  apiserver, prometheus-agent) instead of stub files
- Update postinstall scripts and package configs for the new approach
Add iputil package with NormalizeCIDR for consistent CIDR formatting.
Add publickey validation in the enroll package to reject malformed
WireGuard keys at enrollment time. Update apiserver and controlplane-cli
callers to use the new validation.
Add a smoke-test binary that installs the built artifact, verifies
the helper service starts, checks that WireGuard routes are configured
correctly, and tears down. Includes mise tasks to run the tests on
macOS, Linux, and Windows.
Add smoke-test, signing, and notarization jobs to naisdevice workflow.
Add AGENTS.md for AI agent instructions.
@sechmann sechmann force-pushed the embed-wireguard-go branch from 5384124 to e3c598b Compare March 27, 2026 12:36
Remove manual git tag step that fails when the pre-release tag already
exists from a previous run on the same PR. The softprops/action-gh-release
action creates the tag itself via tag_name and target_commitish inputs,
and updates the existing release if the tag already exists.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant