Skip to content

version,docs: gate Linux OS spoof behind TS_OS_SPOOF_WINDOWS - #3

Merged
Wenri merged 3 commits into
mainfrom
wsl1-cgo-docs
Jul 6, 2026
Merged

Wenri merged 3 commits into
mainfrom
wsl1-cgo-docs

Conversation

@Wenri

@Wenri Wenri commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator

Makes the Linux→"windows" OS spoof opt-in via TS_OS_SPOOF_WINDOWS=1, and adds docs/wsl1.md covering 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=1 tailscaled: WSL1's tgkill lacks the same-thread-group exception, so non-cgo Go syscall.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.

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>
Copilot AI review requested due to automatic review settings July 6, 2026 23:35

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 behind TS_OS_SPOOF_WINDOWS (parsed once via sync.OnceValue).
  • Add docs/wsl1.md describing 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.

Comment thread version/prop.go Outdated
Comment thread docs/wsl1.md Outdated
Wenri and others added 2 commits July 7, 2026 01:44
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>
Copilot finished work on behalf of Wenri July 6, 2026 23:47
@Wenri
Wenri merged commit 347b625 into main Jul 6, 2026
54 of 58 checks passed
@Wenri
Wenri deleted the wsl1-cgo-docs branch July 6, 2026 23:58
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).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants