From 2c7fac5c329894da6bddf5db99328e679743f398 Mon Sep 17 00:00:00 2001 From: Bingchen Gong <6704443+Wenri@users.noreply.github.com> Date: Tue, 7 Jul 2026 01:34:51 +0200 Subject: [PATCH 1/3] version,docs: gate Linux OS spoof behind TS_OS_SPOOF_WINDOWS Reporting "windows" unconditionally on Linux broke established Linux nodes: 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 SSH policy, disabling Tailscale SSH entirely. Make the spoof opt-in for the dual-boot state-sharing machines that need it. Also add docs/wsl1.md documenting WSL1 deployment: tailscaled must be built with CGO_ENABLED=1 because WSL1's tgkill lacks the same-thread-group exception in its signal permission check, so the non-cgo Go setuid (AllThreadsSyscall) spins forever in the SSH incubator after the first thread drops root. glibc's setuid signals sibling threads before transitioning, so the cgo path works. Updates #2 Signed-off-by: Bingchen Gong <6704443+Wenri@users.noreply.github.com> --- docs/wsl1.md | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ version/prop.go | 13 ++++++++++++- 2 files changed, 61 insertions(+), 1 deletion(-) create mode 100644 docs/wsl1.md diff --git a/docs/wsl1.md b/docs/wsl1.md new file mode 100644 index 0000000000000..0091f2be106dc --- /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 this fork. + +## 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..64d59801fc864 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 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 From a357742c376fd1b53bb95f54a1f0daa0f012807a Mon Sep 17 00:00:00 2001 From: Bingchen Gong <6704443+Wenri@users.noreply.github.com> Date: Tue, 7 Jul 2026 01:44:37 +0200 Subject: [PATCH 2/3] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Signed-off-by: Bingchen Gong <6704443+Wenri@users.noreply.github.com> --- docs/wsl1.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/wsl1.md b/docs/wsl1.md index 0091f2be106dc..1a29ed015a247 100644 --- a/docs/wsl1.md +++ b/docs/wsl1.md @@ -1,7 +1,7 @@ # 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 this fork. +and needs two deviations from a stock Linux deployment of Tailscale. ## tailscaled must be built with cgo From f9efed2f30338126daeeca54c368b4b06bfc81b0 Mon Sep 17 00:00:00 2001 From: Bingchen Gong <6704443+Wenri@users.noreply.github.com> Date: Tue, 7 Jul 2026 01:44:47 +0200 Subject: [PATCH 3/3] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Signed-off-by: Bingchen Gong <6704443+Wenri@users.noreply.github.com> --- version/prop.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version/prop.go b/version/prop.go index 64d59801fc864..fd5a58bbbb54a 100644 --- a/version/prop.go +++ b/version/prop.go @@ -65,7 +65,7 @@ func OS() string { // 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 SSH policy. +// and an empty SSH policy. var spoofWindowsOS = sync.OnceValue(func() bool { v, _ := strconv.ParseBool(os.Getenv("TS_OS_SPOOF_WINDOWS")) return v