Repository navigation
version,docs: gate Linux OS spoof behind TS_OS_SPOOF_WINDOWS - #3
Merged
Merged
Conversation
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>
There was a problem hiding this comment.
Pull request overview
This PR makes Linux reporting "windows" via version.OS() opt-in (to avoid control-plane “OS changed” fallout on established nodes), and adds documentation for running Tailscale on WSL1, including the need for a CGO-enabled tailscaled to avoid SSH-incubator hangs.
Changes:
- Gate Linux→
"windows"OS spoofing behindTS_OS_SPOOF_WINDOWS(parsed once viasync.OnceValue). - Add
docs/wsl1.mddescribing WSL1 deployment specifics, including CGO build requirements and--tun=userspace-networking.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| version/prop.go | Makes Linux OS spoofing opt-in via TS_OS_SPOOF_WINDOWS. |
| docs/wsl1.md | Documents WSL1 deployment requirements and the OS-spoof opt-in behavior. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
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>
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>
Wenri
added a commit
that referenced
this pull request
Jul 30, 2026
Wenri
added a commit
that referenced
this pull request
Jul 30, 2026
Lets a Linux tailscaled present as the same tailnet node as a dual-boot
Windows install, sharing one state file:
- version.OS() reports "windows" on linux when TS_OS_SPOOF_WINDOWS is set.
Opt-in per machine on purpose: an established 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.
- The LocalAPI's Windows-only LocalUserID gate is skipped on non-Windows —
the shared profile's CurrentUserID is a Windows SID that no Linux
connection could ever match (ipnserver/actor.go CheckProfileAccess,
ipnlocal CheckIPNConnectionAllowed, and the profiles.go startup scan).
- docs/wsl1.md documents the setup.
Also folded in, unrelated to the feature but carried by the original PRs:
a genuine upstream bug fix in cmd/tsconnect/build.go (the precompressDist
error check tested the wrong, provably-nil variable, so precompression
failures were silently swallowed), two comment typos, a redundant break,
tempfork/sshtest tidy-ups, a .golangci.yml lint-exemption widening, two
.github/workflows fork guards, and .gitignore entries for the dual-boot
patched binaries.
Squashed from gt4o4/tailscale #2 + #3.
Wenri
added a commit
that referenced
this pull request
Aug 5, 2026
Lets a Linux tailscaled present as the same tailnet node as a dual-boot
Windows install, sharing one state file:
- version.OS() reports "windows" on linux when TS_OS_SPOOF_WINDOWS is set.
Opt-in per machine on purpose: an established 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.
- The LocalAPI's Windows-only LocalUserID gate is skipped on non-Windows —
the shared profile's CurrentUserID is a Windows SID that no Linux
connection could ever match (ipnserver/actor.go CheckProfileAccess,
ipnlocal CheckIPNConnectionAllowed, and the profiles.go startup scan).
- docs/wsl1.md documents the setup.
Also folded in, unrelated to the feature but carried by the original PRs:
a genuine upstream bug fix in cmd/tsconnect/build.go (the precompressDist
error check tested the wrong, provably-nil variable, so precompression
failures were silently swallowed), two comment typos, a redundant break,
tempfork/sshtest tidy-ups, a .golangci.yml lint-exemption widening, two
.github/workflows fork guards, and .gitignore entries for the dual-boot
patched binaries.
Squashed from gt4o4/tailscale #2 + #3.
Wenri
added a commit
that referenced
this pull request
Aug 7, 2026
Lets a Linux tailscaled present as the same tailnet node as a dual-boot
Windows install, sharing one state file:
- version.OS() reports "windows" on linux when TS_OS_SPOOF_WINDOWS is set.
Opt-in per machine on purpose: an established 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.
- The LocalAPI's Windows-only LocalUserID gate is skipped on non-Windows —
the shared profile's CurrentUserID is a Windows SID that no Linux
connection could ever match (ipnserver/actor.go CheckProfileAccess,
ipnlocal CheckIPNConnectionAllowed, and the profiles.go startup scan).
- docs/wsl1.md documents the setup.
Also folded in, unrelated to the feature but carried by the original PRs:
a genuine upstream bug fix in cmd/tsconnect/build.go (the precompressDist
error check tested the wrong, provably-nil variable, so precompression
failures were silently swallowed), two comment typos, a redundant break,
tempfork/sshtest tidy-ups, a .golangci.yml lint-exemption widening, two
.github/workflows fork guards, and .gitignore entries for the dual-boot
patched binaries.
Squashed from gt4o4/tailscale #2 + #3.
Wenri
added a commit
that referenced
this pull request
Sep 10, 2026
Lets a Linux tailscaled present as the same tailnet node as a dual-boot
Windows install, sharing one state file:
- version.OS() reports "windows" on linux when TS_OS_SPOOF_WINDOWS is set.
Opt-in per machine on purpose: an established 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.
- The LocalAPI's Windows-only LocalUserID gate is skipped on non-Windows —
the shared profile's CurrentUserID is a Windows SID that no Linux
connection could ever match (ipnserver/actor.go CheckProfileAccess,
ipnlocal CheckIPNConnectionAllowed, and the profiles.go startup scan).
- docs/wsl1.md documents the setup.
Also folded in, unrelated to the feature but carried by the original PRs:
a genuine upstream bug fix in cmd/tsconnect/build.go (the precompressDist
error check tested the wrong, provably-nil variable, so precompression
failures were silently swallowed), two comment typos, a redundant break,
tempfork/sshtest tidy-ups, a .golangci.yml lint-exemption widening,
.github/workflows fork guards on every job (so the fork never runs
upstream's CI), and .gitignore entries for the dual-boot patched binaries.
Squashed from gt4o4/tailscale #2 + #3 + the 2026-08-07 workflow guards;
re-merged onto upstream main 2026-09-10 (the fuzz job we had guarded was
removed upstream).
Wenri
added a commit
that referenced
this pull request
Sep 17, 2026
Lets a Linux tailscaled present as the same tailnet node as a dual-boot
Windows install, sharing one state file:
- version.OS() reports "windows" on linux when TS_OS_SPOOF_WINDOWS is set.
Opt-in per machine on purpose: an established 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.
- The LocalAPI's Windows-only LocalUserID gate is skipped on non-Windows —
the shared profile's CurrentUserID is a Windows SID that no Linux
connection could ever match (ipnserver/actor.go CheckProfileAccess,
ipnlocal CheckIPNConnectionAllowed, and the profiles.go startup scan).
- docs/wsl1.md documents the setup.
Also folded in, unrelated to the feature but carried by the original PRs:
a genuine upstream bug fix in cmd/tsconnect/build.go (the precompressDist
error check tested the wrong, provably-nil variable, so precompression
failures were silently swallowed), two comment typos, a redundant break,
tempfork/sshtest tidy-ups, a .golangci.yml lint-exemption widening,
.github/workflows fork guards on every job (so the fork never runs
upstream's CI), and .gitignore entries for the dual-boot patched binaries.
Squashed from gt4o4/tailscale #2 + #3 + the 2026-08-07 workflow guards;
re-merged onto upstream main 2026-09-10 (the fuzz job we had guarded was
removed upstream).
Wenri
added a commit
that referenced
this pull request
Oct 2, 2026
Lets a Linux tailscaled present as the same tailnet node as a dual-boot
Windows install, sharing one state file:
- version.OS() reports "windows" on linux when TS_OS_SPOOF_WINDOWS is set.
Opt-in per machine on purpose: an established 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.
- The LocalAPI's Windows-only LocalUserID gate is skipped on non-Windows —
the shared profile's CurrentUserID is a Windows SID that no Linux
connection could ever match (ipnserver/actor.go CheckProfileAccess,
ipnlocal CheckIPNConnectionAllowed, and the profiles.go startup scan).
- docs/wsl1.md documents the setup.
Also folded in, unrelated to the feature but carried by the original PRs:
a genuine upstream bug fix in cmd/tsconnect/build.go (the precompressDist
error check tested the wrong, provably-nil variable, so precompression
failures were silently swallowed), two comment typos, a redundant break,
tempfork/sshtest tidy-ups, a .golangci.yml lint-exemption widening,
.github/workflows fork guards on every job (so the fork never runs
upstream's CI), and .gitignore entries for the dual-boot patched binaries.
Squashed from gt4o4/tailscale #2 + #3 + the 2026-08-07 workflow guards;
re-merged onto upstream main 2026-09-10 (the fuzz job we had guarded was
removed upstream).
Wenri
added a commit
that referenced
this pull request
Oct 19, 2026
Lets a Linux tailscaled present as the same tailnet node as a dual-boot
Windows install, sharing one state file:
- version.OS() reports "windows" on linux when TS_OS_SPOOF_WINDOWS is set.
Opt-in per machine on purpose: an established 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.
- The LocalAPI's Windows-only LocalUserID gate is skipped on non-Windows —
the shared profile's CurrentUserID is a Windows SID that no Linux
connection could ever match (ipnserver/actor.go CheckProfileAccess,
ipnlocal CheckIPNConnectionAllowed, and the profiles.go startup scan).
- docs/wsl1.md documents the setup.
Also folded in, unrelated to the feature but carried by the original PRs:
a genuine upstream bug fix in cmd/tsconnect/build.go (the precompressDist
error check tested the wrong, provably-nil variable, so precompression
failures were silently swallowed), two comment typos, a redundant break,
tempfork/sshtest tidy-ups, a .golangci.yml lint-exemption widening,
.github/workflows fork guards on every job (so the fork never runs
upstream's CI), and .gitignore entries for the dual-boot patched binaries.
Squashed from gt4o4/tailscale #2 + #3 + the 2026-08-07 workflow guards;
re-merged onto upstream main 2026-09-10 (the fuzz job we had guarded was
removed upstream).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Makes the Linux→"windows" OS spoof opt-in via
TS_OS_SPOOF_WINDOWS=1, and addsdocs/wsl1.mdcovering WSL1 deployment.Why: with the spoof always on, an established Linux node gets flagged by control ("node OS changed since last connection"), receives an empty peer list and an empty SSH policy — Tailscale SSH stops working entirely. Observed live on slack-wsl when deploying a build of HEAD.
Also documented: WSL1 requires a
CGO_ENABLED=1tailscaled: WSL1's tgkill lacks the same-thread-group exception, so non-cgo Gosyscall.Setuid(AllThreadsSyscall) spins forever at 100% CPU in the SSH incubator when dropping root for command execution (the "ssh runs a command → tailscaled 100% CPU" bug). glibc's setuid signals sibling threads before transitioning, so the cgo build works — verified end-to-end on slack-wsl (ssh command, PTY command, and SFTP all pass; non-cgo incubator hangs in A/B test).Verified: cgo build 1.101.167 deployed on slack-wsl with this gate; node reports linux, 36 peers, SSH commands return in ~0.1s.