diff --git a/docs/wsl1.md b/docs/wsl1.md new file mode 100644 index 0000000000000..1a29ed015a247 --- /dev/null +++ b/docs/wsl1.md @@ -0,0 +1,49 @@ +# Running Tailscale on WSL1 + +WSL1 (kernel `4.4.0-*-Microsoft`) emulates Linux syscalls on the NT kernel +and needs two deviations from a stock Linux deployment of Tailscale. + +## tailscaled must be built with cgo + +Symptom of a non-cgo build: every Tailscale SSH command execution +(`ssh host cmd`, scp, sftp) hangs, and a `tailscaled be-child ssh ...` +process spins at 100% CPU until the client disconnects. Interactive login +shells still work, because they go through `/bin/login`, which changes +users on its own. + +Cause: WSL1's `tgkill` is missing real Linux's same-thread-group exception +in its signal permission check. The SSH incubator drops privileges with +Go's `syscall.Setuid`, which without cgo applies the syscall on the calling +thread first and then signals every other OS thread (`AllThreadsSyscall`). +After the first thread transitions root -> user, WSL1 rejects the signals +to the remaining root threads with EPERM; the Go runtime ignores the +error and spin-waits forever. glibc's setuid signals sibling threads +*before* transitioning, so a cgo build — which dispatches `Setuid` to +glibc — works. + +Build both binaries release-style, with only cgo flipped on: + + TS_USE_TOOLCHAIN=1 CGO_ENABLED=1 ./build_dist.sh tailscale.com/cmd/tailscaled + TS_USE_TOOLCHAIN=1 CGO_ENABLED=1 ./build_dist.sh tailscale.com/cmd/tailscale + +Notes: + +- The resulting binaries are dynamically linked against glibc, so build + them on (or for) the machine that runs them. +- cgo needs the Linux UAPI headers (`/usr/include/linux`); on Slackware + install the `kernel-headers` package first. +- WSL1 has no TUN device; run tailscaled with + `--tun=userspace-networking`. + +## TS_OS_SPOOF_WINDOWS + +`version.OS()` reports `windows` on Linux only when +`TS_OS_SPOOF_WINDOWS=1` is set in tailscaled's environment. This exists +for the dual-boot state-sharing setup, where a Linux install reuses the +state written by a Windows Tailscale and should keep presenting itself as +that Windows node. + +Do not set it on an established Linux node: the coordination server flags +the OS change ("node OS changed since last connection, was node state +copied between devices?") and serves the node an empty peer list and an +empty Tailscale SSH policy, which disables SSH access entirely. diff --git a/version/prop.go b/version/prop.go index 9cc6d08bfcc9f..fd5a58bbbb54a 100644 --- a/version/prop.go +++ b/version/prop.go @@ -54,12 +54,23 @@ func OS() string { if runtime.GOOS == "darwin" { return "macOS" } - if runtime.GOOS == "linux" { + if runtime.GOOS == "linux" && spoofWindowsOS() { return "windows" } return runtime.GOOS } +// spoofWindowsOS reports whether TS_OS_SPOOF_WINDOWS is set to a true value, +// making a Linux node report "windows" for dual-boot state sharing with a +// Windows Tailscale install. It must be opt-in per machine: an established +// Linux node that flips its reported OS is flagged by the coordination server +// ("node OS changed since last connection") and receives an empty peer list +// and an empty SSH policy. +var spoofWindowsOS = sync.OnceValue(func() bool { + v, _ := strconv.ParseBool(os.Getenv("TS_OS_SPOOF_WINDOWS")) + return v +}) + // IsMacGUIVariant reports whether runtime.GOOS=="darwin" and this one of the // two GUI variants (that is, not tailscaled-on-macOS). // This predicate should not be used to determine sandboxing properties. It's