Compare commits

...

49 Commits

Author SHA1 Message Date
Aaron Klotz
ffb37f54c8 ipn/ipnlocal: remove windows exception from profile migration
The check in question results in profiles never being migrated to backend
prefs on Windows clients. We should be doing that on Windows too.

This should be save vis-a-vis unattended mode since we won't see the
unmigrated prefs until the GUI signs in.

Fixes #7398

Signed-off-by: Aaron Klotz <aaron@tailscale.com>
2023-02-28 13:11:47 -07:00
Mihai Parparita
780c56e119 ipn/ipnlocal: add delegated interface information to /interfaces PeerAPI handler
Exposes the delegated interface data added by #7248 in the debug
endpoint. I would have found it useful when working on that PR, and
it may be handy in the future as well.

Also makes the interfaces table slightly easier to parse by adding
borders to it. To make then nicer-looking, the CSP was relaxed to allow
inline styles.

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
2023-02-27 09:39:49 -08:00
Charlotte Brandhorst-Satzkorn
e484e1c0fc words: just words, nothing but words (#7384)
nothing in relation to fish at all.

Signed-off-by: Charlotte Brandhorst-Satzkorn <charlotte@tailscale.com>
2023-02-26 11:29:45 -08:00
Denton Gentry
bf7573c9ee cmd/nginx-auth: build for arm64
Fixes https://github.com/tailscale/tailscale/issues/6978

Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2023-02-25 17:16:31 -08:00
Denton Gentry
9ab992e7a1 syncs: re-enable TestWatchMultipleValues
We've updated to a different set of CI machines since this test
was disabled.

Fixes https://github.com/tailscale/tailscale/issues/1513

Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2023-02-25 17:03:16 -08:00
Maisem Ali
0582829e00 ssh/tailssh: try launching commands with /usr/bin/login on macOS
Updates #4939

Co-authored-by: Adam Eijdenberg <adam@continusec.com>
Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-02-25 15:46:34 -08:00
Charlotte Brandhorst-Satzkorn
e851d134cf words: grasping at straws... wait, do straws have tails? (#7376)
One might argue they have two, but until that hypothesis can be proven
these tails and scales will have to do!

Signed-off-by: Charlotte Brandhorst-Satzkorn <charlotte@tailscale.com>
2023-02-24 21:10:43 -08:00
David Anderson
04be5ea725 release/dist/cli: default to "all" for list if no filters given
Signed-off-by: David Anderson <danderson@tailscale.com>
2023-02-24 15:26:46 -08:00
Jordan Whited
d4122c9f0a cmd/tailscale/cli: fix TestUpdatePrefs over Tailscale SSH (#7374)
Fixes #7373

Signed-off-by: Jordan Whited <jordan@tailscale.com>
2023-02-24 15:26:23 -08:00
David Anderson
b0eba129e6 .github/workflows: add a pass/fail verdict job to the test workflow
Github requires explicitly listing every single job within a workflow
that is required for status checks, instead of letting you list entire
workflows. This is ludicrous, and apparently this nonsense is the
workaround.

Signed-off-by: David Anderson <danderson@tailscale.com>
2023-02-24 23:00:22 +00:00
David Anderson
0ab6a7e7f5 .github/workflows: try to make the merge queue actually run CI
Signed-off-by: David Anderson <danderson@tailscale.com>
2023-02-24 22:31:47 +00:00
David Anderson
587eb32a83 release/dist: add forgotten license headers
Signed-off-by: David Anderson <danderson@tailscale.com>
2023-02-24 22:21:28 +00:00
David Anderson
cf74ee49ee release/dist/cli: factor out the CLI boilerplace from cmd/dist
Signed-off-by: David Anderson <danderson@tailscale.com>
2023-02-24 22:21:28 +00:00
David Anderson
fc4b25d9fd release: open-source release build logic for unix packages
Updates tailscale/corp#9221

Signed-off-by: David Anderson <danderson@tailscale.com>
2023-02-24 21:31:09 +00:00
David Crawshaw
44e027abca tsnet: add data transfer test
Signed-off-by: David Crawshaw <crawshaw@tailscale.com>
2023-02-24 19:18:32 +00:00
David Crawshaw
46467e39c2 logtail: allow multiple calls to Shutdown
Signed-off-by: David Crawshaw <crawshaw@tailscale.com>
2023-02-24 19:18:32 +00:00
David Crawshaw
daa2f1c66e tsnet: add Up method to block until ready
Signed-off-by: David Crawshaw <crawshaw@tailscale.com>
2023-02-24 18:57:55 +00:00
David Anderson
64181e17c8 tool/gocross: support local toolchain for development
This makes gocross and its bootstrap script understand an absolute
path in go.toolchain.rev to mean "use the given toolchain directly".

Signed-off-by: David Anderson <danderson@tailscale.com>
2023-02-24 05:55:46 +00:00
David Anderson
66621ab38e tool/gocross: embed the version explicitly with linker flags
We need to build gocross from multiple repos, but Go's innate
git hash embedding only works when you build gocross from this repo,
not when you build it from elsewhere via 'go build
tailscale.com/tool/gocross'. Instead, explicitly embed the version
found with 'git rev-parse HEAD', which will work from any git repo.

Signed-off-by: David Anderson <danderson@tailscale.com>
2023-02-24 03:11:29 +00:00
David Anderson
7444dabb68 tool/gocross: do all the bootstrap steps in a subshell
This avoids accidentally overwriting variables from the input
environment, which might non-deterministically change the behavior
of gocross.

Signed-off-by: David Anderson <danderson@tailscale.com>
2023-02-24 03:11:29 +00:00
Tom DNetto
abc874b04e tka: add public API on NodeKeySignature key information
This is needed in the coordination server.

Signed-off-by: Tom DNetto <tom@tailscale.com>
2023-02-23 19:20:39 +00:00
License Updater
61a345c8e1 licenses: update win/apple licenses
Signed-off-by: License Updater <noreply@tailscale.com>
2023-02-23 18:56:08 +00:00
Maisem Ali
06a10125fc cmd/k8s-operator: set hostinfo.Package
This allows identifying the operator.

Updates #5055

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-02-23 02:33:23 +00:00
David Anderson
7e65a11df5 tool/gocross: write the wrapper script directly, rather than printing
Turns out directing the printed script into the bootstrap location leads
to irritating "text file busy" problems and then having to muck about with
tempfiles and chmod and all that. Instead, have gocross write everything
with the right values.

Signed-off-by: David Anderson <danderson@tailscale.com>
2023-02-23 02:03:14 +00:00
David Anderson
499d82af8a tool/gocross: add command to print the wrapper shell script
So that when importing and using gocross from other repos, there's
an easy way to get at the right wrapper script that's in sync with
the gocross binary.

Signed-off-by: David Anderson <danderson@tailscale.com>
2023-02-22 20:48:37 +00:00
David Anderson
860734aed9 tool/gocross: a tool for building Tailscale binaries
Signed-off-by: David Anderson <danderson@tailscale.com>
2023-02-22 17:55:16 +00:00
David Anderson
0b8f89c79c cmd/tsconnect: find the build dir independently of -trimpath
trimmed builds don't have absolute path information in executable
metadata, which leads the runtime.Caller approach failing
mysteriously in yarn with complaints about relative package paths.

So, instead of using embedded package metadata to find paths,
expect that we're being invoked within the tailscale repo, and
locate the tsconnect directory that way.

Signed-off-by: David Anderson <danderson@tailscale.com>
2023-02-22 00:02:47 +00:00
Tom DNetto
f9b746846f tailcfg: add RPC structs for /tka/affected-sigs
These RPCs will be used to power the future 'tailscale lock remove' default behavior
of resigning signatures for which trust is about to be removed.

Signed-off-by: Tom DNetto <tom@tailscale.com>
2023-02-21 21:58:38 +00:00
Andrew Dunham
e220fa65dd util/ringbuffer: move generic ringbuffer from corp repo
Also add some basic tests for this implementation.

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: I307ebb6db91d0c172657befb276b38ccb638f828
2023-02-21 19:11:08 +00:00
Shayne Sweeney
cd18bb68a4 gitignore: Add personal .gopath and nix build /result
Signed-off-by: Shayne Sweeney <shayne@tailscale.com>
2023-02-21 03:01:19 +00:00
License Updater
d38abe90be licenses: update tailscale{,d} licenses
Signed-off-by: License Updater <noreply@tailscale.com>
2023-02-19 05:43:53 +00:00
David Anderson
5a2fa3aa95 .github/workflows: add armv5 and armv7 cross tests
armv5 because that's what we ship to most downstreams right now,
armv7 becuase that's what we want to ship more of.

Fixes https://github.com/tailscale/tailscale/issues/7269

Signed-off-by: David Anderson <danderson@tailscale.com>
2023-02-19 05:16:11 +00:00
Maisem Ali
5787989d74 ssh/tailssh: detect user shell correctly on darwin
Updates #6213

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-02-19 01:52:13 +00:00
Denton Gentry
6dabb34c7f scripts/installer.sh: add GalliumOS and Sangoma Linux
Fixes https://github.com/tailscale/tailscale/issues/6541
Fixes https://github.com/tailscale/tailscale/issues/6555

Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2023-02-18 23:13:05 +00:00
David Anderson
093139fafd .github/workflows: fix non-collapsing CI status in PRs
CI status doesn't collapse into "everything OK" if a job gets
skipped. Instead, always run the job, but skip its only step in PRs.

Signed-off-by: David Anderson <danderson@tailscale.com>
2023-02-18 22:31:05 +00:00
Nicolas BERNARD
3db894b78c client/tailscale: add tags field to Device struct
Fixes #7302

Signed-off-by: Nicolas BERNARD <nikkau@nikkau.net>
2023-02-18 21:14:40 +00:00
David Anderson
306c8a713c .github/workflows: run CI and CodeQL in the merge queue
Signed-off-by: David Anderson <danderson@tailscale.com>
2023-02-18 19:21:07 +00:00
David Anderson
149de5e6d6 build_dist.sh: use cmd/mkversion to get version data
Replaces the former shell goop, which was a shell reimplementation
of a subset of version/mkversion.

Signed-off-by: David Anderson <danderson@tailscale.com>
2023-02-18 19:05:39 +00:00
David Anderson
45d9784f9d version/mkversion: allow collecting version only from this repo
With this change, you can collect version info from either a git
checkout of the tailscale.com Go module (this repo), or a git
checkout of a repo that imports the tailscale.com Go module.

Signed-off-by: David Anderson <danderson@tailscale.com>
2023-02-18 19:05:39 +00:00
Flakes Updater
303048a7d5 go.mod.sri: update SRI hash for go.mod changes
Signed-off-by: Flakes Updater <noreply@tailscale.com>
2023-02-18 18:24:33 +00:00
Brad Fitzpatrick
e8a028cf82 go.mod: bump x/crypto
No particular reason. Just good point of our release cycle for some #cleanup.

It also makes dependabot happy about something we're not using?

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
2023-02-18 18:04:02 +00:00
Maisem Ali
a7eab788e4 metrics: add SetInt64 to ease using LabelMap for gauge metrics
Set is provided by the underlying Map.

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-02-18 17:43:43 +00:00
Denton Gentry
1ba0b7fd79 scripts/installer.sh: add postmarketos support.
Fixes https://github.com/tailscale/tailscale/issues/7300

Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2023-02-18 16:06:47 +00:00
David Anderson
7ca54c890e version/mkversion: add exports for major/minor/patch
build_dist.sh needs the minor version by itself, for some reason.

Signed-off-by: David Anderson <danderson@tailscale.com>
2023-02-18 05:21:05 +00:00
David Anderson
8ed27d65ef version/mkversion: add documentation, rename internal terminology
Signed-off-by: David Anderson <danderson@tailscale.com>
2023-02-18 05:21:05 +00:00
David Anderson
1dadbbb72a version/mkversion: open-source version generation logic
In preparation for moving more of the release building here too.

Signed-off-by: David Anderson <danderson@tailscale.com>
2023-02-18 05:21:05 +00:00
Maisem Ali
d811c5a7f0 cmd/tailscale/cli: handle home dir correctly on macOS for kubeconfig
This ensures that we put the kubeconfig in the correct directory from within the macOS Sandbox when
paired with tailscale/corp@3035ef7

Updates #7220

Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-02-17 01:18:52 +00:00
Maisem Ali
4a99481a11 .github/workflows: set TS_FUZZ_CURRENTLY_BROKEN to false
Signed-off-by: Maisem Ali <maisem@tailscale.com>
2023-02-17 01:18:42 +00:00
Will Norris
8b9ee7a558 Makefile: add help text to Makefile
https://rosszurowski.com/log/2022/makefiles#self-documenting-makefiles
Signed-off-by: Will Norris <will@tailscale.com>
2023-02-16 22:39:09 +00:00
63 changed files with 4135 additions and 216 deletions

View File

@@ -17,6 +17,8 @@ on:
pull_request:
# The branches below must be a subset of the branches above
branches: [ main ]
merge_group:
branches: [ main ]
schedule:
- cron: '31 14 * * 5'

View File

@@ -14,7 +14,7 @@ env:
# This variable toggles the fuzz job between two modes:
# - false: we expect fuzzing to be happy, and should report failure if it's not.
# - true: we expect fuzzing is broken, and should report failure if it start working.
TS_FUZZ_CURRENTLY_BROKEN: true
TS_FUZZ_CURRENTLY_BROKEN: false
on:
push:
@@ -24,6 +24,9 @@ on:
pull_request:
branches:
- "*"
merge_group:
branches:
- "main"
concurrency:
# For PRs, later CI runs preempt previous ones. e.g. a force push on a PR
@@ -55,6 +58,7 @@ jobs:
GOARCH: ${{ matrix.goarch }}
- name: build variant CLIs
run: |
export TS_USE_TOOLCHAIN=1
./build_dist.sh --extra-small ./cmd/tailscaled
./build_dist.sh --box ./cmd/tailscaled
./build_dist.sh --extra-small --box ./cmd/tailscaled
@@ -144,6 +148,12 @@ jobs:
goarch: "386" # thanks yaml
- goos: linux
goarch: loong64
- goos: linux
goarch: arm
goarm: "5"
- goos: linux
goarch: arm
goarm: "7"
# macOS
- goos: darwin
goarch: amd64
@@ -169,6 +179,7 @@ jobs:
env:
GOOS: ${{ matrix.goos }}
GOARCH: ${{ matrix.goarch }}
GOARM: ${{ matrix.goarm }}
CGO_ENABLED: "0"
- name: build tests
run: ./tool/go test -exec=true ./...
@@ -351,8 +362,7 @@ jobs:
GOARCH: ${{ matrix.goarch }}
notify_slack:
# Only notify slack for merged commits, not PR failures.
if: failure() && github.event_name == 'push'
if: always()
# Any of these jobs failing causes a slack notification.
needs:
- android
@@ -371,6 +381,14 @@ jobs:
runs-on: ubuntu-22.04
steps:
- name: notify
# Only notify slack for merged commits, not PR failures.
#
# It may be tempting to move this condition into the job's 'if' block, but
# don't: Github only collapses the test list into "everything is OK" if
# all jobs succeeded. A skipped job results in the list staying expanded.
# By having the job always run, but skipping its only step as needed, we
# let the CI output collapse nicely in PRs.
if: failure() && github.event_name == 'push'
uses: ruby/action-slack@v3.0.0
with:
payload: |
@@ -386,3 +404,27 @@ jobs:
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
check_mergeability:
if: always()
runs-on: ubuntu-22.04
needs:
- android
- test
- windows
- vm
- cross
- ios
- wasm
- fuzz
- depaware
- go_generate
- go_mod_tidy
- licenses
- staticcheck
steps:
- name: Decide if change is okay to merge
if: github.event_name != 'push'
uses: re-actors/alls-green@release/v1
with:
jobs: ${{ toJSON(needs) }}

View File

@@ -21,6 +21,7 @@ jobs:
# GOROOT is specified so that the Go/Wasm that is trigged by build-pk
# also picks up our custom Go toolchain.
run: |
export TS_USE_TOOLCHAIN=1
./build_dist.sh tailscale.com/cmd/tsconnect
GOROOT="${HOME}/.cache/tailscale-go" ./tsconnect build-pkg

9
.gitignore vendored
View File

@@ -26,5 +26,14 @@ cmd/tailscaled/tailscaled
# Ignore personal VS Code settings
.vscode/
# Support personal project-specific GOPATH
.gopath/
# Ignore nix build result path
/result
# Ignore direnv nix-shell environment cache
.direnv/
/gocross
/dist

View File

@@ -2,16 +2,13 @@ IMAGE_REPO ?= tailscale/tailscale
SYNO_ARCH ?= "amd64"
SYNO_DSM ?= "7"
usage:
echo "See Makefile"
vet:
vet: ## Run go vet
./tool/go vet ./...
tidy:
tidy: ## Run go mod tidy
./tool/go mod tidy
updatedeps:
updatedeps: ## Update depaware deps
# depaware (via x/tools/go/packages) shells back to "go", so make sure the "go"
# it finds in its $$PATH is the right one.
PATH="$$(./tool/go env GOROOT)/bin:$$PATH" ./tool/go run github.com/tailscale/depaware --update \
@@ -19,7 +16,7 @@ updatedeps:
tailscale.com/cmd/tailscale \
tailscale.com/cmd/derper
depaware:
depaware: ## Run depaware checks
# depaware (via x/tools/go/packages) shells back to "go", so make sure the "go"
# it finds in its $$PATH is the right one.
PATH="$$(./tool/go env GOROOT)/bin:$$PATH" ./tool/go run github.com/tailscale/depaware --check \
@@ -27,42 +24,42 @@ depaware:
tailscale.com/cmd/tailscale \
tailscale.com/cmd/derper
buildwindows:
buildwindows: ## Build tailscale CLI for windows/amd64
GOOS=windows GOARCH=amd64 ./tool/go install tailscale.com/cmd/tailscale tailscale.com/cmd/tailscaled
build386:
build386: ## Build tailscale CLI for linux/386
GOOS=linux GOARCH=386 ./tool/go install tailscale.com/cmd/tailscale tailscale.com/cmd/tailscaled
buildlinuxarm:
buildlinuxarm: ## Build tailscale CLI for linux/arm
GOOS=linux GOARCH=arm ./tool/go install tailscale.com/cmd/tailscale tailscale.com/cmd/tailscaled
buildwasm:
buildwasm: ## Build tailscale CLI for js/wasm
GOOS=js GOARCH=wasm ./tool/go install ./cmd/tsconnect/wasm ./cmd/tailscale/cli
buildlinuxloong64:
buildlinuxloong64: ## Build tailscale CLI for linux/loong64
GOOS=linux GOARCH=loong64 ./tool/go install tailscale.com/cmd/tailscale tailscale.com/cmd/tailscaled
buildmultiarchimage:
buildmultiarchimage: ## Build (and optionally push) multiarch docker image
./build_docker.sh
check: staticcheck vet depaware buildwindows build386 buildlinuxarm buildwasm
check: staticcheck vet depaware buildwindows build386 buildlinuxarm buildwasm ## Perform basic checks and compilation tests
staticcheck:
staticcheck: ## Run staticcheck.io checks
./tool/go run honnef.co/go/tools/cmd/staticcheck -- $$(./tool/go list ./... | grep -v tempfork)
spk:
spk: ## Build synology package for ${SYNO_ARCH} architecture and ${SYNO_DSM} DSM version
PATH="${PWD}/tool:${PATH}" ./tool/go run github.com/tailscale/tailscale-synology@main -o tailscale.spk --source=. --goarch=${SYNO_ARCH} --dsm-version=${SYNO_DSM}
spkall:
spkall: ## Build synology packages for all architectures and DSM versions
mkdir -p spks
PATH="${PWD}/tool:${PATH}" ./tool/go run github.com/tailscale/tailscale-synology@main -o spks --source=. --goarch=all --dsm-version=all
pushspk: spk
pushspk: spk ## Push and install synology package on ${SYNO_HOST} host
echo "Pushing SPK to root@${SYNO_HOST} (env var SYNO_HOST) ..."
scp tailscale.spk root@${SYNO_HOST}:
ssh root@${SYNO_HOST} /usr/syno/bin/synopkg install tailscale.spk
publishdevimage:
publishdevimage: ## Build and publish tailscale image to location specified by ${REPO}
@test -n "${REPO}" || (echo "REPO=... required; e.g. REPO=ghcr.io/${USER}/tailscale" && exit 1)
@test "${REPO}" != "tailscale/tailscale" || (echo "REPO=... must not be tailscale/tailscale" && exit 1)
@test "${REPO}" != "ghcr.io/tailscale/tailscale" || (echo "REPO=... must not be ghcr.io/tailscale/tailscale" && exit 1)
@@ -70,10 +67,18 @@ publishdevimage:
@test "${REPO}" != "ghcr.io/tailscale/k8s-operator" || (echo "REPO=... must not be ghcr.io/tailscale/k8s-operator" && exit 1)
TAGS=latest REPOS=${REPO} PUSH=true TARGET=client ./build_docker.sh
publishdevoperator:
publishdevoperator: ## Build and publish k8s-operator image to location specified by ${REPO}
@test -n "${REPO}" || (echo "REPO=... required; e.g. REPO=ghcr.io/${USER}/tailscale" && exit 1)
@test "${REPO}" != "tailscale/tailscale" || (echo "REPO=... must not be tailscale/tailscale" && exit 1)
@test "${REPO}" != "ghcr.io/tailscale/tailscale" || (echo "REPO=... must not be ghcr.io/tailscale/tailscale" && exit 1)
@test "${REPO}" != "tailscale/k8s-operator" || (echo "REPO=... must not be tailscale/k8s-operator" && exit 1)
@test "${REPO}" != "ghcr.io/tailscale/k8s-operator" || (echo "REPO=... must not be ghcr.io/tailscale/k8s-operator" && exit 1)
TAGS=latest REPOS=${REPO} PUSH=true TARGET=operator ./build_docker.sh
help: ## Show this help
@echo "\nSpecify a command. The choices are:\n"
@grep -hE '^[0-9a-zA-Z_-]+:.*?## .*$$' ${MAKEFILE_LIST} | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[0;36m%-20s\033[m %s\n", $$1, $$2}'
@echo ""
.PHONY: help
.DEFAULT_GOAL := help

View File

@@ -11,42 +11,25 @@
set -eu
IFS=".$IFS" read -r major minor patch <VERSION.txt
git_hash=$(git rev-parse HEAD)
if ! git diff-index --quiet HEAD; then
git_hash="${git_hash}-dirty"
fi
base_hash=$(git rev-list --max-count=1 HEAD -- VERSION.txt)
change_count=$(git rev-list --count HEAD "^$base_hash")
short_hash=$(echo "$git_hash" | cut -c1-9)
if expr "$minor" : "[0-9]*[13579]$" >/dev/null; then
patch="$change_count"
change_suffix=""
elif [ "$change_count" != "0" ]; then
change_suffix="-$change_count"
else
change_suffix=""
go="go"
if [ -n "${TS_USE_TOOLCHAIN:-}" ]; then
go="./tool/go"
fi
long_suffix="$change_suffix-t$short_hash"
MINOR="$major.$minor"
SHORT="$MINOR.$patch"
LONG="${SHORT}$long_suffix"
GIT_HASH="$git_hash"
eval `$go run ./cmd/mkversion`
if [ "$1" = "shellvars" ]; then
cat <<EOF
VERSION_MINOR="$MINOR"
VERSION_SHORT="$SHORT"
VERSION_LONG="$LONG"
VERSION_GIT_HASH="$GIT_HASH"
VERSION_MINOR="$VERSION_MINOR"
VERSION_SHORT="$VERSION_SHORT"
VERSION_LONG="$VERSION_LONG"
VERSION_GIT_HASH="$VERSION_GIT_HASH"
EOF
exit 0
fi
tags=""
ldflags="-X tailscale.com/version.longStamp=${LONG} -X tailscale.com/version.shortStamp=${SHORT}"
ldflags="-X tailscale.com/version.longStamp=${VERSION_LONG} -X tailscale.com/version.shortStamp=${VERSION_SHORT}"
# build_dist.sh arguments must precede go build arguments.
while [ "$#" -gt 1 ]; do

View File

@@ -44,17 +44,18 @@ type Device struct {
Name string `json:"name"`
Hostname string `json:"hostname"`
ClientVersion string `json:"clientVersion"` // Empty for external devices.
UpdateAvailable bool `json:"updateAvailable"` // Empty for external devices.
OS string `json:"os"`
Created string `json:"created"` // Empty for external devices.
LastSeen string `json:"lastSeen"`
KeyExpiryDisabled bool `json:"keyExpiryDisabled"`
Expires string `json:"expires"`
Authorized bool `json:"authorized"`
IsExternal bool `json:"isExternal"`
MachineKey string `json:"machineKey"` // Empty for external devices.
NodeKey string `json:"nodeKey"`
ClientVersion string `json:"clientVersion"` // Empty for external devices.
UpdateAvailable bool `json:"updateAvailable"` // Empty for external devices.
OS string `json:"os"`
Tags []string `json:"tags"`
Created string `json:"created"` // Empty for external devices.
LastSeen string `json:"lastSeen"`
KeyExpiryDisabled bool `json:"keyExpiryDisabled"`
Expires string `json:"expires"`
Authorized bool `json:"authorized"`
IsExternal bool `json:"isExternal"`
MachineKey string `json:"machineKey"` // Empty for external devices.
NodeKey string `json:"nodeKey"`
// BlocksIncomingConnections is configured via the device's
// Tailscale client preferences. This field is only reported

28
cmd/dist/dist.go vendored Normal file
View File

@@ -0,0 +1,28 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// The dist command builds Tailscale release packages for distribution.
package main
import (
"context"
"errors"
"flag"
"log"
"os"
"tailscale.com/release/dist"
"tailscale.com/release/dist/cli"
"tailscale.com/release/dist/unixpkgs"
)
func getTargets() ([]dist.Target, error) {
return unixpkgs.Targets(), nil
}
func main() {
cmd := cli.CLI(getTargets)
if err := cmd.ParseAndRun(context.Background(), os.Args[1:]); err != nil && !errors.Is(err, flag.ErrHelp) {
log.Fatal(err)
}
}

View File

@@ -39,10 +39,12 @@ import (
"sigs.k8s.io/controller-runtime/pkg/source"
"sigs.k8s.io/yaml"
"tailscale.com/client/tailscale"
"tailscale.com/hostinfo"
"tailscale.com/ipn"
"tailscale.com/ipn/store/kubestore"
"tailscale.com/tsnet"
"tailscale.com/types/logger"
"tailscale.com/types/opt"
"tailscale.com/util/dnsname"
)
@@ -61,7 +63,7 @@ func main() {
clientSecretPath = defaultEnv("CLIENT_SECRET_FILE", "")
image = defaultEnv("PROXY_IMAGE", "tailscale/tailscale:latest")
tags = defaultEnv("PROXY_TAGS", "tag:k8s")
shouldRunAuthProxy = defaultEnv("AUTH_PROXY", "false")
shouldRunAuthProxy = defaultBool("AUTH_PROXY", false)
)
var opts []kzap.Opts
@@ -95,6 +97,13 @@ func main() {
}
tsClient := tailscale.NewClient("-", nil)
tsClient.HTTPClient = credentials.Client(context.Background())
if shouldRunAuthProxy {
hostinfo.SetPackage("k8s-operator-proxy")
} else {
hostinfo.SetPackage("k8s-operator")
}
s := &tsnet.Server{
Hostname: hostname,
Logf: zlog.Named("tailscaled").Debugf,
@@ -225,7 +234,7 @@ waitOnline:
}
startlog.Infof("Startup complete, operator running")
if shouldRunAuthProxy == "true" {
if shouldRunAuthProxy {
rc, err := rest.TransportFor(restConfig)
if err != nil {
startlog.Fatalf("could not get rest transport: %v", err)
@@ -696,6 +705,15 @@ func getSingleObject[T any, O ptrObject[T]](ctx context.Context, c client.Client
return ret, nil
}
func defaultBool(envName string, defVal bool) bool {
vs := os.Getenv(envName)
if vs == "" {
return defVal
}
v, _ := opt.Bool(vs).Get()
return v
}
func defaultEnv(envName, defVal string) string {
v := os.Getenv(envName)
if v == "" {

View File

@@ -0,0 +1,44 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// mkversion gets version info from git and outputs a bunch of shell variables
// that get used elsewhere in the build system to embed version numbers into
// binaries.
package main
import (
"bufio"
"bytes"
"fmt"
"io"
"os"
"time"
"tailscale.com/tailcfg"
"tailscale.com/version/mkversion"
)
func main() {
prefix := ""
if len(os.Args) > 1 {
if os.Args[1] == "--export" {
prefix = "export "
} else {
fmt.Println("usage: mkversion [--export|-h|--help]")
os.Exit(1)
}
}
var b bytes.Buffer
io.WriteString(&b, mkversion.Info().String())
// Copyright and the client capability are not part of the version
// information, but similarly used in Xcode builds to embed in the metadata,
// thus generate them now.
copyright := fmt.Sprintf("Copyright © %d Tailscale Inc. All Rights Reserved.", time.Now().Year())
fmt.Fprintf(&b, "VERSION_COPYRIGHT=%q\n", copyright)
fmt.Fprintf(&b, "VERSION_CAPABILITY=%d\n", tailcfg.CurrentCapabilityVersion)
s := bufio.NewScanner(&b)
for s.Scan() {
fmt.Println(prefix + s.Text())
}
}

View File

@@ -2,30 +2,31 @@
set -e
CGO_ENABLED=0 GOARCH=amd64 GOOS=linux go build -o tailscale.nginx-auth .
VERSION=0.1.3
for ARCH in amd64 arm64; do
CGO_ENABLED=0 GOARCH=${ARCH} GOOS=linux go build -o tailscale.nginx-auth .
VERSION=0.1.2
mkpkg \
--out=tailscale-nginx-auth-${VERSION}-${ARCH}.deb \
--name=tailscale-nginx-auth \
--version=${VERSION} \
--type=deb \
--arch=${ARCH} \
--postinst=deb/postinst.sh \
--postrm=deb/postrm.sh \
--prerm=deb/prerm.sh \
--description="Tailscale NGINX authentication protocol handler" \
--files=./tailscale.nginx-auth:/usr/sbin/tailscale.nginx-auth,./tailscale.nginx-auth.socket:/lib/systemd/system/tailscale.nginx-auth.socket,./tailscale.nginx-auth.service:/lib/systemd/system/tailscale.nginx-auth.service,./README.md:/usr/share/tailscale/nginx-auth/README.md
mkpkg \
--out=tailscale-nginx-auth-${VERSION}-amd64.deb \
--name=tailscale-nginx-auth \
--version=${VERSION} \
--type=deb \
--arch=amd64 \
--postinst=deb/postinst.sh \
--postrm=deb/postrm.sh \
--prerm=deb/prerm.sh \
--description="Tailscale NGINX authentication protocol handler" \
--files=./tailscale.nginx-auth:/usr/sbin/tailscale.nginx-auth,./tailscale.nginx-auth.socket:/lib/systemd/system/tailscale.nginx-auth.socket,./tailscale.nginx-auth.service:/lib/systemd/system/tailscale.nginx-auth.service,./README.md:/usr/share/tailscale/nginx-auth/README.md
mkpkg \
--out=tailscale-nginx-auth-${VERSION}-amd64.rpm \
--name=tailscale-nginx-auth \
--version=${VERSION} \
--type=rpm \
--arch=amd64 \
--postinst=rpm/postinst.sh \
--postrm=rpm/postrm.sh \
--prerm=rpm/prerm.sh \
--description="Tailscale NGINX authentication protocol handler" \
--files=./tailscale.nginx-auth:/usr/sbin/tailscale.nginx-auth,./tailscale.nginx-auth.socket:/lib/systemd/system/tailscale.nginx-auth.socket,./tailscale.nginx-auth.service:/lib/systemd/system/tailscale.nginx-auth.service,./README.md:/usr/share/tailscale/nginx-auth/README.md
mkpkg \
--out=tailscale-nginx-auth-${VERSION}-${ARCH}.rpm \
--name=tailscale-nginx-auth \
--version=${VERSION} \
--type=rpm \
--arch=${ARCH} \
--postinst=rpm/postinst.sh \
--postrm=rpm/postrm.sh \
--prerm=rpm/prerm.sh \
--description="Tailscale NGINX authentication protocol handler" \
--files=./tailscale.nginx-auth:/usr/sbin/tailscale.nginx-auth,./tailscale.nginx-auth.socket:/lib/systemd/system/tailscale.nginx-auth.socket,./tailscale.nginx-auth.service:/lib/systemd/system/tailscale.nginx-auth.service,./README.md:/usr/share/tailscale/nginx-auth/README.md
done

View File

@@ -1078,6 +1078,13 @@ func TestUpdatePrefs(t *testing.T) {
old := getSSHClientEnvVar
getSSHClientEnvVar = func() string { return "100.100.100.100 1 1" }
t.Cleanup(func() { getSSHClientEnvVar = old })
} else if isSSHOverTailscale() {
// The test is being executed over a "real" tailscale SSH
// session, but sshOverTailscale is unset. Make the test appear
// as if it's not over tailscale SSH.
old := getSSHClientEnvVar
getSSHClientEnvVar = func() string { return "" }
t.Cleanup(func() { getSSHClientEnvVar = old })
}
if tt.env.goos == "" {
tt.env.goos = "linux"

View File

@@ -17,6 +17,7 @@ import (
"golang.org/x/exp/slices"
"k8s.io/client-go/util/homedir"
"sigs.k8s.io/yaml"
"tailscale.com/version"
)
func init() {
@@ -39,6 +40,22 @@ The hostname argument should be set to the Tailscale hostname of the peer runnin
Exec: runConfigureKubeconfig,
}
// kubeconfigPath returns the path to the kubeconfig file for the current user.
func kubeconfigPath() string {
var dir string
if version.IsSandboxedMacOS() {
// The HOME environment variable in macOS sandboxed apps is set to
// ~/Library/Containers/<app-id>/Data, but the kubeconfig file is
// located in ~/.kube/config. We rely on the "com.apple.security.temporary-exception.files.home-relative-path.read-write"
// entitlement to access the file.
containerHome := os.Getenv("HOME")
dir, _, _ = strings.Cut(containerHome, "/Library/Containers/")
} else {
dir = homedir.HomeDir()
}
return filepath.Join(dir, ".kube", "config")
}
func runConfigureKubeconfig(ctx context.Context, args []string) error {
if len(args) != 1 {
return errors.New("unknown arguments")
@@ -57,8 +74,7 @@ func runConfigureKubeconfig(ctx context.Context, args []string) error {
return fmt.Errorf("no peer found with hostname %q", hostOrFQDN)
}
targetFQDN = strings.TrimSuffix(targetFQDN, ".")
confPath := filepath.Join(homedir.HomeDir(), ".kube", "config")
if err := setKubeconfigForPeer(targetFQDN, confPath); err != nil {
if err := setKubeconfigForPeer(targetFQDN, kubeconfigPath()); err != nil {
return err
}
printf("kubeconfig configured for %q\n", hostOrFQDN)
@@ -140,9 +156,23 @@ func updateKubeconfig(cfgYaml []byte, fqdn string) ([]byte, error) {
}
func setKubeconfigForPeer(fqdn, filePath string) error {
dir := filepath.Dir(filePath)
if _, err := os.Stat(dir); err != nil {
if !os.IsNotExist(err) {
return err
}
if err := os.Mkdir(dir, 0755); err != nil {
if version.IsSandboxedMacOS() && errors.Is(err, os.ErrPermission) {
// macOS sandboxing prevents us from creating the .kube directory
// in the home directory.
return errors.New("unable to create .kube directory in home directory, please create it manually (e.g. mkdir ~/.kube")
}
return err
}
}
b, err := os.ReadFile(filePath)
if err != nil && !os.IsNotExist(err) {
return err
return fmt.Errorf("reading kubeconfig: %w", err)
}
b, err = updateKubeconfig(b, fqdn)
if err != nil {

View File

@@ -28,10 +28,13 @@ const (
func commonSetup(dev bool) (*esbuild.BuildOptions, error) {
// Change cwd to to where this file lives -- that's where all inputs for
// esbuild and other build steps live.
if _, filename, _, ok := runtime.Caller(0); ok {
if err := os.Chdir(path.Dir(filename)); err != nil {
return nil, fmt.Errorf("Cannot change cwd: %w", err)
}
root, err := findRepoRoot()
if err != nil {
return nil, err
}
tsConnectDir := filepath.Join(root, "cmd", "tsconnect")
if err := os.Chdir(tsConnectDir); err != nil {
return nil, fmt.Errorf("Cannot change cwd: %w", err)
}
if err := installJSDeps(); err != nil {
return nil, fmt.Errorf("Cannot install JS deps: %w", err)
@@ -67,6 +70,22 @@ func commonSetup(dev bool) (*esbuild.BuildOptions, error) {
}, nil
}
func findRepoRoot() (string, error) {
cwd, err := os.Getwd()
if err != nil {
return "", err
}
for {
if _, err := os.Stat(path.Join(cwd, "go.mod")); err == nil {
return cwd, nil
}
if cwd == "/" {
return "", fmt.Errorf("Cannot find repo root")
}
cwd = path.Dir(cwd)
}
}
func commonPkgSetup(dev bool) (*esbuild.BuildOptions, error) {
buildOptions, err := commonSetup(dev)
if err != nil {

View File

@@ -114,4 +114,4 @@
in
flake-utils.lib.eachDefaultSystem (system: flakeForSystem nixpkgs system);
}
# nix-direnv cache busting line: sha256-y6WQxNJQeqKzmG/n1f5EkH7pxdqDyMmBlyfwhSK4wJE=
# nix-direnv cache busting line: sha256-zyyqBRFPNPzPYCMgnbnOy5rb3fkn4XEHZlTlJvwqunM=

4
go.mod
View File

@@ -70,8 +70,9 @@ require (
go.uber.org/zap v1.21.0
go4.org/mem v0.0.0-20210711025021-927187094b94
go4.org/netipx v0.0.0-20220725152314-7e7bdc8411bf
golang.org/x/crypto v0.3.0
golang.org/x/crypto v0.6.0
golang.org/x/exp v0.0.0-20221205204356-47842c84f3db
golang.org/x/mod v0.7.0
golang.org/x/net v0.7.0
golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5
golang.org/x/sync v0.1.0
@@ -304,7 +305,6 @@ require (
go.uber.org/multierr v1.6.0 // indirect
golang.org/x/exp/typeparams v0.0.0-20221208152030-732eee02a75a // indirect
golang.org/x/image v0.0.0-20201208152932-35266b937fa6 // indirect
golang.org/x/mod v0.7.0 // indirect
golang.org/x/text v0.7.0 // indirect
gomodules.xyz/jsonpatch/v2 v2.2.0 // indirect
google.golang.org/appengine v1.6.7 // indirect

View File

@@ -1 +1 @@
sha256-y6WQxNJQeqKzmG/n1f5EkH7pxdqDyMmBlyfwhSK4wJE=
sha256-zyyqBRFPNPzPYCMgnbnOy5rb3fkn4XEHZlTlJvwqunM=

4
go.sum
View File

@@ -1338,8 +1338,8 @@ golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5y
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20211117183948-ae814b36b871/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.3.0 h1:a06MkbcxBrEFc0w0QIZWXrH/9cCX6KJyWbBOIwAn+7A=
golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc=
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=

View File

@@ -670,7 +670,7 @@ func (h *peerAPIHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return
}
if peerAPIRequestShouldGetSecurityHeaders(r) {
w.Header().Set("Content-Security-Policy", `default-src 'none'; frame-ancestors 'none'; script-src 'none'; script-src-elem 'none'; script-src-attr 'none'`)
w.Header().Set("Content-Security-Policy", `default-src 'none'; frame-ancestors 'none'; script-src 'none'; script-src-elem 'none'; script-src-attr 'none'; style-src 'unsafe-inline'`)
w.Header().Set("X-Frame-Options", "DENY")
w.Header().Set("X-Content-Type-Options", "nosniff")
}
@@ -799,15 +799,21 @@ func (h *peerAPIHandler) handleServeInterfaces(w http.ResponseWriter, r *http.Re
fmt.Fprintf(w, "<h3>Could not get the default route: %s</h3>\n", html.EscapeString(err.Error()))
}
if hasCGNATInterface, err := interfaces.HasCGNATInterface(); hasCGNATInterface {
fmt.Fprintln(w, "<p>There is another interface using the CGNAT range.</p>")
} else if err != nil {
fmt.Fprintf(w, "<p>Could not check for CGNAT interfaces: %s</p>\n", html.EscapeString(err.Error()))
}
i, err := interfaces.GetList()
if err != nil {
fmt.Fprintf(w, "Could not get interfaces: %s\n", html.EscapeString(err.Error()))
return
}
fmt.Fprintln(w, "<table>")
fmt.Fprintln(w, "<table style='border-collapse: collapse' border=1 cellspacing=0 cellpadding=2>")
fmt.Fprint(w, "<tr>")
for _, v := range []any{"Index", "Name", "MTU", "Flags", "Addrs"} {
for _, v := range []any{"Index", "Name", "MTU", "Flags", "Addrs", "Extra"} {
fmt.Fprintf(w, "<th>%v</th> ", v)
}
fmt.Fprint(w, "</tr>\n")
@@ -816,6 +822,11 @@ func (h *peerAPIHandler) handleServeInterfaces(w http.ResponseWriter, r *http.Re
for _, v := range []any{iface.Index, iface.Name, iface.MTU, iface.Flags, ipps} {
fmt.Fprintf(w, "<td>%s</td> ", html.EscapeString(fmt.Sprintf("%v", v)))
}
if extras, err := interfaces.InterfaceDebugExtras(iface.Index); err == nil && extras != "" {
fmt.Fprintf(w, "<td>%s</td> ", html.EscapeString(extras))
} else if err != nil {
fmt.Fprintf(w, "<td>%s</td> ", html.EscapeString(err.Error()))
}
fmt.Fprint(w, "</tr>\n")
})
fmt.Fprintln(w, "</table>")

View File

@@ -534,7 +534,7 @@ func newProfileManagerWithGOOS(store ipn.StateStore, logf logger.Logf, goos stri
if err := pm.setPrefsLocked(prefs); err != nil {
return nil, err
}
} else if len(knownProfiles) == 0 && goos != "windows" {
} else if len(knownProfiles) == 0 {
// No known profiles, try a migration.
if err := pm.migrateFromLegacyPrefs(); err != nil {
return nil, err

View File

@@ -41,7 +41,7 @@ and [iOS][]. See also the dependencies in the [Tailscale CLI][].
- [github.com/x448/float16](https://pkg.go.dev/github.com/x448/float16) ([MIT](https://github.com/x448/float16/blob/v0.8.4/LICENSE))
- [go4.org/mem](https://pkg.go.dev/go4.org/mem) ([Apache-2.0](https://github.com/go4org/mem/blob/4f986261bf13/LICENSE))
- [go4.org/netipx](https://pkg.go.dev/go4.org/netipx) ([BSD-3-Clause](https://github.com/go4org/netipx/blob/7e7bdc8411bf/LICENSE))
- [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) ([BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/v0.3.0:LICENSE))
- [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) ([BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/v0.6.0:LICENSE))
- [golang.org/x/exp](https://pkg.go.dev/golang.org/x/exp) ([BSD-3-Clause](https://cs.opensource.google/go/x/exp/+/47842c84:LICENSE))
- [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://cs.opensource.google/go/x/net/+/v0.7.0:LICENSE))
- [golang.org/x/sync/errgroup](https://pkg.go.dev/golang.org/x/sync/errgroup) ([BSD-3-Clause](https://cs.opensource.google/go/x/sync/+/v0.1.0:LICENSE))

View File

@@ -77,7 +77,7 @@ Some packages may only be included on certain architectures or operating systems
- [github.com/x448/float16](https://pkg.go.dev/github.com/x448/float16) ([MIT](https://github.com/x448/float16/blob/v0.8.4/LICENSE))
- [go4.org/mem](https://pkg.go.dev/go4.org/mem) ([Apache-2.0](https://github.com/go4org/mem/blob/927187094b94/LICENSE))
- [go4.org/netipx](https://pkg.go.dev/go4.org/netipx) ([BSD-3-Clause](https://github.com/go4org/netipx/blob/7e7bdc8411bf/LICENSE))
- [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) ([BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/v0.3.0:LICENSE))
- [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) ([BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/v0.6.0:LICENSE))
- [golang.org/x/exp](https://pkg.go.dev/golang.org/x/exp) ([BSD-3-Clause](https://cs.opensource.google/go/x/exp/+/47842c84:LICENSE))
- [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://cs.opensource.google/go/x/net/+/v0.7.0:LICENSE))
- [golang.org/x/sync/errgroup](https://pkg.go.dev/golang.org/x/sync/errgroup) ([BSD-3-Clause](https://cs.opensource.google/go/x/sync/+/v0.1.0:LICENSE))

View File

@@ -31,7 +31,7 @@ Windows][]. See also the dependencies in the [Tailscale CLI][].
- [github.com/x448/float16](https://pkg.go.dev/github.com/x448/float16) ([MIT](https://github.com/x448/float16/blob/v0.8.4/LICENSE))
- [go4.org/mem](https://pkg.go.dev/go4.org/mem) ([Apache-2.0](https://github.com/go4org/mem/blob/4f986261bf13/LICENSE))
- [go4.org/netipx](https://pkg.go.dev/go4.org/netipx) ([BSD-3-Clause](https://github.com/go4org/netipx/blob/7e7bdc8411bf/LICENSE))
- [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) ([BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/v0.3.0:LICENSE))
- [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) ([BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/v0.6.0:LICENSE))
- [golang.org/x/exp](https://pkg.go.dev/golang.org/x/exp) ([BSD-3-Clause](https://cs.opensource.google/go/x/exp/+/47842c84:LICENSE))
- [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://cs.opensource.google/go/x/net/+/v0.7.0:LICENSE))
- [golang.org/x/sync/errgroup](https://pkg.go.dev/golang.org/x/sync/errgroup) ([BSD-3-Clause](https://cs.opensource.google/go/x/sync/+/v0.1.0:LICENSE))

View File

@@ -198,8 +198,9 @@ type Logger struct {
procSequence uint64
flushTimer *time.Timer // used when flushDelay is >0
shutdownStart chan struct{} // closed when shutdown begins
shutdownDone chan struct{} // closed when shutdown complete
shutdownStartMu sync.Mutex // guards the closing of shutdownStart
shutdownStart chan struct{} // closed when shutdown begins
shutdownDone chan struct{} // closed when shutdown complete
}
// SetVerbosityLevel controls the verbosity level that should be
@@ -240,7 +241,16 @@ func (l *Logger) Shutdown(ctx context.Context) error {
close(done)
}()
l.shutdownStartMu.Lock()
select {
case <-l.shutdownStart:
l.shutdownStartMu.Unlock()
return nil
default:
}
close(l.shutdownStart)
l.shutdownStartMu.Unlock()
io.WriteString(l, "logger closing down\n")
<-done

View File

@@ -33,6 +33,11 @@ type LabelMap struct {
expvar.Map
}
// SetInt64 sets the *Int value stored under the given map key.
func (m *LabelMap) SetInt64(key string, v int64) {
m.Get(key).Set(v)
}
// Get returns a direct pointer to the expvar.Int for key, creating it
// if necessary.
func (m *LabelMap) Get(key string) *expvar.Int {

View File

@@ -756,3 +756,15 @@ func HasCGNATInterface() (bool, error) {
}
return hasCGNATInterface, nil
}
var interfaceDebugExtras func(ifIndex int) (string, error)
// InterfaceDebugExtras returns extra debugging information about an interface
// if any (an empty string will be returned if there are no additional details).
// Formatting is platform-dependent and should not be parsed.
func InterfaceDebugExtras(ifIndex int) (string, error) {
if interfaceDebugExtras != nil {
return interfaceDebugExtras(ifIndex)
}
return "", nil
}

View File

@@ -4,6 +4,7 @@
package interfaces
import (
"fmt"
"net"
"strings"
"sync"
@@ -29,6 +30,10 @@ var ifNames struct {
m map[int]string // ifindex => name
}
func init() {
interfaceDebugExtras = interfaceDebugExtrasDarwin
}
// getDelegatedInterface returns the interface index of the underlying interface
// for the given interface index. 0 is returned if the interface does not
// delegate.
@@ -93,3 +98,14 @@ func getDelegatedInterface(ifIndex int) (int, error) {
}
return int(ifr.ifr_delegated), nil
}
func interfaceDebugExtrasDarwin(ifIndex int) (string, error) {
delegated, err := getDelegatedInterface(ifIndex)
if err != nil {
return "", err
}
if delegated == 0 {
return "", nil
}
return fmt.Sprintf("delegated=%d", delegated), nil
}

13
release/deb/debian.postinst.sh Executable file
View File

@@ -0,0 +1,13 @@
if [ "$1" = "configure" ] || [ "$1" = "abort-upgrade" ] || [ "$1" = "abort-deconfigure" ] || [ "$1" = "abort-remove" ] ; then
deb-systemd-helper unmask 'tailscaled.service' >/dev/null || true
if deb-systemd-helper --quiet was-enabled 'tailscaled.service'; then
deb-systemd-helper enable 'tailscaled.service' >/dev/null || true
else
deb-systemd-helper update-state 'tailscaled.service' >/dev/null || true
fi
if [ -d /run/systemd/system ]; then
systemctl --system daemon-reload >/dev/null || true
deb-systemd-invoke restart 'tailscaled.service' >/dev/null || true
fi
fi

17
release/deb/debian.postrm.sh Executable file
View File

@@ -0,0 +1,17 @@
#!/bin/sh
set -e
if [ -d /run/systemd/system ] ; then
systemctl --system daemon-reload >/dev/null || true
fi
if [ -x "/usr/bin/deb-systemd-helper" ]; then
if [ "$1" = "remove" ]; then
deb-systemd-helper mask 'tailscaled.service' >/dev/null || true
fi
if [ "$1" = "purge" ]; then
deb-systemd-helper purge 'tailscaled.service' >/dev/null || true
deb-systemd-helper unmask 'tailscaled.service' >/dev/null || true
rm -rf /var/lib/tailscale
fi
fi

7
release/deb/debian.prerm.sh Executable file
View File

@@ -0,0 +1,7 @@
#!/bin/sh
set -e
if [ "$1" = "remove" ]; then
if [ -d /run/systemd/system ]; then
deb-systemd-invoke stop 'tailscaled.service' >/dev/null || true
fi
fi

140
release/dist/cli/cli.go vendored Normal file
View File

@@ -0,0 +1,140 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// Package cli provides the skeleton of a CLI for building release packages.
package cli
import (
"context"
"errors"
"flag"
"fmt"
"os"
"path/filepath"
"strings"
"time"
"github.com/peterbourgon/ff/v3/ffcli"
"tailscale.com/release/dist"
)
// CLI returns a CLI root command to build release packages.
//
// getTargets is a function that gets run in the Exec function of commands that
// need to know the target list. Its execution is deferred in this way to allow
// customization of command FlagSets with flags that influence the target list.
func CLI(getTargets func() ([]dist.Target, error)) *ffcli.Command {
return &ffcli.Command{
Name: "dist",
ShortUsage: "dist [flags] <command> [command flags]",
ShortHelp: "Build tailscale release packages for distribution",
LongHelp: `For help on subcommands, add --help after: "dist list --help".`,
Subcommands: []*ffcli.Command{
{
Name: "list",
Exec: func(ctx context.Context, args []string) error {
targets, err := getTargets()
if err != nil {
return err
}
return runList(ctx, args, targets)
},
ShortUsage: "dist list [target filters]",
ShortHelp: "List all available release targets.",
LongHelp: strings.TrimSpace(`
If filters are provided, only targets matching at least one filter are listed.
Filters can use glob patterns (* and ?).
`),
},
{
Name: "build",
Exec: func(ctx context.Context, args []string) error {
targets, err := getTargets()
if err != nil {
return err
}
return runBuild(ctx, args, targets)
},
ShortUsage: "dist build [target filters]",
ShortHelp: "Build release files",
FlagSet: (func() *flag.FlagSet {
fs := flag.NewFlagSet("build", flag.ExitOnError)
fs.StringVar(&buildArgs.manifest, "manifest", "", "manifest file to write")
return fs
})(),
LongHelp: strings.TrimSpace(`
If filters are provided, only targets matching at least one filter are built.
Filters can use glob patterns (* and ?).
`),
},
},
Exec: func(context.Context, []string) error { return flag.ErrHelp },
}
}
func runList(ctx context.Context, filters []string, targets []dist.Target) error {
if len(filters) == 0 {
filters = []string{"all"}
}
tgts, err := dist.FilterTargets(targets, filters)
if err != nil {
return err
}
for _, tgt := range tgts {
fmt.Println(tgt)
}
return nil
}
var buildArgs struct {
manifest string
}
func runBuild(ctx context.Context, filters []string, targets []dist.Target) error {
tgts, err := dist.FilterTargets(targets, filters)
if err != nil {
return err
}
if len(tgts) == 0 {
return errors.New("no targets matched (did you mean 'dist build all'?)")
}
st := time.Now()
wd, err := os.Getwd()
if err != nil {
return fmt.Errorf("getting working directory: %w", err)
}
b, err := dist.NewBuild(wd, filepath.Join(wd, "dist"))
if err != nil {
return fmt.Errorf("creating build context: %w", err)
}
defer b.Close()
out, err := b.Build(tgts)
if err != nil {
return fmt.Errorf("building targets: %w", err)
}
if buildArgs.manifest != "" {
// Make the built paths relative to the manifest file.
manifest, err := filepath.Abs(buildArgs.manifest)
if err != nil {
return fmt.Errorf("getting absolute path of manifest: %w", err)
}
fmt.Println(manifest)
fmt.Println(filepath.Join(b.Out, out[0]))
for i := range out {
rel, err := filepath.Rel(filepath.Dir(manifest), filepath.Join(b.Out, out[i]))
if err != nil {
return fmt.Errorf("making path relative: %w", err)
}
out[i] = rel
}
if err := os.WriteFile(manifest, []byte(strings.Join(out, "\n")), 0644); err != nil {
return fmt.Errorf("writing manifest: %w", err)
}
}
fmt.Println("Done! Took", time.Since(st))
return nil
}

271
release/dist/dist.go vendored Normal file
View File

@@ -0,0 +1,271 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// Package dist is a release artifact builder library.
package dist
import (
"errors"
"fmt"
"log"
"os"
"os/exec"
"path/filepath"
"regexp"
"runtime"
"sort"
"strings"
"sync"
"tailscale.com/util/multierr"
"tailscale.com/version/mkversion"
)
// A Target is something that can be build in a Build.
type Target interface {
String() string
Build(build *Build) ([]string, error)
}
// A Build is a build context for Targets.
type Build struct {
// Repo is a path to the root Go module for the build.
Repo string
// Tmp is a temporary directory that gets deleted when the Builder is closed.
Tmp string
// Out is where build artifacts are written.
Out string
// Go is the path to the Go binary to use for building.
Go string
// Version is the version info of the build.
Version mkversion.VersionInfo
// once is a cache of function invocations that should run once per process
// (for example building a helper docker container)
once once
extraMu sync.Mutex
extra map[any]any
goBuilds Memoize[string]
// When running `dist build all` on a cold Go build cache, the fanout of
// gooses and goarches results in a very large number of compile processes,
// which bogs down the build machine.
//
// This throttles the number of concurrent `go build` invocations to the
// number of CPU cores, which empirically keeps the builder responsive
// without impacting overall build time.
goBuildLimit chan struct{}
}
// NewBuild creates a new Build rooted at repo, and writing artifacts to out.
func NewBuild(repo, out string) (*Build, error) {
if err := os.MkdirAll(out, 0750); err != nil {
return nil, fmt.Errorf("creating out dir: %w", err)
}
tmp, err := os.MkdirTemp("", "dist-*")
if err != nil {
return nil, fmt.Errorf("creating tempdir: %w", err)
}
repo, err = findModRoot(repo)
if err != nil {
return nil, fmt.Errorf("finding module root: %w", err)
}
goTool, err := findGo(repo)
if err != nil {
return nil, fmt.Errorf("finding go binary: %w", err)
}
b := &Build{
Repo: repo,
Tmp: tmp,
Out: out,
Go: goTool,
Version: mkversion.Info(),
extra: map[any]any{},
goBuildLimit: make(chan struct{}, runtime.NumCPU()),
}
return b, nil
}
// Close ends the build and cleans up temporary files.
func (b *Build) Close() error {
return os.RemoveAll(b.Tmp)
}
// Build builds all targets concurrently.
func (b *Build) Build(targets []Target) (files []string, err error) {
if len(targets) == 0 {
return nil, errors.New("no targets specified")
}
log.Printf("Building %d targets: %v", len(targets), targets)
var (
wg sync.WaitGroup
errs = make([]error, len(targets))
buildFiles = make([][]string, len(targets))
)
for i, t := range targets {
wg.Add(1)
go func(i int, t Target) {
var err error
defer func() {
errs[i] = err
wg.Done()
}()
fs, err := t.Build(b)
buildFiles[i] = fs
}(i, t)
}
wg.Wait()
for _, fs := range buildFiles {
files = append(files, fs...)
}
sort.Strings(files)
return files, multierr.New(errs...)
}
// Once runs fn if Once hasn't been called with name before.
func (b *Build) Once(name string, fn func() error) error {
return b.once.Do(name, fn)
}
// Extra returns a value from the build's extra state, creating it if necessary.
func (b *Build) Extra(key any, constructor func() any) any {
b.extraMu.Lock()
defer b.extraMu.Unlock()
ret, ok := b.extra[key]
if !ok {
ret = constructor()
b.extra[key] = ret
}
return ret
}
// GoPkg returns the path on disk of pkg.
// The module of pkg must be imported in b.Repo's go.mod.
func (b *Build) GoPkg(pkg string) (string, error) {
bs, err := exec.Command(b.Go, "list", "-f", "{{.Dir}}", pkg).Output()
if err != nil {
return "", fmt.Errorf("finding package %q: %w", pkg, err)
}
return strings.TrimSpace(string(bs)), nil
}
// TmpDir creates and returns a new empty temporary directory.
// The caller does not need to clean up the directory after use, it will get
// deleted by b.Close().
func (b *Build) TmpDir() string {
// Because we're creating all temp dirs in our parent temp dir, the only
// failures that can happen at this point are sequence breaks (e.g. if b.Tmp
// is deleted while stuff is still running). So, panic on error to slightly
// simplify callsites.
ret, err := os.MkdirTemp(b.Tmp, "")
if err != nil {
panic(fmt.Sprintf("creating temp dir: %v", err))
}
return ret
}
// BuildGoBinary builds the Go binary at path and returns the path to the
// binary. Builds are cached by path and env, so each build only happens once
// per process execution.
func (b *Build) BuildGoBinary(path string, env map[string]string) (string, error) {
err := b.Once("init-go", func() error {
log.Printf("Initializing Go toolchain")
// If the build is using a tool/go, it may need to download a toolchain
// and do other initialization. Running `go version` once takes care of
// all of that and avoids that initialization happening concurrently
// later on in builds.
_, err := exec.Command(b.Go, "version").Output()
return err
})
if err != nil {
return "", err
}
buildKey := []any{"go-build", path, env}
return b.goBuilds.Do(buildKey, func() (string, error) {
b.goBuildLimit <- struct{}{}
defer func() { <-b.goBuildLimit }()
var envStrs []string
for k, v := range env {
envStrs = append(envStrs, k+"="+v)
}
sort.Strings(envStrs)
log.Printf("Building %s (with env %s)", path, strings.Join(envStrs, " "))
buildDir := b.TmpDir()
cmd := exec.Command(b.Go, "build", "-o", buildDir, path)
cmd.Dir = b.Repo
cmd.Env = os.Environ()
for k, v := range env {
cmd.Env = append(cmd.Env, k+"="+v)
}
cmd.Env = append(cmd.Env, "TS_USE_GOCROSS=1")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return "", err
}
out := filepath.Join(buildDir, filepath.Base(path))
if env["GOOS"] == "windows" || env["GOOS"] == "windowsgui" {
out += ".exe"
}
return out, nil
})
}
func findModRoot(path string) (string, error) {
for {
modpath := filepath.Join(path, "go.mod")
if _, err := os.Stat(modpath); err == nil {
return path, nil
} else if !errors.Is(err, os.ErrNotExist) {
return "", err
}
path = filepath.Dir(path)
if path == "/" {
return "", fmt.Errorf("no go.mod found in %q or any parent directory", path)
}
}
}
func findGo(path string) (string, error) {
toolGo := filepath.Join(path, "tool/go")
if _, err := os.Stat(toolGo); err == nil {
return toolGo, nil
}
toolGo, err := exec.LookPath("go")
if err != nil {
return "", err
}
return toolGo, nil
}
// FilterTargets returns the subset of targets that match any of the filters.
// If filters is empty, returns all targets.
func FilterTargets(targets []Target, filters []string) ([]Target, error) {
var filts []*regexp.Regexp
for _, f := range filters {
if f == "all" {
return targets, nil
}
filt, err := regexp.Compile(f)
if err != nil {
return nil, fmt.Errorf("invalid filter %q: %w", f, err)
}
filts = append(filts, filt)
}
var ret []Target
for _, t := range targets {
for _, filt := range filts {
if filt.MatchString(t.String()) {
ret = append(ret, t)
break
}
}
}
return ret, nil
}

86
release/dist/memoize.go vendored Normal file
View File

@@ -0,0 +1,86 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package dist
import (
"sync"
"tailscale.com/util/deephash"
)
// MemoizedFn is a function that memoize.Do can call.
type MemoizedFn[T any] func() (T, error)
// Memoize runs MemoizedFns and remembers their results.
type Memoize[O any] struct {
mu sync.Mutex
cond *sync.Cond
outs map[deephash.Sum]O
errs map[deephash.Sum]error
inflight map[deephash.Sum]bool
}
// Do runs fn and returns its result.
// fn is only run once per unique key. Subsequent Do calls with the same key
// return the memoized result of the first call, even if fn is a different
// function.
func (m *Memoize[O]) Do(key any, fn MemoizedFn[O]) (ret O, err error) {
m.mu.Lock()
defer m.mu.Unlock()
if m.cond == nil {
m.cond = sync.NewCond(&m.mu)
m.outs = map[deephash.Sum]O{}
m.errs = map[deephash.Sum]error{}
m.inflight = map[deephash.Sum]bool{}
}
k := deephash.Hash(&key)
for m.inflight[k] {
m.cond.Wait()
}
if err := m.errs[k]; err != nil {
var ret O
return ret, err
}
if ret, ok := m.outs[k]; ok {
return ret, nil
}
m.inflight[k] = true
m.mu.Unlock()
defer func() {
m.mu.Lock()
delete(m.inflight, k)
if err != nil {
m.errs[k] = err
} else {
m.outs[k] = ret
}
m.cond.Broadcast()
}()
ret, err = fn()
if err != nil {
var ret O
return ret, err
}
return ret, nil
}
// once is like memoize, but for functions that don't return non-error values.
type once struct {
m Memoize[any]
}
// Do runs fn.
// fn is only run once per unique key. Subsequent Do calls with the same key
// return the memoized result of the first call, even if fn is a different
// function.
func (o *once) Do(key any, fn func() error) error {
_, err := o.m.Do(key, func() (any, error) {
return nil, fn()
})
return err
}

378
release/dist/unixpkgs/pkgs.go vendored Normal file
View File

@@ -0,0 +1,378 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// Package unixpkgs contains dist Targets for building unix Tailscale packages.
package unixpkgs
import (
"archive/tar"
"compress/gzip"
"errors"
"fmt"
"io"
"log"
"os"
"path/filepath"
"strings"
"time"
"github.com/goreleaser/nfpm"
"tailscale.com/release/dist"
)
type tgzTarget struct {
filenameArch string // arch to use in filename instead of deriving from goenv["GOARCH"]
goenv map[string]string
}
func (t *tgzTarget) arch() string {
if t.filenameArch != "" {
return t.filenameArch
}
return t.goenv["GOARCH"]
}
func (t *tgzTarget) os() string {
return t.goenv["GOOS"]
}
func (t *tgzTarget) String() string {
return fmt.Sprintf("%s/%s/tgz", t.os(), t.arch())
}
func (t *tgzTarget) Build(b *dist.Build) ([]string, error) {
var filename string
if t.goenv["GOOS"] == "linux" {
// Linux used to be the only tgz architecture, so we didn't put the OS
// name in the filename.
filename = fmt.Sprintf("tailscale_%s_%s.tgz", b.Version.Short, t.arch())
} else {
filename = fmt.Sprintf("tailscale_%s_%s_%s.tgz", b.Version.Short, t.os(), t.arch())
}
ts, err := b.BuildGoBinary("tailscale.com/cmd/tailscale", t.goenv)
if err != nil {
return nil, err
}
tsd, err := b.BuildGoBinary("tailscale.com/cmd/tailscaled", t.goenv)
if err != nil {
return nil, err
}
log.Printf("Building %s", filename)
out := filepath.Join(b.Out, filename)
f, err := os.Create(out)
if err != nil {
return nil, err
}
defer f.Close()
gw := gzip.NewWriter(f)
defer gw.Close()
tw := tar.NewWriter(gw)
defer tw.Close()
buildTime := time.Now()
addFile := func(src, dst string, mode int64) error {
f, err := os.Open(src)
if err != nil {
return err
}
defer f.Close()
fi, err := f.Stat()
if err != nil {
return err
}
hdr := &tar.Header{
Name: dst,
Size: fi.Size(),
Mode: mode,
ModTime: buildTime,
Uid: 0,
Gid: 0,
Uname: "root",
Gname: "root",
}
if err := tw.WriteHeader(hdr); err != nil {
return err
}
if _, err = io.Copy(tw, f); err != nil {
return err
}
return nil
}
addDir := func(name string) error {
hdr := &tar.Header{
Name: name + "/",
Mode: 0755,
ModTime: buildTime,
Uid: 0,
Gid: 0,
Uname: "root",
Gname: "root",
}
return tw.WriteHeader(hdr)
}
dir := strings.TrimSuffix(filename, ".tgz")
if err := addDir(dir); err != nil {
return nil, err
}
if err := addFile(tsd, filepath.Join(dir, "tailscaled"), 0755); err != nil {
return nil, err
}
if err := addFile(ts, filepath.Join(dir, "tailscale"), 0755); err != nil {
return nil, err
}
if t.os() == "linux" {
dir = filepath.Join(dir, "systemd")
if err := addDir(dir); err != nil {
return nil, err
}
tailscaledDir, err := b.GoPkg("tailscale.com/cmd/tailscaled")
if err != nil {
return nil, err
}
if err := addFile(filepath.Join(tailscaledDir, "tailscaled.service"), filepath.Join(dir, "tailscaled.service"), 0644); err != nil {
return nil, err
}
if err := addFile(filepath.Join(tailscaledDir, "tailscaled.defaults"), filepath.Join(dir, "tailscaled.defaults"), 0644); err != nil {
return nil, err
}
}
if err := tw.Close(); err != nil {
return nil, err
}
if err := gw.Close(); err != nil {
return nil, err
}
if err := f.Close(); err != nil {
return nil, err
}
return []string{filename}, nil
}
type debTarget struct {
goenv map[string]string
}
func (t *debTarget) os() string {
return t.goenv["GOOS"]
}
func (t *debTarget) arch() string {
return t.goenv["GOARCH"]
}
func (t *debTarget) String() string {
return fmt.Sprintf("linux/%s/deb", t.goenv["GOARCH"])
}
func (t *debTarget) Build(b *dist.Build) ([]string, error) {
if t.os() != "linux" {
return nil, errors.New("deb only supported on linux")
}
ts, err := b.BuildGoBinary("tailscale.com/cmd/tailscale", t.goenv)
if err != nil {
return nil, err
}
tsd, err := b.BuildGoBinary("tailscale.com/cmd/tailscaled", t.goenv)
if err != nil {
return nil, err
}
tailscaledDir, err := b.GoPkg("tailscale.com/cmd/tailscaled")
if err != nil {
return nil, err
}
repoDir, err := b.GoPkg("tailscale.com")
if err != nil {
return nil, err
}
arch := debArch(t.arch())
info := nfpm.WithDefaults(&nfpm.Info{
Name: "tailscale",
Arch: arch,
Platform: "linux",
Version: b.Version.Short,
Maintainer: "Tailscale Inc <info@tailscale.com>",
Description: "The easiest, most secure, cross platform way to use WireGuard + oauth2 + 2FA/SSO",
Homepage: "https://www.tailscale.com",
License: "MIT",
Section: "net",
Priority: "extra",
Overridables: nfpm.Overridables{
Files: map[string]string{
ts: "/usr/bin/tailscale",
tsd: "/usr/sbin/tailscaled",
filepath.Join(tailscaledDir, "tailscaled.service"): "/lib/systemd/system/tailscaled.service",
},
ConfigFiles: map[string]string{
filepath.Join(tailscaledDir, "tailscaled.defaults"): "/etc/default/tailscaled",
},
Scripts: nfpm.Scripts{
PostInstall: filepath.Join(repoDir, "release/deb/debian.postinst.sh"),
PreRemove: filepath.Join(repoDir, "release/deb/debian.prerm.sh"),
PostRemove: filepath.Join(repoDir, "release/deb/debian.postrm.sh"),
},
Depends: []string{"iptables", "iproute2"},
Recommends: []string{"tailscale-archive-keyring (>= 1.35.181)"},
Replaces: []string{"tailscale-relay"},
Conflicts: []string{"tailscale-relay"},
},
})
pkg, err := nfpm.Get("deb")
if err != nil {
return nil, err
}
filename := fmt.Sprintf("tailscale_%s_%s.deb", b.Version.Short, arch)
log.Printf("Building %s", filename)
f, err := os.Create(filepath.Join(b.Out, filename))
if err != nil {
return nil, err
}
defer f.Close()
if err := pkg.Package(info, f); err != nil {
return nil, err
}
if err := f.Close(); err != nil {
return nil, err
}
return []string{filename}, nil
}
type rpmTarget struct {
goenv map[string]string
}
func (t *rpmTarget) os() string {
return t.goenv["GOOS"]
}
func (t *rpmTarget) arch() string {
return t.goenv["GOARCH"]
}
func (t *rpmTarget) String() string {
return fmt.Sprintf("linux/%s/rpm", t.arch())
}
func (t *rpmTarget) Build(b *dist.Build) ([]string, error) {
if t.os() != "linux" {
return nil, errors.New("rpm only supported on linux")
}
ts, err := b.BuildGoBinary("tailscale.com/cmd/tailscale", t.goenv)
if err != nil {
return nil, err
}
tsd, err := b.BuildGoBinary("tailscale.com/cmd/tailscaled", t.goenv)
if err != nil {
return nil, err
}
tailscaledDir, err := b.GoPkg("tailscale.com/cmd/tailscaled")
if err != nil {
return nil, err
}
repoDir, err := b.GoPkg("tailscale.com")
if err != nil {
return nil, err
}
arch := rpmArch(t.arch())
info := nfpm.WithDefaults(&nfpm.Info{
Name: "tailscale",
Arch: arch,
Platform: "linux",
Version: b.Version.Short,
Maintainer: "Tailscale Inc <info@tailscale.com>",
Description: "The easiest, most secure, cross platform way to use WireGuard + oauth2 + 2FA/SSO",
Homepage: "https://www.tailscale.com",
License: "MIT",
Overridables: nfpm.Overridables{
Files: map[string]string{
ts: "/usr/bin/tailscale",
tsd: "/usr/sbin/tailscaled",
filepath.Join(tailscaledDir, "tailscaled.service"): "/lib/systemd/system/tailscaled.service",
},
ConfigFiles: map[string]string{
filepath.Join(tailscaledDir, "tailscaled.defaults"): "/etc/default/tailscaled",
},
// SELinux policy on e.g. CentOS 8 forbids writing to /var/cache.
// Creating an empty directory at install time resolves this issue.
EmptyFolders: []string{"/var/cache/tailscale"},
Scripts: nfpm.Scripts{
PostInstall: filepath.Join(repoDir, "release/rpm/rpm.postinst.sh"),
PreRemove: filepath.Join(repoDir, "release/rpm/rpm.prerm.sh"),
PostRemove: filepath.Join(repoDir, "release/rpm/rpm.postrm.sh"),
},
Depends: []string{"iptables", "iproute"},
Replaces: []string{"tailscale-relay"},
Conflicts: []string{"tailscale-relay"},
RPM: nfpm.RPM{
Group: "Network",
},
},
})
pkg, err := nfpm.Get("rpm")
if err != nil {
return nil, err
}
filename := fmt.Sprintf("tailscale_%s_%s.rpm", b.Version.Short, arch)
log.Printf("Building %s", filename)
f, err := os.Create(filepath.Join(b.Out, filename))
if err != nil {
return nil, err
}
defer f.Close()
if err := pkg.Package(info, f); err != nil {
return nil, err
}
if err := f.Close(); err != nil {
return nil, err
}
return []string{filename}, nil
}
// debArch returns the debian arch name for the given Go arch name.
// nfpm also does this translation internally, but we need to do it outside nfpm
// because we also need the filename to be correct.
func debArch(arch string) string {
switch arch {
case "386":
return "i386"
case "arm":
// TODO: this is supposed to be "armel" for GOARM=5, and "armhf" for
// GOARM=6 and 7. But we have some tech debt to pay off here before we
// can ship more than 1 ARM deb, so for now match redo's behavior of
// shipping armv5 binaries in an armv7 trenchcoat.
return "armhf"
default:
return arch
}
}
// rpmArch returns the RPM arch name for the given Go arch name.
// nfpm also does this translation internally, but we need to do it outside nfpm
// because we also need the filename to be correct.
func rpmArch(arch string) string {
switch arch {
case "amd64":
return "x86_64"
case "386":
return "i386"
case "arm":
return "armv7hl"
case "arm64":
return "aarch64"
default:
return arch
}
}

119
release/dist/unixpkgs/targets.go vendored Normal file
View File

@@ -0,0 +1,119 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package unixpkgs
import (
"fmt"
"sort"
"strings"
"tailscale.com/release/dist"
_ "github.com/goreleaser/nfpm/deb"
_ "github.com/goreleaser/nfpm/rpm"
)
func Targets() []dist.Target {
var ret []dist.Target
for goosgoarch := range tarballs {
goos, goarch := splitGoosGoarch(goosgoarch)
ret = append(ret, &tgzTarget{
goenv: map[string]string{
"GOOS": goos,
"GOARCH": goarch,
},
})
}
for goosgoarch := range debs {
goos, goarch := splitGoosGoarch(goosgoarch)
ret = append(ret, &debTarget{
goenv: map[string]string{
"GOOS": goos,
"GOARCH": goarch,
},
})
}
for goosgoarch := range rpms {
goos, goarch := splitGoosGoarch(goosgoarch)
ret = append(ret, &rpmTarget{
goenv: map[string]string{
"GOOS": goos,
"GOARCH": goarch,
},
})
}
// Special case: AMD Geode is 386 with softfloat. Tarballs only since it's
// an ancient architecture.
ret = append(ret, &tgzTarget{
filenameArch: "geode",
goenv: map[string]string{
"GOOS": "linux",
"GOARCH": "386",
"GO386": "softfloat",
},
})
sort.Slice(ret, func(i, j int) bool {
return ret[i].String() < ret[j].String()
})
return ret
}
var (
tarballs = map[string]bool{
"linux/386": true,
"linux/amd64": true,
"linux/arm": true,
"linux/arm64": true,
"linux/mips64": true,
"linux/mips64le": true,
"linux/mips": true,
"linux/mipsle": true,
"linux/riscv64": true,
// TODO: more tarballs we could distribute, but don't currently. Leaving
// out for initial parity with redo.
// "darwin/amd64": true,
// "darwin/arm64": true,
// "freebsd/amd64": true,
// "openbsd/amd64": true,
}
debs = map[string]bool{
"linux/386": true,
"linux/amd64": true,
"linux/arm": true,
"linux/arm64": true,
"linux/riscv64": true,
// TODO: maybe mipses, we accidentally started building them at some
// point even though they probably don't work right.
// "linux/mips": true,
// "linux/mipsle": true,
// "linux/mips64": true,
// "linux/mips64le": true,
}
rpms = map[string]bool{
"linux/386": true,
"linux/amd64": true,
"linux/arm": true,
"linux/arm64": true,
"linux/riscv64": true,
// TODO: maybe mipses, we accidentally started building them at some
// point even though they probably don't work right.
// "linux/mips": true,
// "linux/mipsle": true,
// "linux/mips64": true,
// "linux/mips64le": true,
}
)
func splitGoosGoarch(s string) (string, string) {
goos, goarch, ok := strings.Cut(s, "/")
if !ok {
panic(fmt.Sprintf("invalid target %q", s))
}
return goos, goarch
}

41
release/rpm/rpm.postinst.sh Executable file
View File

@@ -0,0 +1,41 @@
# $1 == 1 for initial installation.
# $1 == 2 for upgrades.
if [ $1 -eq 1 ] ; then
# Normally, the tailscale-relay package would request shutdown of
# its service before uninstallation. Unfortunately, the
# tailscale-relay package we distributed doesn't have those
# scriptlets. We definitely want relaynode to be stopped when
# installing tailscaled though, so we blindly try to turn off
# relaynode here.
#
# However, we also want this package installation to look like an
# upgrade from relaynode! Therefore, if relaynode is currently
# enabled, we want to also enable tailscaled. If relaynode is
# currently running, we also want to start tailscaled.
#
# If there doesn't seem to be an active or enabled relaynode on
# the system, we follow the RPM convention for package installs,
# which is to not enable or start the service.
relaynode_enabled=0
relaynode_running=0
if systemctl is-enabled tailscale-relay.service >/dev/null 2>&1; then
relaynode_enabled=1
fi
if systemctl is-active tailscale-relay.service >/dev/null 2>&1; then
relaynode_running=1
fi
systemctl --no-reload disable tailscale-relay.service >/dev/null 2>&1 || :
systemctl stop tailscale-relay.service >/dev/null 2>&1 || :
if [ $relaynode_enabled -eq 1 ]; then
systemctl enable tailscaled.service >/dev/null 2>&1 || :
else
systemctl preset tailscaled.service >/dev/null 2>&1 || :
fi
if [ $relaynode_running -eq 1 ]; then
systemctl start tailscaled.service >/dev/null 2>&1 || :
fi
fi

8
release/rpm/rpm.postrm.sh Executable file
View File

@@ -0,0 +1,8 @@
# $1 == 0 for uninstallation.
# $1 == 1 for removing old package during upgrade.
systemctl daemon-reload >/dev/null 2>&1 || :
if [ $1 -ge 1 ] ; then
# Package upgrade, not uninstall
systemctl try-restart tailscaled.service >/dev/null 2>&1 || :
fi

8
release/rpm/rpm.prerm.sh Executable file
View File

@@ -0,0 +1,8 @@
# $1 == 0 for uninstallation.
# $1 == 1 for removing old package during upgrade.
if [ $1 -eq 0 ] ; then
# Package removal, not upgrade
systemctl --no-reload disable tailscaled.service > /dev/null 2>&1 || :
systemctl stop tailscaled.service > /dev/null 2>&1 || :
fi

View File

@@ -113,6 +113,12 @@ main() {
APT_KEY_TYPE="keyring"
fi
;;
galliumos)
OS="ubuntu"
PACKAGETYPE="apt"
VERSION="bionic"
APT_KEY_TYPE="legacy"
;;
raspbian)
OS="$ID"
VERSION="$VERSION_CODENAME"
@@ -171,7 +177,7 @@ main() {
VERSION=""
PACKAGETYPE="dnf"
;;
rocky|almalinux|nobara|openmandriva)
rocky|almalinux|nobara|openmandriva|sangoma)
OS="fedora"
VERSION=""
PACKAGETYPE="dnf"
@@ -211,6 +217,11 @@ main() {
VERSION="$VERSION_ID"
PACKAGETYPE="apk"
;;
postmarketos)
OS="alpine"
VERSION="$VERSION_ID"
PACKAGETYPE="apk"
;;
nixos)
echo "Please add Tailscale to your NixOS configuration directly:"
echo

View File

@@ -16,4 +16,4 @@
) {
src = ./.;
}).shellNix
# nix-direnv cache busting line: sha256-y6WQxNJQeqKzmG/n1f5EkH7pxdqDyMmBlyfwhSK4wJE=
# nix-direnv cache busting line: sha256-zyyqBRFPNPzPYCMgnbnOy5rb3fkn4XEHZlTlJvwqunM=

View File

@@ -83,7 +83,7 @@ func (ss *sshSession) newIncubatorCommand() (cmd *exec.Cmd) {
case "sftp":
isSFTP = true
case "":
name = loginShell(ss.conn.localUser.Uid)
name = loginShell(ss.conn.localUser)
if rawCmd := ss.RawCommand(); rawCmd != "" {
args = append(args, "-c", rawCmd)
} else {
@@ -124,7 +124,11 @@ func (ss *sshSession) newIncubatorCommand() (cmd *exec.Cmd) {
} else {
if isShell {
incubatorArgs = append(incubatorArgs, "--shell")
// Currently (2022-05-09) `login` is only used for shells
}
if isShell || runtime.GOOS == "darwin" {
// Only the macOS version of the login command supports executing a
// command, all other versions only support launching a shell
// without taking any arguments.
if lp, err := exec.LookPath("login"); err == nil {
incubatorArgs = append(incubatorArgs, "--login-cmd="+lp)
}
@@ -215,11 +219,12 @@ func beIncubator(args []string) error {
euid := uint64(os.Geteuid())
runningAsRoot := euid == 0
if runningAsRoot && ia.isShell && ia.loginCmdPath != "" && ia.hasTTY {
// If we are trying to launch a login shell, just exec into login
// instead. We can only do this if a TTY was requested, otherwise login
// exits immediately, which breaks things likes mosh and VSCode.
return unix.Exec(ia.loginCmdPath, ia.loginArgs(), os.Environ())
if runningAsRoot && ia.loginCmdPath != "" {
// Check if we can exec into the login command instead of trying to
// incubate ourselves.
if la := ia.loginArgs(); la != nil {
return unix.Exec(ia.loginCmdPath, la, os.Environ())
}
}
// Inform the system that we are about to log someone in.
@@ -572,15 +577,23 @@ func (ss *sshSession) startWithStdPipes() (err error) {
return nil
}
func loginShell(uid string) string {
func loginShell(u *user.User) string {
switch runtime.GOOS {
case "linux":
out, _ := exec.Command("getent", "passwd", uid).Output()
out, _ := exec.Command("getent", "passwd", u.Uid).Output()
// out is "root:x:0:0:root:/root:/bin/bash"
f := strings.SplitN(string(out), ":", 10)
if len(f) > 6 {
return strings.TrimSpace(f[6]) // shell
}
case "darwin":
// Note: /Users/username is key, and not the same as u.HomeDir.
out, _ := exec.Command("dscl", ".", "-read", filepath.Join("/Users", u.Username), "UserShell").Output()
// out is "UserShell: /bin/bash"
s, ok := strings.CutPrefix(string(out), "UserShell: ")
if ok {
return strings.TrimSpace(s)
}
}
if e := os.Getenv("SHELL"); e != "" {
return e
@@ -590,7 +603,7 @@ func loginShell(uid string) string {
func envForUser(u *user.User) []string {
return []string{
fmt.Sprintf("SHELL=" + loginShell(u.Uid)),
fmt.Sprintf("SHELL=" + loginShell(u)),
fmt.Sprintf("USER=" + u.Username),
fmt.Sprintf("HOME=" + u.HomeDir),
fmt.Sprintf("PATH=" + defaultPathForUser(u)),
@@ -699,9 +712,43 @@ func fileExists(path string) bool {
return err == nil
}
// loginArgs returns the arguments to use to exec the login binary.
// It returns nil if the login binary should not be used.
// The login binary is only used:
// - on darwin, if the client is requesting a shell or a command.
// - on linux and BSD, if the client is requesting a shell with a TTY.
func (ia *incubatorArgs) loginArgs() []string {
if ia.isSFTP {
return nil
}
switch runtime.GOOS {
case "darwin":
args := []string{
ia.loginCmdPath,
"-f", // already authenticated
// login typically discards the previous environment, but we want to
// preserve any environment variables that we currently have.
"-p",
"-h", ia.remoteIP, // -h is "remote host"
ia.localUser,
}
if !ia.hasTTY {
args[2] = "-pq" // -q is "quiet" which suppresses the login banner
}
if ia.cmdName != "" {
args = append(args, ia.cmdName)
args = append(args, ia.cmdArgs...)
}
return args
case "linux":
if !ia.isShell || !ia.hasTTY {
// We can only use login command if a shell was requested with a TTY. If
// there is no TTY, login exits immediately, which breaks things likes
// mosh and VSCode.
return nil
}
if distro.Get() == distro.Arch && !fileExists("/etc/pam.d/remote") {
// See https://github.com/tailscale/tailscale/issues/4924
//
@@ -711,7 +758,13 @@ func (ia *incubatorArgs) loginArgs() []string {
return []string{ia.loginCmdPath, "-f", ia.localUser, "-p"}
}
return []string{ia.loginCmdPath, "-f", ia.localUser, "-h", ia.remoteIP, "-p"}
case "darwin", "freebsd", "openbsd":
case "freebsd", "openbsd":
if !ia.isShell || !ia.hasTTY {
// We can only use login command if a shell was requested with a TTY. If
// there is no TTY, login exits immediately, which breaks things likes
// mosh and VSCode.
return nil
}
return []string{ia.loginCmdPath, "-fp", "-h", ia.remoteIP, ia.localUser}
}
panic("unimplemented")

View File

@@ -8,8 +8,6 @@ import (
"sync"
"testing"
"time"
"tailscale.com/util/cibuild"
)
// Time-based tests are fundamentally flaky.
@@ -47,12 +45,6 @@ func TestWatchContended(t *testing.T) {
}
func TestWatchMultipleValues(t *testing.T) {
if cibuild.On() {
// On the CI machine, it sometimes takes 500ms to start a new goroutine.
// When this happens, we don't get enough events quickly enough.
// Nothing's wrong, and it's not worth working around. Just skip the test.
t.Skip("flaky on CI")
}
mu := new(sync.Mutex)
ctx, cancel := context.WithCancel(context.Background())
defer cancel() // not necessary, but keep vet happy

View File

@@ -238,3 +238,27 @@ type TKASubmitSignatureRequest struct {
type TKASubmitSignatureResponse struct {
// Nothing. (yet?)
}
// TKASignaturesUsingKeyRequest asks the control plane for
// all signatures which are signed by the provided keyID.
//
// This is the request schema for a /tka/affected-sigs RPC.
type TKASignaturesUsingKeyRequest struct {
// Version is the client's capabilities.
Version CapabilityVersion
// NodeKey is the client's current node key.
NodeKey key.NodePublic
// KeyID is the key we are querying using.
KeyID tkatype.KeyID
}
// TKASignaturesUsingKeyResponse is the JSON response to
// a /tka/affected-sigs RPC.
//
// It enumerates all signatures which are signed by the
// queried keyID.
type TKASignaturesUsingKeyResponse struct {
Signatures []tkatype.MarshaledSignature
}

View File

@@ -96,6 +96,18 @@ type NodeKeySignature struct {
WrappingPubkey []byte `cbor:"6,keyasint,omitempty"`
}
// UnverifiedWrappingPublic returns the public key which must sign a
// signature which embeds this one, if any.
//
// See docs on NodeKeySignature.WrappingPubkey & SigRotation for documentation
// about wrapping public keys.
//
// SAFETY: The caller MUST verify the signature using
// Authority.NodeKeyAuthorized if treating this as authentic information.
func (s NodeKeySignature) UnverifiedWrappingPublic() (pub ed25519.PublicKey, ok bool) {
return s.wrappingPublic()
}
// wrappingPublic returns the public key which must sign a signature which
// embeds this one, if any.
func (s NodeKeySignature) wrappingPublic() (pub ed25519.PublicKey, ok bool) {
@@ -115,6 +127,15 @@ func (s NodeKeySignature) wrappingPublic() (pub ed25519.PublicKey, ok bool) {
}
}
// UnverifiedAuthorizingKeyID returns the KeyID of the key which authorizes
// this signature.
//
// SAFETY: The caller MUST verify the signature using
// Authority.NodeKeyAuthorized if treating this as authentic information.
func (s NodeKeySignature) UnverifiedAuthorizingKeyID() (tkatype.KeyID, error) {
return s.authorizingKeyID()
}
// authorizingKeyID returns the KeyID of the key trusted by network-lock which authorizes
// this signature.
func (s NodeKeySignature) authorizingKeyID() (tkatype.KeyID, error) {

79
tool/go
View File

@@ -4,81 +4,4 @@
# currently-desired version from https://github.com/tailscale/go,
# downloading it first if necessary.
set -eu
log() {
echo "$@" >&2
}
DEFAULT_TOOLCHAIN_DIR="${HOME}/.cache/tailscale-go"
TOOLCHAIN="${TOOLCHAIN-${DEFAULT_TOOLCHAIN_DIR}}"
TOOLCHAIN_GO="${TOOLCHAIN}/bin/go"
read -r REV < "$(dirname "$0")/../go.toolchain.rev"
# Fast, quiet path, when Tailscale is already current.
if [ -e "${TOOLCHAIN_GO}" ]; then
short_hash=$("${TOOLCHAIN_GO}" version | sed 's/.*-ts//; s/ .*//')
case $REV in
"$short_hash"*)
unset GOROOT
exec "${TOOLCHAIN_GO}" "$@"
esac
fi
# This works for linux and darwin, which is sufficient
# (we do not build tailscale-go for other targets).
host_os=$(uname -s | tr A-Z a-z)
host_arch="$(uname -m)"
if [ "$host_arch" = "aarch64" ]; then
# Go uses the name "arm64".
host_arch="arm64"
elif [ "$host_arch" = "x86_64" ]; then
# Go uses the name "amd64".
host_arch="amd64"
fi
get_cached() {
if [ ! -d "$TOOLCHAIN" ]; then
mkdir -p "$TOOLCHAIN"
fi
archive="$TOOLCHAIN-$REV.tar.gz"
mark="$TOOLCHAIN.extracted"
extracted=
# Ignore the error from read, which may error if the mark file does not contain a line end.
read -r extracted < "$mark" || true
if [ "$extracted" = "$REV" ] && [ -e "${TOOLCHAIN_GO}" ]; then
# already ok
log "Go toolchain '$REV' already extracted."
return 0
fi
rm -f "$archive.new" "$TOOLCHAIN.extracted"
if [ ! -e "$archive" ]; then
log "Need to download go '$REV'."
curl -f -L -o "$archive.new" "https://github.com/tailscale/go/releases/download/build-${REV}/${host_os}-${host_arch}.tar.gz"
rm -f "$archive"
mv "$archive.new" "$archive"
fi
log "Extracting tailscale/go rev '$REV'" >&2
log " into '$TOOLCHAIN'." >&2
rm -rf "$TOOLCHAIN"
mkdir -p "$TOOLCHAIN"
(cd "$TOOLCHAIN" && tar --strip-components=1 -xf "$archive")
echo "$REV" >$mark
}
if [ "${REV}" = "SKIP" ] ||
[ "${host_os}" != "darwin" -a "${host_os}" != "linux" ] ||
[ "${host_arch}" != "amd64" -a "${host_arch}" != "arm64" ]; then
# Use whichever go is available
exec go "$@"
else
get_cached
fi
unset GOROOT
exec "${TOOLCHAIN_GO}" "$@"
exec "$(dirname "$0")/../tool/gocross/gocross-wrapper.sh" "$@"

183
tool/gocross/autoflags.go Normal file
View File

@@ -0,0 +1,183 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package main
import (
"fmt"
"runtime"
"strings"
"tailscale.com/version/mkversion"
)
// Autoflags adjusts the commandline argv into a new commandline
// newArgv and envvar alterations in env.
func Autoflags(argv []string, goroot string) (newArgv []string, env *Environment, err error) {
return autoflagsForTest(argv, NewEnvironment(), goroot, runtime.GOOS, runtime.GOARCH, mkversion.Info)
}
func autoflagsForTest(argv []string, env *Environment, goroot, nativeGOOS, nativeGOARCH string, getVersion func() mkversion.VersionInfo) (newArgv []string, newEnv *Environment, err error) {
// This is where all our "automatic flag injection" decisions get
// made. Modifying this code will modify the environment variables
// and commandline flags that the final `go` tool invocation will
// receive.
//
// When choosing between making this code concise or readable,
// please err on the side of being readable. Our build
// environments are relatively complicated by Go standards, and we
// want to keep it intelligible and malleable for our future
// selves.
var (
subcommand = ""
targetOS = env.Get("GOOS", nativeGOOS)
targetArch = env.Get("GOARCH", nativeGOARCH)
buildFlags = []string{"-trimpath"}
cgoCflags = []string{"-O3", "-std=gnu11"}
cgoLdflags []string
ldflags []string
tags = []string{"tailscale_go"}
cgo = false
failReflect = false
)
if len(argv) > 1 {
subcommand = argv[1]
}
switch subcommand {
case "build", "env", "install", "run", "test", "list":
default:
return argv, env, nil
}
vi := getVersion()
ldflags = []string{
"-X", "tailscale.com/version.longStamp=" + vi.Long,
"-X", "tailscale.com/version.shortStamp=" + vi.Short,
"-X", "tailscale.com/version.gitCommitStamp=" + vi.GitHash,
"-X", "tailscale.com/version.extraGitCommitStamp=" + vi.OtherHash,
}
switch targetOS {
case "linux":
// Getting Go to build a static binary with cgo enabled is a
// minor ordeal. The incantations you apparently need are
// documented at: https://github.com/golang/go/issues/26492
tags = append(tags, "osusergo", "netgo")
cgo = targetOS == nativeGOOS && targetArch == nativeGOARCH
// When in a Nix environment, the gcc package is built with only dynamic
// versions of glibc. You can get a static version of glibc via
// pkgs.glibc.static, but then you are reliant on Nix's gcc wrapper
// magic to inject that as a -L path to linker invocations.
//
// We can't rely on that magic linker flag injection, because that
// injection breaks redo's go machinery for dynamic go+cgo linking due
// to flag ordering issues that we can't easily fix (since the nix
// machinery controls the flag ordering, not us).
//
// So, instead, we unset NIX_LDFLAGS in our nix shell, which disables
// the magic linker flag passing; and we have shell.nix drop the path to
// the static glibc files in GOCROSS_GLIBC_DIR. Finally, we reinject it
// into the build process here, so that the linker can find static glibc
// and complete a static-with-cgo linkage.
extldflags := []string{"-static"}
if glibcDir := env.Get("GOCROSS_GLIBC_DIR", ""); glibcDir != "" {
extldflags = append(extldflags, "-L", glibcDir)
}
// -extldflags, when it contains multiple external linker flags, must be
// quoted in its entirety as a member of -ldflags. Source:
// https://github.com/golang/go/issues/6234
ldflags = append(ldflags, fmt.Sprintf("'-extldflags=%s'", strings.Join(extldflags, " ")))
case "windowsgui":
// Fake GOOS that translates to "windows, but building GUI .exes not console .exes"
targetOS = "windows"
ldflags = append(ldflags, "-H", "windowsgui", "-s")
case "windows":
ldflags = append(ldflags, "-H", "windows", "-s")
case "ios":
failReflect = true
fallthrough
case "darwin":
cgo = nativeGOOS == "darwin"
tags = append(tags, "omitidna", "omitpemdecrypt")
if env.IsSet("XCODE_VERSION_ACTUAL") {
var xcodeFlags []string
// Minimum OS version being targeted, results in
// e.g. -mmacosx-version-min=11.3
minOSKey := env.Get("DEPLOYMENT_TARGET_CLANG_FLAG_NAME", "")
minOSVal := env.Get(env.Get("DEPLOYMENT_TARGET_CLANG_ENV_NAME", ""), "")
xcodeFlags = append(xcodeFlags, fmt.Sprintf("-%s=%s", minOSKey, minOSVal))
// Target-specific SDK directory. Must be passed as two
// words ("-isysroot PATH", not "-isysroot=PATH").
xcodeFlags = append(xcodeFlags, "-isysroot", env.Get("SDKROOT", ""))
// What does clang call the target GOARCH?
var clangArch string
switch targetArch {
case "amd64":
clangArch = "x86_64"
case "arm64":
clangArch = "arm64"
default:
return nil, nil, fmt.Errorf("unsupported GOARCH=%q when building from Xcode", targetArch)
}
xcodeFlags = append(xcodeFlags, "-arch", clangArch)
cgoCflags = append(cgoCflags, xcodeFlags...)
cgoLdflags = append(cgoLdflags, xcodeFlags...)
ldflags = append(ldflags, "-w")
}
}
// Finished computing the settings we want. Generate the modified
// commandline and environment modifications.
newArgv = append(newArgv, argv[:2]...) // Program name and `go` tool subcommand
newArgv = append(newArgv, buildFlags...)
if len(tags) > 0 {
newArgv = append(newArgv, fmt.Sprintf("-tags=%s", strings.Join(tags, ",")))
}
if len(ldflags) > 0 {
newArgv = append(newArgv, "-ldflags", strings.Join(ldflags, " "))
}
newArgv = append(newArgv, argv[2:]...)
env.Set("GOOS", targetOS)
env.Set("GOARCH", targetArch)
env.Set("GOARM", "5") // TODO: fix, see go/internal-bug/3092
env.Set("GOMIPS", "softfloat")
env.Set("CGO_ENABLED", boolStr(cgo))
env.Set("CGO_CFLAGS", strings.Join(cgoCflags, " "))
env.Set("CGO_LDFLAGS", strings.Join(cgoLdflags, " "))
env.Set("CC", "cc")
env.Set("TS_LINK_FAIL_REFLECT", boolStr(failReflect))
env.Set("GOROOT", goroot)
if subcommand == "env" {
return argv, env, nil
}
return newArgv, env, nil
}
// boolStr formats v as a string 0 or 1.
// Used because CGO_ENABLED doesn't strconv.ParseBool, so
// strconv.FormatBool breaks.
func boolStr(v bool) string {
if v {
return "1"
}
return "0"
}
// formatArgv formats a []string similarly to %v, but quotes each
// string so that the reader can clearly see each array element.
func formatArgv(v []string) string {
var ret strings.Builder
ret.WriteByte('[')
for _, s := range v {
fmt.Fprintf(&ret, "%q ", s)
}
ret.WriteByte(']')
return ret.String()
}

View File

@@ -0,0 +1,409 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package main
import (
"reflect"
"testing"
"tailscale.com/version/mkversion"
)
var fakeVersion = mkversion.VersionInfo{
Short: "1.2.3",
Long: "1.2.3-long",
GitHash: "abcd",
OtherHash: "defg",
Xcode: "100.2.3",
Winres: "1,2,3,0",
}
func TestAutoflags(t *testing.T) {
tests := []struct {
// name convention: "<hostos>_<hostarch>_to_<targetos>_<targetarch>_<anything else?>"
name string
env map[string]string
argv []string
goroot string
nativeGOOS string
nativeGOARCH string
wantEnv map[string]string
envDiff string
wantArgv []string
}{
{
name: "linux_amd64_to_linux_amd64",
argv: []string{"gocross", "build", "./cmd/tailcontrol"},
goroot: "/goroot",
nativeGOOS: "linux",
nativeGOARCH: "amd64",
envDiff: `CC=cc (was <nil>)
CGO_CFLAGS=-O3 -std=gnu11 (was <nil>)
CGO_ENABLED=1 (was <nil>)
CGO_LDFLAGS= (was <nil>)
GOARCH=amd64 (was <nil>)
GOARM=5 (was <nil>)
GOMIPS=softfloat (was <nil>)
GOOS=linux (was <nil>)
GOROOT=/goroot (was <nil>)
TS_LINK_FAIL_REFLECT=0 (was <nil>)`,
wantArgv: []string{
"gocross", "build",
"-trimpath",
"-tags=tailscale_go,osusergo,netgo",
"-ldflags", "-X tailscale.com/version.longStamp=1.2.3-long -X tailscale.com/version.shortStamp=1.2.3 -X tailscale.com/version.gitCommitStamp=abcd -X tailscale.com/version.extraGitCommitStamp=defg '-extldflags=-static'",
"./cmd/tailcontrol",
},
},
{
name: "install_linux_amd64_to_linux_amd64",
argv: []string{"gocross", "install", "./cmd/tailcontrol"},
goroot: "/goroot",
nativeGOOS: "linux",
nativeGOARCH: "amd64",
envDiff: `CC=cc (was <nil>)
CGO_CFLAGS=-O3 -std=gnu11 (was <nil>)
CGO_ENABLED=1 (was <nil>)
CGO_LDFLAGS= (was <nil>)
GOARCH=amd64 (was <nil>)
GOARM=5 (was <nil>)
GOMIPS=softfloat (was <nil>)
GOOS=linux (was <nil>)
GOROOT=/goroot (was <nil>)
TS_LINK_FAIL_REFLECT=0 (was <nil>)`,
wantArgv: []string{
"gocross", "install",
"-trimpath",
"-tags=tailscale_go,osusergo,netgo",
"-ldflags", "-X tailscale.com/version.longStamp=1.2.3-long -X tailscale.com/version.shortStamp=1.2.3 -X tailscale.com/version.gitCommitStamp=abcd -X tailscale.com/version.extraGitCommitStamp=defg '-extldflags=-static'",
"./cmd/tailcontrol",
},
},
{
name: "linux_amd64_to_linux_riscv64",
env: map[string]string{
"GOARCH": "riscv64",
},
argv: []string{"gocross", "build", "./cmd/tailcontrol"},
goroot: "/goroot",
nativeGOOS: "linux",
nativeGOARCH: "amd64",
envDiff: `CC=cc (was <nil>)
CGO_CFLAGS=-O3 -std=gnu11 (was <nil>)
CGO_ENABLED=0 (was <nil>)
CGO_LDFLAGS= (was <nil>)
GOARCH=riscv64 (was riscv64)
GOARM=5 (was <nil>)
GOMIPS=softfloat (was <nil>)
GOOS=linux (was <nil>)
GOROOT=/goroot (was <nil>)
TS_LINK_FAIL_REFLECT=0 (was <nil>)`,
wantArgv: []string{
"gocross", "build",
"-trimpath",
"-tags=tailscale_go,osusergo,netgo",
"-ldflags", "-X tailscale.com/version.longStamp=1.2.3-long -X tailscale.com/version.shortStamp=1.2.3 -X tailscale.com/version.gitCommitStamp=abcd -X tailscale.com/version.extraGitCommitStamp=defg '-extldflags=-static'",
"./cmd/tailcontrol",
},
},
{
name: "linux_amd64_to_freebsd_amd64",
env: map[string]string{
"GOOS": "freebsd",
},
argv: []string{"gocross", "build", "./cmd/tailcontrol"},
goroot: "/goroot",
nativeGOOS: "linux",
nativeGOARCH: "amd64",
envDiff: `CC=cc (was <nil>)
CGO_CFLAGS=-O3 -std=gnu11 (was <nil>)
CGO_ENABLED=0 (was <nil>)
CGO_LDFLAGS= (was <nil>)
GOARCH=amd64 (was <nil>)
GOARM=5 (was <nil>)
GOMIPS=softfloat (was <nil>)
GOOS=freebsd (was freebsd)
GOROOT=/goroot (was <nil>)
TS_LINK_FAIL_REFLECT=0 (was <nil>)`,
wantArgv: []string{
"gocross", "build",
"-trimpath",
"-tags=tailscale_go",
"-ldflags", "-X tailscale.com/version.longStamp=1.2.3-long -X tailscale.com/version.shortStamp=1.2.3 -X tailscale.com/version.gitCommitStamp=abcd -X tailscale.com/version.extraGitCommitStamp=defg",
"./cmd/tailcontrol",
},
},
{
name: "linux_amd64_to_linux_amd64_race",
argv: []string{"gocross", "test", "-race", "./cmd/tailcontrol"},
goroot: "/goroot",
nativeGOOS: "linux",
nativeGOARCH: "amd64",
envDiff: `CC=cc (was <nil>)
CGO_CFLAGS=-O3 -std=gnu11 (was <nil>)
CGO_ENABLED=1 (was <nil>)
CGO_LDFLAGS= (was <nil>)
GOARCH=amd64 (was <nil>)
GOARM=5 (was <nil>)
GOMIPS=softfloat (was <nil>)
GOOS=linux (was <nil>)
GOROOT=/goroot (was <nil>)
TS_LINK_FAIL_REFLECT=0 (was <nil>)`,
wantArgv: []string{
"gocross", "test",
"-trimpath",
"-tags=tailscale_go,osusergo,netgo",
"-ldflags", "-X tailscale.com/version.longStamp=1.2.3-long -X tailscale.com/version.shortStamp=1.2.3 -X tailscale.com/version.gitCommitStamp=abcd -X tailscale.com/version.extraGitCommitStamp=defg '-extldflags=-static'",
"-race",
"./cmd/tailcontrol",
},
},
{
name: "linux_amd64_to_windows_amd64",
env: map[string]string{
"GOOS": "windows",
},
argv: []string{"gocross", "build", "./cmd/tailcontrol"},
goroot: "/goroot",
nativeGOOS: "linux",
nativeGOARCH: "amd64",
envDiff: `CC=cc (was <nil>)
CGO_CFLAGS=-O3 -std=gnu11 (was <nil>)
CGO_ENABLED=0 (was <nil>)
CGO_LDFLAGS= (was <nil>)
GOARCH=amd64 (was <nil>)
GOARM=5 (was <nil>)
GOMIPS=softfloat (was <nil>)
GOOS=windows (was windows)
GOROOT=/goroot (was <nil>)
TS_LINK_FAIL_REFLECT=0 (was <nil>)`,
wantArgv: []string{
"gocross", "build",
"-trimpath",
"-tags=tailscale_go",
"-ldflags", "-X tailscale.com/version.longStamp=1.2.3-long -X tailscale.com/version.shortStamp=1.2.3 -X tailscale.com/version.gitCommitStamp=abcd -X tailscale.com/version.extraGitCommitStamp=defg -H windows -s",
"./cmd/tailcontrol",
},
},
{
name: "darwin_arm64_to_darwin_arm64",
argv: []string{"gocross", "build", "./cmd/tailcontrol"},
goroot: "/goroot",
nativeGOOS: "darwin",
nativeGOARCH: "arm64",
envDiff: `CC=cc (was <nil>)
CGO_CFLAGS=-O3 -std=gnu11 (was <nil>)
CGO_ENABLED=1 (was <nil>)
CGO_LDFLAGS= (was <nil>)
GOARCH=arm64 (was <nil>)
GOARM=5 (was <nil>)
GOMIPS=softfloat (was <nil>)
GOOS=darwin (was <nil>)
GOROOT=/goroot (was <nil>)
TS_LINK_FAIL_REFLECT=0 (was <nil>)`,
wantArgv: []string{
"gocross", "build",
"-trimpath",
"-tags=tailscale_go,omitidna,omitpemdecrypt",
"-ldflags", "-X tailscale.com/version.longStamp=1.2.3-long -X tailscale.com/version.shortStamp=1.2.3 -X tailscale.com/version.gitCommitStamp=abcd -X tailscale.com/version.extraGitCommitStamp=defg",
"./cmd/tailcontrol",
},
},
{
name: "darwin_arm64_to_darwin_amd64",
env: map[string]string{
"GOARCH": "amd64",
},
argv: []string{"gocross", "build", "./cmd/tailcontrol"},
goroot: "/goroot",
nativeGOOS: "darwin",
nativeGOARCH: "arm64",
envDiff: `CC=cc (was <nil>)
CGO_CFLAGS=-O3 -std=gnu11 (was <nil>)
CGO_ENABLED=1 (was <nil>)
CGO_LDFLAGS= (was <nil>)
GOARCH=amd64 (was amd64)
GOARM=5 (was <nil>)
GOMIPS=softfloat (was <nil>)
GOOS=darwin (was <nil>)
GOROOT=/goroot (was <nil>)
TS_LINK_FAIL_REFLECT=0 (was <nil>)`,
wantArgv: []string{
"gocross", "build",
"-trimpath",
"-tags=tailscale_go,omitidna,omitpemdecrypt",
"-ldflags", "-X tailscale.com/version.longStamp=1.2.3-long -X tailscale.com/version.shortStamp=1.2.3 -X tailscale.com/version.gitCommitStamp=abcd -X tailscale.com/version.extraGitCommitStamp=defg",
"./cmd/tailcontrol",
},
},
{
name: "darwin_arm64_to_ios_arm64",
env: map[string]string{
"GOOS": "ios",
},
argv: []string{"gocross", "build", "./cmd/tailcontrol"},
goroot: "/goroot",
nativeGOOS: "darwin",
nativeGOARCH: "arm64",
envDiff: `CC=cc (was <nil>)
CGO_CFLAGS=-O3 -std=gnu11 (was <nil>)
CGO_ENABLED=1 (was <nil>)
CGO_LDFLAGS= (was <nil>)
GOARCH=arm64 (was <nil>)
GOARM=5 (was <nil>)
GOMIPS=softfloat (was <nil>)
GOOS=ios (was ios)
GOROOT=/goroot (was <nil>)
TS_LINK_FAIL_REFLECT=1 (was <nil>)`,
wantArgv: []string{
"gocross", "build",
"-trimpath",
"-tags=tailscale_go,omitidna,omitpemdecrypt",
"-ldflags", "-X tailscale.com/version.longStamp=1.2.3-long -X tailscale.com/version.shortStamp=1.2.3 -X tailscale.com/version.gitCommitStamp=abcd -X tailscale.com/version.extraGitCommitStamp=defg",
"./cmd/tailcontrol",
},
},
{
name: "darwin_arm64_to_darwin_amd64_xcode",
env: map[string]string{
"GOOS": "darwin",
"GOARCH": "amd64",
"XCODE_VERSION_ACTUAL": "1300",
"DEPLOYMENT_TARGET_CLANG_FLAG_NAME": "mmacosx-version-min",
"MACOSX_DEPLOYMENT_TARGET": "11.3",
"DEPLOYMENT_TARGET_CLANG_ENV_NAME": "MACOSX_DEPLOYMENT_TARGET",
"SDKROOT": "/my/sdk/root",
},
argv: []string{"gocross", "build", "./cmd/tailcontrol"},
goroot: "/goroot",
nativeGOOS: "darwin",
nativeGOARCH: "arm64",
envDiff: `CC=cc (was <nil>)
CGO_CFLAGS=-O3 -std=gnu11 -mmacosx-version-min=11.3 -isysroot /my/sdk/root -arch x86_64 (was <nil>)
CGO_ENABLED=1 (was <nil>)
CGO_LDFLAGS=-mmacosx-version-min=11.3 -isysroot /my/sdk/root -arch x86_64 (was <nil>)
GOARCH=amd64 (was amd64)
GOARM=5 (was <nil>)
GOMIPS=softfloat (was <nil>)
GOOS=darwin (was darwin)
GOROOT=/goroot (was <nil>)
TS_LINK_FAIL_REFLECT=0 (was <nil>)`,
wantArgv: []string{
"gocross", "build",
"-trimpath",
"-tags=tailscale_go,omitidna,omitpemdecrypt",
"-ldflags", "-X tailscale.com/version.longStamp=1.2.3-long -X tailscale.com/version.shortStamp=1.2.3 -X tailscale.com/version.gitCommitStamp=abcd -X tailscale.com/version.extraGitCommitStamp=defg -w",
"./cmd/tailcontrol",
},
},
{
name: "linux_amd64_to_linux_amd64_in_goroot",
argv: []string{"go", "build", "./cmd/tailcontrol"},
goroot: "/special/toolchain/path",
nativeGOOS: "linux",
nativeGOARCH: "amd64",
envDiff: `CC=cc (was <nil>)
CGO_CFLAGS=-O3 -std=gnu11 (was <nil>)
CGO_ENABLED=1 (was <nil>)
CGO_LDFLAGS= (was <nil>)
GOARCH=amd64 (was <nil>)
GOARM=5 (was <nil>)
GOMIPS=softfloat (was <nil>)
GOOS=linux (was <nil>)
GOROOT=/special/toolchain/path (was <nil>)
TS_LINK_FAIL_REFLECT=0 (was <nil>)`,
wantArgv: []string{
"go", "build",
"-trimpath",
"-tags=tailscale_go,osusergo,netgo",
"-ldflags", "-X tailscale.com/version.longStamp=1.2.3-long -X tailscale.com/version.shortStamp=1.2.3 -X tailscale.com/version.gitCommitStamp=abcd -X tailscale.com/version.extraGitCommitStamp=defg '-extldflags=-static'",
"./cmd/tailcontrol",
},
},
{
name: "linux_list_amd64_to_linux_amd64",
argv: []string{"gocross", "list", "./cmd/tailcontrol"},
goroot: "/goroot",
nativeGOOS: "linux",
nativeGOARCH: "amd64",
envDiff: `CC=cc (was <nil>)
CGO_CFLAGS=-O3 -std=gnu11 (was <nil>)
CGO_ENABLED=1 (was <nil>)
CGO_LDFLAGS= (was <nil>)
GOARCH=amd64 (was <nil>)
GOARM=5 (was <nil>)
GOMIPS=softfloat (was <nil>)
GOOS=linux (was <nil>)
GOROOT=/goroot (was <nil>)
TS_LINK_FAIL_REFLECT=0 (was <nil>)`,
wantArgv: []string{
"gocross", "list",
"-trimpath",
"-tags=tailscale_go,osusergo,netgo",
"-ldflags", "-X tailscale.com/version.longStamp=1.2.3-long -X tailscale.com/version.shortStamp=1.2.3 -X tailscale.com/version.gitCommitStamp=abcd -X tailscale.com/version.extraGitCommitStamp=defg '-extldflags=-static'",
"./cmd/tailcontrol",
},
},
{
name: "linux_amd64_to_linux_amd64_with_extra_glibc_path",
env: map[string]string{
"GOCROSS_GLIBC_DIR": "/my/glibc/path",
},
argv: []string{"gocross", "build", "./cmd/tailcontrol"},
goroot: "/goroot",
nativeGOOS: "linux",
nativeGOARCH: "amd64",
envDiff: `CC=cc (was <nil>)
CGO_CFLAGS=-O3 -std=gnu11 (was <nil>)
CGO_ENABLED=1 (was <nil>)
CGO_LDFLAGS= (was <nil>)
GOARCH=amd64 (was <nil>)
GOARM=5 (was <nil>)
GOMIPS=softfloat (was <nil>)
GOOS=linux (was <nil>)
GOROOT=/goroot (was <nil>)
TS_LINK_FAIL_REFLECT=0 (was <nil>)`,
wantArgv: []string{
"gocross", "build",
"-trimpath",
"-tags=tailscale_go,osusergo,netgo",
"-ldflags", "-X tailscale.com/version.longStamp=1.2.3-long -X tailscale.com/version.shortStamp=1.2.3 -X tailscale.com/version.gitCommitStamp=abcd -X tailscale.com/version.extraGitCommitStamp=defg '-extldflags=-static -L /my/glibc/path'",
"./cmd/tailcontrol",
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
getver := func() mkversion.VersionInfo { return fakeVersion }
env := newEnvironmentForTest(test.env, nil, nil)
gotArgv, env, err := autoflagsForTest(test.argv, env, test.goroot, test.nativeGOOS, test.nativeGOARCH, getver)
if err != nil {
t.Fatalf("newAutoflagsForTest failed: %v", err)
}
if diff := env.Diff(); diff != test.envDiff {
t.Errorf("wrong environment diff, got:\n%s\n\nwant:\n%s", diff, test.envDiff)
}
if !reflect.DeepEqual(gotArgv, test.wantArgv) {
t.Errorf("wrong argv:\n got : %s\n want: %s", formatArgv(gotArgv), formatArgv(test.wantArgv))
}
})
}
}

131
tool/gocross/env.go Normal file
View File

@@ -0,0 +1,131 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package main
import (
"fmt"
"os"
"sort"
"strings"
)
// Environment starts from an initial set of environment variables, and tracks
// mutations to the environment. It can then apply those mutations to the
// environment, or produce debugging output that illustrates the changes it
// would make.
type Environment struct {
init map[string]string
set map[string]string
unset map[string]bool
setenv func(string, string) error
unsetenv func(string) error
}
// NewEnvironment returns an Environment initialized from os.Environ.
func NewEnvironment() *Environment {
init := map[string]string{}
for _, env := range os.Environ() {
fs := strings.SplitN(env, "=", 2)
if len(fs) != 2 {
panic("bad environ provided")
}
init[fs[0]] = fs[1]
}
return newEnvironmentForTest(init, os.Setenv, os.Unsetenv)
}
func newEnvironmentForTest(init map[string]string, setenv func(string, string) error, unsetenv func(string) error) *Environment {
return &Environment{
init: init,
set: map[string]string{},
unset: map[string]bool{},
setenv: setenv,
unsetenv: unsetenv,
}
}
// Set sets the environment variable k to v.
func (e *Environment) Set(k, v string) {
e.set[k] = v
delete(e.unset, k)
}
// Unset removes the environment variable k.
func (e *Environment) Unset(k string) {
delete(e.set, k)
e.unset[k] = true
}
// IsSet reports whether the environment variable k is set.
func (e *Environment) IsSet(k string) bool {
if e.unset[k] {
return false
}
if _, ok := e.init[k]; ok {
return true
}
if _, ok := e.set[k]; ok {
return true
}
return false
}
// Get returns the value of the environment variable k, or defaultVal if it is
// not set.
func (e *Environment) Get(k, defaultVal string) string {
if e.unset[k] {
return defaultVal
}
if v, ok := e.set[k]; ok {
return v
}
if v, ok := e.init[k]; ok {
return v
}
return defaultVal
}
// Apply applies all pending mutations to the environment.
func (e *Environment) Apply() error {
for k, v := range e.set {
if err := e.setenv(k, v); err != nil {
return fmt.Errorf("setting %q: %v", k, err)
}
e.init[k] = v
delete(e.set, k)
}
for k := range e.unset {
if err := e.unsetenv(k); err != nil {
return fmt.Errorf("unsetting %q: %v", k, err)
}
delete(e.init, k)
delete(e.unset, k)
}
return nil
}
// Diff returns a string describing the pending mutations to the environment.
func (e *Environment) Diff() string {
lines := make([]string, 0, len(e.set)+len(e.unset))
for k, v := range e.set {
old, ok := e.init[k]
if ok {
lines = append(lines, fmt.Sprintf("%s=%s (was %s)", k, v, old))
} else {
lines = append(lines, fmt.Sprintf("%s=%s (was <nil>)", k, v))
}
}
for k := range e.unset {
old, ok := e.init[k]
if ok {
lines = append(lines, fmt.Sprintf("%s=<nil> (was %s)", k, old))
} else {
lines = append(lines, fmt.Sprintf("%s=<nil> (was <nil>)", k))
}
}
sort.Strings(lines)
return strings.Join(lines, "\n")
}

99
tool/gocross/env_test.go Normal file
View File

@@ -0,0 +1,99 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package main
import (
"testing"
"github.com/google/go-cmp/cmp"
)
func TestEnv(t *testing.T) {
var (
init = map[string]string{
"FOO": "bar",
}
wasSet = map[string]string{}
wasUnset = map[string]bool{}
setenv = func(k, v string) error {
wasSet[k] = v
return nil
}
unsetenv = func(k string) error {
wasUnset[k] = true
return nil
}
)
env := newEnvironmentForTest(init, setenv, unsetenv)
if got, want := env.Get("FOO", ""), "bar"; got != want {
t.Errorf(`env.Get("FOO") = %q, want %q`, got, want)
}
if got, want := env.IsSet("FOO"), true; got != want {
t.Errorf(`env.IsSet("FOO") = %v, want %v`, got, want)
}
if got, want := env.Get("BAR", "defaultVal"), "defaultVal"; got != want {
t.Errorf(`env.Get("BAR") = %q, want %q`, got, want)
}
if got, want := env.IsSet("BAR"), false; got != want {
t.Errorf(`env.IsSet("BAR") = %v, want %v`, got, want)
}
env.Set("BAR", "quux")
if got, want := env.Get("BAR", ""), "quux"; got != want {
t.Errorf(`env.Get("BAR") = %q, want %q`, got, want)
}
if got, want := env.IsSet("BAR"), true; got != want {
t.Errorf(`env.IsSet("BAR") = %v, want %v`, got, want)
}
diff := "BAR=quux (was <nil>)"
if got := env.Diff(); got != diff {
t.Errorf("env.Diff() = %q, want %q", got, diff)
}
env.Set("FOO", "foo2")
if got, want := env.Get("FOO", ""), "foo2"; got != want {
t.Errorf(`env.Get("FOO") = %q, want %q`, got, want)
}
if got, want := env.IsSet("FOO"), true; got != want {
t.Errorf(`env.IsSet("FOO") = %v, want %v`, got, want)
}
diff = `BAR=quux (was <nil>)
FOO=foo2 (was bar)`
if got := env.Diff(); got != diff {
t.Errorf("env.Diff() = %q, want %q", got, diff)
}
env.Unset("FOO")
if got, want := env.Get("FOO", "default"), "default"; got != want {
t.Errorf(`env.Get("FOO") = %q, want %q`, got, want)
}
if got, want := env.IsSet("FOO"), false; got != want {
t.Errorf(`env.IsSet("FOO") = %v, want %v`, got, want)
}
diff = `BAR=quux (was <nil>)
FOO=<nil> (was bar)`
if got := env.Diff(); got != diff {
t.Errorf("env.Diff() = %q, want %q", got, diff)
}
if err := env.Apply(); err != nil {
t.Fatalf("env.Apply() failed: %v", err)
}
wantSet := map[string]string{"BAR": "quux"}
wantUnset := map[string]bool{"FOO": true}
if diff := cmp.Diff(wasSet, wantSet); diff != "" {
t.Errorf("env.Apply didn't set as expected (-got+want):\n%s", diff)
}
if diff := cmp.Diff(wasUnset, wantUnset); diff != "" {
t.Errorf("env.Apply didn't unset as expected (-got+want):\n%s", diff)
}
}

View File

@@ -0,0 +1,20 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build !unix
package main
import (
"os"
"os/exec"
)
func doExec(cmd string, args []string, env []string) error {
c := exec.Command(cmd, args...)
c.Env = env
c.Stdin = os.Stdin
c.Stdout = os.Stdout
c.Stderr = os.Stderr
return c.Run()
}

12
tool/gocross/exec_unix.go Normal file
View File

@@ -0,0 +1,12 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build unix
package main
import "golang.org/x/sys/unix"
func doExec(cmd string, args []string, env []string) error {
return unix.Exec(cmd, args, env)
}

85
tool/gocross/gocross-wrapper.sh Executable file
View File

@@ -0,0 +1,85 @@
#!/usr/bin/env sh
# Copyright (c) Tailscale Inc & AUTHORS
# SPDX-License-Identifier: BSD-3-Clause
#
# gocross-wrapper.sh is a wrapper that can be aliased to 'go', which
# transparently builds gocross using a "bootstrap" Go toolchain, and
# then invokes gocross.
set -eu
if [ "${CI:-}" = "true" ]; then
set -x
fi
# Locate a bootstrap toolchain and (re)build gocross if necessary. We run all of
# this in a subshell because posix shell semantics make it very easy to
# accidentally mutate the input environment that will get passed to gocross at
# the bottom of this script.
(
repo_root="$(dirname $0)/../.."
toolchain="$HOME/.cache/tailscale-go"
if [ ! -d "$toolchain" ]; then
mkdir -p "$HOME/.cache"
# We need any Go toolchain to build gocross, but the toolchain also has to
# be reasonably recent because we upgrade eagerly and gocross might not
# build with Go N-1. So, if we have no cached tailscale toolchain at all,
# fetch the initial one in shell. Once gocross is built, it'll manage
# updates.
read -r REV <$repo_root/go.toolchain.rev
case "$REV" in
/*)
toolchain="$REV"
;;
*)
# This works for linux and darwin, which is sufficient
# (we do not build tailscale-go for other targets).
HOST_OS=$(uname -s | tr A-Z a-z)
HOST_ARCH="$(uname -m)"
if [ "$HOST_ARCH" = "aarch64" ]; then
# Go uses the name "arm64".
HOST_ARCH="arm64"
elif [ "$HOST_ARCH" = "x86_64" ]; then
# Go uses the name "amd64".
HOST_ARCH="amd64"
fi
rm -rf "$toolchain" "$toolchain.extracted"
curl -f -L -o "$toolchain.tar.gz" "https://github.com/tailscale/go/releases/download/build-${REV}/${HOST_OS}-${HOST_ARCH}.tar.gz"
mkdir -p "$toolchain"
(cd "$toolchain" && tar --strip-components=1 -xf "$toolchain.tar.gz")
echo "$REV" >"$toolchain.extracted"
;;
esac
fi
# Binaries run with `gocross run` can reinvoke gocross, resulting in a
# potentially fancy build that invokes external linkers, might be
# cross-building for other targets, and so forth. In one hilarious
# case, cmd/cloner invokes go with GO111MODULE=off at some stage.
#
# Anyway, build gocross in a stripped down universe.
gocross_path="$repo_root/gocross"
gocross_ok=0
if [ -x "$gocross_path" ]; then
gotver="$($gocross_path gocross-version 2>/dev/null || echo '')"
wantver="$(git rev-parse HEAD)"
if [ "$gotver" = "$wantver" ]; then
gocross_ok=1
fi
fi
if [ "$gocross_ok" = "0" ]; then
unset GOOS
unset GOARCH
unset GO111MODULE
unset GOROOT
export CGO_ENABLED=0
"$toolchain/bin/go" build -o "$gocross_path" -ldflags='-X tailscale.com/version/gitCommitStamp=$wantver' tailscale.com/tool/gocross
fi
) # End of the subshell execution.
exec "$(dirname $0)/../../gocross" "$@"

129
tool/gocross/gocross.go Normal file
View File

@@ -0,0 +1,129 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// gocross is a wrapper around the `go` tool that invokes `go` from Tailscale's
// custom toolchain, with the right build parameters injected based on the
// native+target GOOS/GOARCH.
//
// In short, when aliased to `go`, using `go build`, `go test` behave like the
// upstream Go tools, but produce correctly configured, correctly linked
// binaries stamped with version information.
package main
import (
_ "embed"
"fmt"
"os"
"path/filepath"
"tailscale.com/atomicfile"
"tailscale.com/version"
)
func main() {
if len(os.Args) > 1 {
// These additional subcommands are various support commands to handle
// integration with Tailscale's existing build system. Unless otherwise
// specified, these are not stable APIs, and may change or go away at
// any time.
switch os.Args[1] {
case "gocross-version":
fmt.Println(version.GetMeta().GitCommit)
os.Exit(0)
case "is-gocross":
// This subcommand exits with an error code when called on a
// regular go binary, so it can be used to detect when `go` is
// actually gocross.
os.Exit(0)
case "make-goroot":
_, gorootDir, err := getToolchain()
if err != nil {
fmt.Fprintf(os.Stderr, "getting toolchain: %v\n", err)
os.Exit(1)
}
fmt.Println(gorootDir)
os.Exit(0)
case "gocross-get-toolchain-go":
toolchain, _, err := getToolchain()
if err != nil {
fmt.Fprintf(os.Stderr, "getting toolchain: %v\n", err)
os.Exit(1)
}
fmt.Println(filepath.Join(toolchain, "bin/go"))
os.Exit(0)
case "gocross-write-wrapper-script":
if len(os.Args) != 3 {
fmt.Fprintf(os.Stderr, "usage: gocross write-wrapper-script <path>\n")
os.Exit(1)
}
if err := atomicfile.WriteFile(os.Args[2], wrapperScript, 0755); err != nil {
fmt.Fprintf(os.Stderr, "writing wrapper script: %v\n", err)
os.Exit(1)
}
os.Exit(0)
}
}
toolchain, goroot, err := getToolchain()
if err != nil {
fmt.Fprintf(os.Stderr, "getting toolchain: %v\n", err)
os.Exit(1)
}
args := os.Args
if os.Getenv("GOCROSS_BYPASS") == "" {
newArgv, env, err := Autoflags(os.Args, goroot)
if err != nil {
fmt.Fprintf(os.Stderr, "computing flags: %v\n", err)
os.Exit(1)
}
// Make sure the right version of cmd/go is the first thing in the PATH
// for tests that execute `go build` or `go test`.
// TODO: if we really need to do this, do it inside Autoflags, not here.
path := filepath.Join(toolchain, "bin") + string(os.PathListSeparator) + os.Getenv("PATH")
env.Set("PATH", path)
debug("Input: %s\n", formatArgv(os.Args))
debug("Command: %s\n", formatArgv(newArgv))
debug("Set the following flags/envvars:\n%s\n", env.Diff())
args = newArgv
if err := env.Apply(); err != nil {
fmt.Fprintf(os.Stderr, "modifying environment: %v\n", err)
os.Exit(1)
}
}
doExec(filepath.Join(toolchain, "bin/go"), args, os.Environ())
}
//go:embed gocross-wrapper.sh
var wrapperScript []byte
func debug(format string, args ...interface{}) {
debug := os.Getenv("GOCROSS_DEBUG")
var (
out *os.File
err error
)
switch debug {
case "0", "":
return
case "1":
out = os.Stderr
default:
out, err = os.OpenFile(debug, os.O_APPEND|os.O_CREATE|os.O_RDWR, 0640)
if err != nil {
fmt.Fprintf(os.Stderr, "opening debug file %q: %v", debug, err)
out = os.Stderr
} else {
defer out.Close() // May lose some write errors, but we don't care.
}
}
fmt.Fprintf(out, format, args...)
}

90
tool/gocross/goroot.go Normal file
View File

@@ -0,0 +1,90 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package main
import (
"errors"
"fmt"
"io"
"io/fs"
"os"
"path/filepath"
)
// makeGoroot constructs a GOROOT-like file structure in outPath,
// which consists of toolchainRoot except for the `go` binary, which
// points to gocross.
//
// It's useful for integrating with tooling that expects to be handed
// a GOROOT, like the Goland IDE or depaware.
func makeGoroot(toolchainRoot, outPath string) error {
self, err := os.Executable()
if err != nil {
return fmt.Errorf("getting gocross's path: %v", err)
}
os.RemoveAll(outPath)
if err := os.MkdirAll(filepath.Join(outPath, "bin"), 0750); err != nil {
return fmt.Errorf("making %q: %v", outPath, err)
}
if err := os.Symlink(self, filepath.Join(outPath, "bin/go")); err != nil {
return fmt.Errorf("linking gocross into outpath: %v", err)
}
if err := linkFarm(toolchainRoot, outPath); err != nil {
return fmt.Errorf("creating GOROOT link farm: %v", err)
}
if err := linkFarm(filepath.Join(toolchainRoot, "bin"), filepath.Join(outPath, "bin")); err != nil {
return fmt.Errorf("creating GOROOT/bin link farm: %v", err)
}
return nil
}
func copyFile(src, dst string) error {
s, err := os.Open(src)
if err != nil {
return fmt.Errorf("opening %q: %v", src, err)
}
defer s.Close()
d, err := os.OpenFile(dst, os.O_RDWR|os.O_CREATE, 0755)
if err != nil {
return fmt.Errorf("opening %q: %v", dst, err)
}
if _, err := io.Copy(d, s); err != nil {
d.Close()
return fmt.Errorf("copying %q to %q: %v", src, dst, err)
}
if err := d.Close(); err != nil {
return fmt.Errorf("closing %q: %v", dst, err)
}
return nil
}
// linkFarm symlinks every entry in srcDir into outDir, unless that
// directory entry already exists.
func linkFarm(srcDir, outDir string) error {
ents, err := os.ReadDir(srcDir)
if err != nil {
return fmt.Errorf("reading %q: %v", srcDir, err)
}
for _, ent := range ents {
dst := filepath.Join(outDir, ent.Name())
_, err := os.Lstat(dst)
if errors.Is(err, fs.ErrNotExist) {
if err := os.Symlink(filepath.Join(srcDir, ent.Name()), dst); err != nil {
return fmt.Errorf("symlinking %q to %q: %v", ent.Name(), outDir, err)
}
} else if err != nil {
return fmt.Errorf("stat-ing %q: %v", dst, err)
}
}
return nil
}

182
tool/gocross/toolchain.go Normal file
View File

@@ -0,0 +1,182 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package main
import (
"bytes"
"fmt"
"io"
"net/http"
"os"
"os/exec"
"path/filepath"
"runtime"
)
func toolchainRev() (string, error) {
cwd, err := os.Getwd()
if err != nil {
return "", fmt.Errorf("getting CWD: %v", err)
}
d := cwd
findTopLevel:
for {
if _, err := os.Lstat(filepath.Join(d, ".git")); err == nil {
break findTopLevel
} else if !os.IsNotExist(err) {
return "", fmt.Errorf("finding .git: %v", err)
}
d = filepath.Dir(d)
if d == "/" {
return "", fmt.Errorf("couldn't find .git starting from %q, cannot manage toolchain", cwd)
}
}
return readRevFile(filepath.Join(d, "go.toolchain.rev"))
}
func readRevFile(path string) (string, error) {
bs, err := os.ReadFile(path)
if err != nil {
if os.IsNotExist(err) {
return "", nil
}
return "", err
}
return string(bytes.TrimSpace(bs)), nil
}
func getToolchain() (toolchainDir, gorootDir string, err error) {
cache := filepath.Join(os.Getenv("HOME"), ".cache")
toolchainDir = filepath.Join(cache, "tailscale-go")
gorootDir = filepath.Join(toolchainDir, "gocross-goroot")
// You might wonder why getting the toolchain also provisions and returns a
// path suitable for use as GOROOT. Wonder no longer!
//
// A bunch of our tests and build processes involve re-invoking 'go build'
// or other build-ish commands (install, run, ...). These typically use
// runtime.GOROOT + "bin/go" to get at the Go binary. Even more edge case-y,
// tailscale.com/cmd/tsconnect needs to fish a javascript glue file out of
// GOROOT in order to build the javascript bundle for serving.
//
// Gocross always does a -trimpath on builds for reproducibility, which
// wipes out the burned-in runtime.GOROOT value from the binary. This means
// that using gocross on these various test and build processes ends up
// breaking with mysterious path errors.
//
// We don't want to stop using -trimpath, or otherwise make GOROOT work in
// "normal" builds, because that is a footgun that lets people accidentally
// create assumptions that the build toolchain is still around at runtime.
// Instead, we want to make 'go test' and 'go run' have access to GOROOT,
// while still removing it from standalone binaries.
//
// So, construct and pass a GOROOT to the actual 'go' invocation, which lets
// tests and build processes locate and use GOROOT. For consistency, the
// GOROOT that's passed in is a symlink farm that mostly points to the
// toolchain's underlying GOROOT, but 'bin/go' points back to gocross. This
// means that if you invoke 'go test' via gocross, and that test tries to
// build code, that build will also end up using gocross.
if err := ensureToolchain(cache, toolchainDir); err != nil {
return "", "", err
}
if err := ensureGoroot(toolchainDir, gorootDir); err != nil {
return "", "", err
}
return toolchainDir, gorootDir, nil
}
func ensureToolchain(cacheDir, toolchainDir string) error {
stampFile := toolchainDir + ".extracted"
wantRev, err := toolchainRev()
if err != nil {
return err
}
gotRev, err := readRevFile(stampFile)
if err != nil {
return fmt.Errorf("reading stamp file %q: %v", stampFile, err)
}
if gotRev == wantRev {
// Toolchain already good.
return nil
}
if err := os.RemoveAll(toolchainDir); err != nil {
return err
}
if err := os.RemoveAll(stampFile); err != nil {
return err
}
if filepath.IsAbs(wantRev) {
// Local dev toolchain.
if err := os.Symlink(wantRev, toolchainDir); err != nil {
return err
}
return nil
} else {
if err := downloadCachedgo(toolchainDir, wantRev); err != nil {
return err
}
}
if err := os.WriteFile(stampFile, []byte(wantRev), 0644); err != nil {
return err
}
return nil
}
func ensureGoroot(toolchainDir, gorootDir string) error {
if _, err := os.Stat(gorootDir); err == nil {
return nil
} else if !os.IsNotExist(err) {
return err
}
return makeGoroot(toolchainDir, gorootDir)
}
func downloadCachedgo(toolchainDir, toolchainRev string) error {
url := fmt.Sprintf("https://github.com/tailscale/go/releases/download/build-%s/%s-%s.tar.gz", toolchainRev, runtime.GOOS, runtime.GOARCH)
archivePath := toolchainDir + ".tar.gz"
f, err := os.Create(archivePath)
if err != nil {
return err
}
resp, err := http.Get(url)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != 200 {
return fmt.Errorf("failed to get %q: %v", url, resp.Status)
}
if _, err := io.Copy(f, resp.Body); err != nil {
return err
}
if err := f.Close(); err != nil {
return err
}
if err := os.MkdirAll(toolchainDir, 0755); err != nil {
return err
}
cmd := exec.Command("tar", "--strip-components=1", "-xf", archivePath)
cmd.Dir = toolchainDir
if err := cmd.Run(); err != nil {
return err
}
if err := os.RemoveAll(archivePath); err != nil {
return err
}
return nil
}

View File

@@ -28,6 +28,7 @@ import (
"tailscale.com/hostinfo"
"tailscale.com/ipn"
"tailscale.com/ipn/ipnlocal"
"tailscale.com/ipn/ipnstate"
"tailscale.com/ipn/localapi"
"tailscale.com/ipn/store"
"tailscale.com/ipn/store/mem"
@@ -146,6 +147,52 @@ func (s *Server) Start() error {
return s.initErr
}
// Up connects the server to the tailnet and waits until it is running.
// On success it returns the current status, including a Tailscale IP address.
func (s *Server) Up(ctx context.Context) (*ipnstate.Status, error) {
lc, err := s.LocalClient() // calls Start
if err != nil {
return nil, fmt.Errorf("tsnet.Up: %w", err)
}
watcher, err := lc.WatchIPNBus(ctx, ipn.NotifyInitialState|ipn.NotifyNoPrivateKeys)
if err != nil {
return nil, fmt.Errorf("tsnet.Up: %w", err)
}
defer watcher.Close()
for {
n, err := watcher.Next()
if err != nil {
return nil, fmt.Errorf("tsnet.Up: %w", err)
}
if n.ErrMessage != nil {
return nil, fmt.Errorf("tsnet.Up: backend: %s", *n.ErrMessage)
}
if s := n.State; s != nil {
switch *s {
case ipn.Running:
status, err := lc.Status(ctx)
if err != nil {
return nil, fmt.Errorf("tsnet.Up: %w", err)
}
if len(status.TailscaleIPs) == 0 {
return nil, errors.New("tsnet.Up: running, but no ip")
}
return status, nil
case ipn.NeedsMachineAuth:
return nil, errors.New("tsnet.Up: tailnet requested machine auth")
}
// TODO: in the future, return an error on NeedsLogin
// to improve the UX of trying out the tsnet package.
//
// Unfortunately today, even when using an AuthKey we
// briefly see a NeedsLogin state. It would be nice
// to fix that.
}
}
}
// Close stops the server.
//
// It must not be called before or concurrently with Start.
@@ -157,11 +204,15 @@ func (s *Server) Close() error {
go func() {
defer wg.Done()
// Perform a best-effort final flush.
s.logtail.Shutdown(ctx)
s.logbuffer.Close()
if s.logtail != nil {
s.logtail.Shutdown(ctx)
}
if s.logbuffer != nil {
s.logbuffer.Close()
}
}()
if _, isMemStore := s.Store.(*mem.Store); isMemStore && s.Ephemeral {
if _, isMemStore := s.Store.(*mem.Store); isMemStore && s.Ephemeral && s.lb != nil {
wg.Add(1)
go func() {
defer wg.Done()
@@ -174,11 +225,21 @@ func (s *Server) Close() error {
s.netstack.Close()
s.netstack = nil
}
s.shutdownCancel()
s.lb.Shutdown()
s.linkMon.Close()
s.dialer.Close()
s.localAPIListener.Close()
if s.shutdownCancel != nil {
s.shutdownCancel()
}
if s.lb != nil {
s.lb.Shutdown()
}
if s.linkMon != nil {
s.linkMon.Close()
}
if s.dialer != nil {
s.dialer.Close()
}
if s.localAPIListener != nil {
s.localAPIListener.Close()
}
s.mu.Lock()
defer s.mu.Unlock()

View File

@@ -4,8 +4,23 @@
package tsnet
import (
"context"
"errors"
"flag"
"fmt"
"io"
"path/filepath"
"os"
"net/http/httptest"
"testing"
"time"
"tailscale.com/ipn/store/mem"
"tailscale.com/tailcfg"
"tailscale.com/tstest/integration"
"tailscale.com/net/netns"
"tailscale.com/tstest/integration/testcontrol"
"tailscale.com/types/logger"
)
// TestListener_Server ensures that the listener type always keeps the Server
@@ -44,3 +59,111 @@ func TestListenerPort(t *testing.T) {
}
}
}
var verboseDERP = flag.Bool("verbose-derp", false, "if set, print DERP and STUN logs")
var verboseNodes = flag.Bool("verbose-nodes", false, "if set, print tsnet.Server logs")
func TestConn(t *testing.T) {
// Corp#4520: don't use netns for tests.
netns.SetEnabled(false)
t.Cleanup(func() {
netns.SetEnabled(true)
})
derpLogf := logger.Discard
if *verboseDERP {
derpLogf = t.Logf
}
derpMap := integration.RunDERPAndSTUN(t, derpLogf, "127.0.0.1")
control := &testcontrol.Server{
DERPMap: derpMap,
}
control.HTTPTestServer = httptest.NewUnstartedServer(control)
control.HTTPTestServer.Start()
t.Cleanup(control.HTTPTestServer.Close)
controlURL := control.HTTPTestServer.URL
t.Logf("testcontrol listening on %s", controlURL)
tmp := t.TempDir()
tmps1 := filepath.Join(tmp, "s1")
os.MkdirAll(tmps1, 0755)
s1 := &Server{
Dir: tmps1,
ControlURL: controlURL,
Hostname: "s1",
Store: new(mem.Store),
Ephemeral: true,
}
defer s1.Close()
tmps2 := filepath.Join(tmp, "s1")
os.MkdirAll(tmps2, 0755)
s2 := &Server{
Dir: tmps2,
ControlURL: controlURL,
Hostname: "s2",
Store: new(mem.Store),
Ephemeral: true,
}
defer s2.Close()
if !*verboseNodes {
s1.Logf = logger.Discard
s2.Logf = logger.Discard
}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
s1status, err := s1.Up(ctx)
if err != nil {
t.Fatal(err)
}
s1ip := s1status.TailscaleIPs[0]
if _, err := s2.Up(ctx); err != nil {
t.Fatal(err)
}
lc2, err := s2.LocalClient()
if err != nil {
t.Fatal(err)
}
// ping to make sure the connection is up.
res, err := lc2.Ping(ctx, s1ip, tailcfg.PingICMP)
if err != nil {
t.Fatal(err)
}
t.Logf("ping success: %#+v", res)
// pass some data through TCP.
ln, err := s1.Listen("tcp", ":8081")
if err != nil {
t.Fatal(err)
}
defer ln.Close()
w, err := s2.Dial(ctx, "tcp", fmt.Sprintf("%s:8081", s1ip))
if err != nil {
t.Fatal(err)
}
r, err := ln.Accept()
if err != nil {
t.Fatal(err)
}
want := "hello"
if _, err := io.WriteString(w, want); err != nil {
t.Fatal(err)
}
got := make([]byte, len(want))
if _, err := io.ReadAtLeast(r, got, len(got)); err != nil {
t.Fatal(err)
}
t.Logf("got: %q", got)
if string(got) != want {
t.Errorf("got %q, want %q", got, want)
}
}

View File

@@ -0,0 +1,72 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// Package ringbuffer contains a fixed-size concurrency-safe generic ring
// buffer.
package ringbuffer
import "sync"
// New creates a new RingBuffer containing at most max items.
func New[T any](max int) *RingBuffer[T] {
return &RingBuffer[T]{
max: max,
}
}
// RingBuffer is a concurrency-safe ring buffer.
type RingBuffer[T any] struct {
mu sync.Mutex
pos int
buf []T
max int
}
// Add appends a new item to the RingBuffer, possibly overwriting the oldest
// item in the buffer if it is already full.
func (rb *RingBuffer[T]) Add(t T) {
rb.mu.Lock()
defer rb.mu.Unlock()
if len(rb.buf) < rb.max {
rb.buf = append(rb.buf, t)
} else {
rb.buf[rb.pos] = t
rb.pos = (rb.pos + 1) % rb.max
}
}
// GetAll returns a copy of all the entries in the ring buffer in the order they
// were added.
func (rb *RingBuffer[T]) GetAll() []T {
if rb == nil {
return nil
}
rb.mu.Lock()
defer rb.mu.Unlock()
out := make([]T, len(rb.buf))
for i := 0; i < len(rb.buf); i++ {
x := (rb.pos + i) % rb.max
out[i] = rb.buf[x]
}
return out
}
// Len returns the number of elements in the ring buffer. Note that this value
// could change immediately after being returned if a concurrent caller
// modifies the buffer.
func (rb *RingBuffer[T]) Len() int {
if rb == nil {
return 0
}
rb.mu.Lock()
defer rb.mu.Unlock()
return len(rb.buf)
}
// Clear will empty the ring buffer.
func (rb *RingBuffer[T]) Clear() {
rb.mu.Lock()
defer rb.mu.Unlock()
rb.pos = 0
rb.buf = nil
}

View File

@@ -0,0 +1,55 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package ringbuffer
import (
"reflect"
"testing"
)
func TestRingBuffer(t *testing.T) {
const numItems = 10
rb := New[int](numItems)
for i := 0; i < numItems-1; i++ {
rb.Add(i)
}
t.Run("NotFull", func(t *testing.T) {
if ll := rb.Len(); ll != numItems-1 {
t.Fatalf("got len %d; want %d", ll, numItems-1)
}
all := rb.GetAll()
want := []int{0, 1, 2, 3, 4, 5, 6, 7, 8}
if !reflect.DeepEqual(all, want) {
t.Fatalf("items mismatch\ngot: %v\nwant %v", all, want)
}
})
t.Run("Full", func(t *testing.T) {
// Append items to evict something
rb.Add(98)
rb.Add(99)
if ll := rb.Len(); ll != numItems {
t.Fatalf("got len %d; want %d", ll, numItems)
}
all := rb.GetAll()
want := []int{1, 2, 3, 4, 5, 6, 7, 8, 98, 99}
if !reflect.DeepEqual(all, want) {
t.Fatalf("items mismatch\ngot: %v\nwant %v", all, want)
}
})
t.Run("Clear", func(t *testing.T) {
rb.Clear()
if ll := rb.Len(); ll != 0 {
t.Fatalf("got len %d; want 0", ll)
}
all := rb.GetAll()
if len(all) != 0 {
t.Fatalf("got non-empty list; want empty")
}
})
}

View File

@@ -0,0 +1,484 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// Package mkversion gets version info from git and provides a bunch of
// differently formatted version strings that get used elsewhere in the build
// system to embed version numbers into binaries.
package mkversion
import (
"bytes"
"fmt"
"log"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/google/uuid"
"golang.org/x/mod/modfile"
)
// VersionInfo is version information extracted from a git checkout.
type VersionInfo struct {
// Major is the major version number portion of Short.
Major int
// Minor is the minor version number portion of Short.
Minor int
// Patch is the patch version number portion of Short.
Patch int
// Short is the short version string. See the documentation of version.Short
// for possible values.
Short string
// Long is the long version string. See the documentation for version.Long
// for possible values.
Long string
// GitHash is the git hash of the tailscale.com Go module.
GitHash string
// OtherHash is the git hash of a supplemental git repository, if any. For
// example, the commit of the tailscale-android repository.
OtherHash string
// Xcode is the version string that gets embedded into Xcode builds for the
// Tailscale iOS app and macOS standalone (aka "macsys") app.
//
// It is the same as Short, but with 100 added to the major version number.
// This is because Apple requires monotonically increasing version numbers,
// and very early builds of Tailscale used a single incrementing integer,
// which the Apple interprets as the major version number. When we switched
// to the current scheme, we started the major version number at 100 (v0,
// plus 100) to make the transition.
Xcode string
// XcodeMacOS is the version string that gets embedded into Xcode builds for
// the Tailscale macOS app store app.
//
// This used to be the same as Xcode, but at some point Xcode reverted to
// auto-incrementing build numbers instead of using the version we embedded.
// As a result, we had to alter the version scheme again, and switched to
// GitHash's commit date, in the format "YYYY.DDD.HHMMSS"
XcodeMacOS string
// Winres is the version string that gets embedded into Windows exe
// metadata. It is of the form "x,y,z,0".
Winres string
// GitDate is the unix timestamp of GitHash's commit date.
GitDate string
// OtherDate is the unix timestamp of OtherHash's commit date, if any.
OtherDate string
// Track is the release track of this build: "stable" if the minor version
// number is even, "unstable" if it's odd.
Track string
// MSIProductCodes is a map of Windows CPU architecture names to UUIDv5
// hashes that uniquely identify the version of the build. These are used in
// the MSI installer logic to uniquely identify particular builds.
MSIProductCodes map[string]string
}
// String returns v's information as shell variable assignments.
func (v VersionInfo) String() string {
f := fmt.Fprintf
var b bytes.Buffer
f(&b, "VERSION_MAJOR=%d\n", v.Major)
f(&b, "VERSION_MINOR=%d\n", v.Minor)
f(&b, "VERSION_PATCH=%d\n", v.Patch)
f(&b, "VERSION_SHORT=%q\n", v.Short)
f(&b, "VERSION_LONG=%q\n", v.Long)
f(&b, "VERSION_GIT_HASH=%q\n", v.GitHash)
f(&b, "VERSION_TRACK=%q\n", v.Track)
if v.OtherHash != "" {
f(&b, "VERSION_EXTRA_HASH=%q\n", v.OtherHash)
f(&b, "VERSION_XCODE=%q\n", v.Xcode)
f(&b, "VERSION_XCODE_MACOS=%q\n", v.XcodeMacOS)
f(&b, "VERSION_WINRES=%q\n", v.Winres)
// Ensure a predictable order for these variables for testing purposes.
for _, k := range []string{"amd64", "arm64", "x86"} {
f(&b, "VERSION_MSIPRODUCT_%s=%q\n", strings.ToUpper(k), v.MSIProductCodes[k])
}
}
return b.String()
}
// Info constructs a VersionInfo from the current working directory and returns
// it, or terminates the process via log.Fatal.
func Info() VersionInfo {
v, err := InfoFrom("")
if err != nil {
log.Fatal(err)
}
return v
}
// InfoFrom constructs a VersionInfo from dir and returns it, or an error.
func InfoFrom(dir string) (VersionInfo, error) {
runner := dirRunner(dir)
gitRoot, err := runner.output("git", "rev-parse", "--show-toplevel")
if err != nil {
return VersionInfo{}, fmt.Errorf("finding git root: %w", err)
}
runner = dirRunner(gitRoot)
modBs, err := os.ReadFile(filepath.Join(gitRoot, "go.mod"))
if err != nil {
return VersionInfo{}, fmt.Errorf("reading go.mod: %w", err)
}
modPath := modfile.ModulePath(modBs)
if modPath == "" {
return VersionInfo{}, fmt.Errorf("no module path in go.mod")
}
if modPath == "tailscale.com" {
// Invoked in the tailscale.com repo directly, just no further info to
// collect.
v, err := infoFromDir(gitRoot)
if err != nil {
return VersionInfo{}, err
}
return mkOutput(v)
}
// We seem to be in a repo that imports tailscale.com. Find the
// tailscale.com repo and collect additional info from it.
otherHash, err := runner.output("git", "rev-parse", "HEAD")
if err != nil {
return VersionInfo{}, fmt.Errorf("getting git hash: %w", err)
}
otherDate, err := runner.output("git", "log", "-n1", "--format=%ct", "HEAD")
if err != nil {
return VersionInfo{}, fmt.Errorf("getting git date: %w", err)
}
// Note, this mechanism doesn't correctly support go.mod replacements,
// or go workdirs. We only parse out the commit hash from go.mod's
// "require" line, nothing else.
tailscaleHash, err := tailscaleModuleHash(modBs)
if err != nil {
return VersionInfo{}, err
}
v, err := infoFromCache(tailscaleHash, runner)
if err != nil {
return VersionInfo{}, err
}
v.otherHash = otherHash
v.otherDate = otherDate
if !runner.ok("git", "diff-index", "--quiet", "HEAD") {
v.otherHash = v.otherHash + "-dirty"
}
return mkOutput(v)
}
// tailscaleModuleHash returns the git hash of the 'require tailscale.com' line
// in the given go.mod bytes.
func tailscaleModuleHash(modBs []byte) (string, error) {
mod, err := modfile.Parse("go.mod", modBs, nil)
if err != nil {
return "", err
}
for _, req := range mod.Require {
if req.Mod.Path != "tailscale.com" {
continue
}
// Get the last - separated part of req.Mod.Version
// (which is the git hash).
if i := strings.LastIndexByte(req.Mod.Version, '-'); i != -1 {
return req.Mod.Version[i+1:], nil
}
return "", fmt.Errorf("couldn't parse git hash from tailscale.com version %q", req.Mod.Version)
}
return "", fmt.Errorf("no require tailscale.com line in go.mod")
}
func mkOutput(v verInfo) (VersionInfo, error) {
var changeSuffix string
if v.minor%2 == 1 {
// Odd minor numbers are unstable builds.
if v.patch != 0 {
return VersionInfo{}, fmt.Errorf("unstable release %d.%d.%d has a non-zero patch number, which is not allowed", v.major, v.minor, v.patch)
}
v.patch = v.changeCount
} else if v.changeCount != 0 {
// Even minor numbers are stable builds, but stable builds are
// supposed to have a zero change count. Therefore, we're currently
// describing a commit that's on a release branch, but hasn't been
// tagged as a patch release yet.
//
// We used to change the version number to 0.0.0 in that case, but that
// caused some features to get disabled due to the low version number.
// Instead, add yet another suffix to the version number, with a change
// count.
changeSuffix = "-" + strconv.Itoa(v.changeCount)
}
var hashes string
if v.otherHash != "" {
hashes = "-g" + shortHash(v.otherHash)
}
if v.hash != "" {
hashes = "-t" + shortHash(v.hash) + hashes
}
var track string
if v.minor%2 == 1 {
track = "unstable"
} else {
track = "stable"
}
ret := VersionInfo{
Major: v.major,
Minor: v.minor,
Patch: v.patch,
Short: fmt.Sprintf("%d.%d.%d", v.major, v.minor, v.patch),
Long: fmt.Sprintf("%d.%d.%d%s%s", v.major, v.minor, v.patch, changeSuffix, hashes),
GitHash: fmt.Sprintf("%s", v.hash),
GitDate: fmt.Sprintf("%s", v.date),
Track: track,
}
if v.otherHash != "" {
ret.OtherHash = fmt.Sprintf("%s", v.otherHash)
// Technically we could populate these fields without the otherHash, but
// these version numbers only make sense when building from Tailscale's
// proprietary repo, so don't clutter open-source-only outputs with
// them.
ret.Xcode = fmt.Sprintf("%d.%d.%d", v.major+100, v.minor, v.patch)
ret.Winres = fmt.Sprintf("%d,%d,%d,0", v.major, v.minor, v.patch)
ret.MSIProductCodes = makeMSIProductCodes(v, track)
}
if v.otherDate != "" {
ret.OtherDate = fmt.Sprintf("%s", v.otherDate)
// Generate a monotonically increasing version number for the macOS app, as
// expected by Apple. We use the date so that it's always increasing (if we
// based it on the actual version number we'd run into issues when doing
// cherrypick stable builds from a release branch after unstable builds from
// HEAD).
otherSec, err := strconv.ParseInt(v.otherDate, 10, 64)
if err != nil {
return VersionInfo{}, fmt.Errorf("Could not parse otherDate %q: %w", v.otherDate, err)
}
otherTime := time.Unix(otherSec, 0).UTC()
// We started to need to do this in 2023, and the last Apple-generated
// incrementing build number was 273. To avoid using up the space, we
// use <year - 1750> as the major version (thus 273.*, 274.* in 2024, etc.),
// so that we we're still in the same range. This way if Apple goes back to
// auto-incrementing the number for us, we can go back to it with
// reasonable-looking numbers.
ret.XcodeMacOS = fmt.Sprintf("%d.%d.%d", otherTime.Year()-1750, otherTime.YearDay(), otherTime.Hour()*60*60+otherTime.Minute()*60+otherTime.Second())
}
return ret, nil
}
// makeMSIProductCodes produces per-architecture v5 UUIDs derived from the pkgs
// url that would be used for the current version, thus ensuring that product IDs
// are mapped 1:1 to a unique version number.
func makeMSIProductCodes(v verInfo, track string) map[string]string {
urlBase := fmt.Sprintf("https://pkgs.tailscale.com/%s/tailscale-setup-%d.%d.%d-", track, v.major, v.minor, v.patch)
result := map[string]string{}
for _, arch := range []string{"amd64", "arm64", "x86"} {
url := fmt.Sprintf("%s%s.msi", urlBase, arch)
curUUID := uuid.NewSHA1(uuid.NameSpaceURL, []byte(url))
// MSI prefers hex digits in UUIDs to be uppercase.
result[arch] = strings.ToUpper(curUUID.String())
}
return result
}
type verInfo struct {
major, minor, patch int
changeCount int
hash string
date string
otherHash string
otherDate string
}
// unknownPatchVersion is the patch version used when the tailscale.com package
// doesn't contain enough version information to derive the correct version.
// Such builds only get used when generating bug reports in an ephemeral working
// environment, so will never be distributed. As such, we use a highly visible
// sentinel patch number.
const unknownPatchVersion = 9999999
func infoFromCache(shortHash string, runner dirRunner) (verInfo, error) {
cacheDir, err := os.UserCacheDir()
if err != nil {
return verInfo{}, fmt.Errorf("Getting user cache dir: %w", err)
}
tailscaleCache := filepath.Join(cacheDir, "tailscale-oss")
r := dirRunner(tailscaleCache)
if _, err := os.Stat(tailscaleCache); err != nil {
if !runner.ok("git", "clone", "https://github.com/tailscale/tailscale", tailscaleCache) {
return verInfo{}, fmt.Errorf("cloning tailscale.com repo failed")
}
}
if !r.ok("git", "cat-file", "-e", shortHash) {
if !r.ok("git", "fetch", "origin") {
return verInfo{}, fmt.Errorf("updating OSS repo failed")
}
}
hash, err := r.output("git", "rev-parse", shortHash)
if err != nil {
return verInfo{}, err
}
date, err := r.output("git", "log", "-n1", "--format=%ct", shortHash)
if err != nil {
return verInfo{}, err
}
baseHash, err := r.output("git", "rev-list", "--max-count=1", hash, "--", "VERSION.txt")
if err != nil {
return verInfo{}, err
}
s, err := r.output("git", "show", baseHash+":VERSION.txt")
if err != nil {
return verInfo{}, err
}
major, minor, patch, err := parseVersion(s)
if err != nil {
return verInfo{}, err
}
s, err = r.output("git", "rev-list", "--count", hash, "^"+baseHash)
if err != nil {
return verInfo{}, err
}
changeCount, err := strconv.Atoi(s)
if err != nil {
return verInfo{}, fmt.Errorf("infoFromCache: parsing changeCount %q: %w", changeCount, err)
}
return verInfo{
major: major,
minor: minor,
patch: patch,
changeCount: changeCount,
hash: hash,
date: date,
}, nil
}
func infoFromDir(dir string) (verInfo, error) {
r := dirRunner(dir)
gitDir := filepath.Join(dir, ".git")
if _, err := os.Stat(gitDir); err != nil {
// Raw directory fetch, get as much info as we can and make up the rest.
bs, err := os.ReadFile(filepath.Join(dir, "VERSION.txt"))
if err != nil {
return verInfo{}, err
}
major, minor, patch, err := parseVersion(strings.TrimSpace(string(bs)))
return verInfo{
major: major,
minor: minor,
patch: patch,
changeCount: unknownPatchVersion,
}, err
}
hash, err := r.output("git", "rev-parse", "HEAD")
if err != nil {
return verInfo{}, err
}
date, err := r.output("git", "log", "-n1", "--format=%%ct", "HEAD")
if err != nil {
return verInfo{}, err
}
baseHash, err := r.output("git", "rev-list", "--max-count=1", hash, "--", "VERSION.txt")
if err != nil {
return verInfo{}, err
}
s, err := r.output("git", "show", baseHash+":VERSION.txt")
if err != nil {
return verInfo{}, err
}
major, minor, patch, err := parseVersion(s)
if err != nil {
return verInfo{}, err
}
s, err = r.output("git", "rev-list", "--count", hash, "^"+baseHash)
if err != nil {
return verInfo{}, err
}
changeCount, err := strconv.Atoi(s)
if err != nil {
return verInfo{}, err
}
return verInfo{
major: major,
minor: minor,
patch: patch,
changeCount: changeCount,
hash: hash,
date: date,
}, nil
}
func parseVersion(s string) (major, minor, patch int, err error) {
fs := strings.Split(strings.TrimSpace(s), ".")
if len(fs) != 3 {
err = fmt.Errorf("parseVersion: parsing %q: wrong number of parts: %d", s, len(fs))
return
}
ints := make([]int, 0, 3)
for _, s := range fs {
var i int
i, err = strconv.Atoi(s)
if err != nil {
err = fmt.Errorf("parseVersion: parsing %q: %w", s, err)
return
}
ints = append(ints, i)
}
return ints[0], ints[1], ints[2], nil
}
func shortHash(hash string) string {
if len(hash) < 9 {
return hash
}
return hash[:9]
}
// dirRunner executes commands in the specified dir.
type dirRunner string
func (r dirRunner) output(prog string, args ...string) (string, error) {
cmd := exec.Command(prog, args...)
// Sometimes, our binaries end up running in a world where
// GO111MODULE=off, because x/tools/go/packages disables Go
// modules on occasion and then runs other Go code. This breaks
// executing "go mod edit", which requires that Go modules be
// enabled.
//
// Since nothing we do here ever wants Go modules to be turned
// off, force it on here so that we can read module data
// regardless of the environment.
cmd.Env = append(os.Environ(), "GO111MODULE=on")
cmd.Dir = string(r)
out, err := cmd.Output()
if err != nil {
if ee, ok := err.(*exec.ExitError); ok {
return "", fmt.Errorf("running %v: %w, out=%s, err=%s", cmd.Args, err, out, ee.Stderr)
}
return "", fmt.Errorf("running %v: %w, %s", cmd.Args, err, out)
}
return strings.TrimSpace(string(out)), nil
}
func (r dirRunner) ok(prog string, args ...string) bool {
cmd := exec.Command(prog, args...)
cmd.Dir = string(r)
return cmd.Run() == nil
}

View File

@@ -0,0 +1,131 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package mkversion
import (
"fmt"
"strings"
"testing"
"time"
"github.com/google/go-cmp/cmp"
)
func mkInfo(gitHash, otherHash, otherDate string, major, minor, patch, changeCount int) verInfo {
return verInfo{
major: major,
minor: minor,
patch: patch,
changeCount: changeCount,
hash: gitHash,
otherHash: otherHash,
otherDate: otherDate,
}
}
func TestMkversion(t *testing.T) {
otherDate := fmt.Sprintf("%d", time.Date(2023, time.January, 27, 1, 2, 3, 4, time.UTC).Unix())
tests := []struct {
in verInfo
want string
}{
{mkInfo("abcdef", "", otherDate, 0, 98, 0, 0), `
VERSION_MAJOR=0
VERSION_MINOR=98
VERSION_PATCH=0
VERSION_SHORT="0.98.0"
VERSION_LONG="0.98.0-tabcdef"
VERSION_GIT_HASH="abcdef"
VERSION_TRACK="stable"`},
{mkInfo("abcdef", "", otherDate, 0, 98, 1, 0), `
VERSION_MAJOR=0
VERSION_MINOR=98
VERSION_PATCH=1
VERSION_SHORT="0.98.1"
VERSION_LONG="0.98.1-tabcdef"
VERSION_GIT_HASH="abcdef"
VERSION_TRACK="stable"`},
{mkInfo("abcdef", "", otherDate, 1, 2, 9, 0), `
VERSION_MAJOR=1
VERSION_MINOR=2
VERSION_PATCH=9
VERSION_SHORT="1.2.9"
VERSION_LONG="1.2.9-tabcdef"
VERSION_GIT_HASH="abcdef"
VERSION_TRACK="stable"`},
{mkInfo("abcdef", "", otherDate, 1, 15, 0, 129), `
VERSION_MAJOR=1
VERSION_MINOR=15
VERSION_PATCH=129
VERSION_SHORT="1.15.129"
VERSION_LONG="1.15.129-tabcdef"
VERSION_GIT_HASH="abcdef"
VERSION_TRACK="unstable"`},
{mkInfo("abcdef", "", otherDate, 1, 2, 0, 17), `
VERSION_MAJOR=1
VERSION_MINOR=2
VERSION_PATCH=0
VERSION_SHORT="1.2.0"
VERSION_LONG="1.2.0-17-tabcdef"
VERSION_GIT_HASH="abcdef"
VERSION_TRACK="stable"`},
{mkInfo("abcdef", "defghi", otherDate, 1, 15, 0, 129), `
VERSION_MAJOR=1
VERSION_MINOR=15
VERSION_PATCH=129
VERSION_SHORT="1.15.129"
VERSION_LONG="1.15.129-tabcdef-gdefghi"
VERSION_GIT_HASH="abcdef"
VERSION_TRACK="unstable"
VERSION_EXTRA_HASH="defghi"
VERSION_XCODE="101.15.129"
VERSION_XCODE_MACOS="273.27.3723"
VERSION_WINRES="1,15,129,0"
VERSION_MSIPRODUCT_AMD64="89C96952-1FB8-5A4D-B02E-16A8060C56AA"
VERSION_MSIPRODUCT_ARM64="DB1A2E86-66C4-5CEC-8F4C-7DB805370F3A"
VERSION_MSIPRODUCT_X86="DC57C0C3-5164-5C92-86B3-2800CEFF0540"`},
{mkInfo("abcdef", "", otherDate, 1, 2, 0, 17), `
VERSION_MAJOR=1
VERSION_MINOR=2
VERSION_PATCH=0
VERSION_SHORT="1.2.0"
VERSION_LONG="1.2.0-17-tabcdef"
VERSION_GIT_HASH="abcdef"
VERSION_TRACK="stable"`},
{mkInfo("abcdef", "defghi", otherDate, 1, 15, 0, 129), `
VERSION_MAJOR=1
VERSION_MINOR=15
VERSION_PATCH=129
VERSION_SHORT="1.15.129"
VERSION_LONG="1.15.129-tabcdef-gdefghi"
VERSION_GIT_HASH="abcdef"
VERSION_TRACK="unstable"
VERSION_EXTRA_HASH="defghi"
VERSION_XCODE="101.15.129"
VERSION_XCODE_MACOS="273.27.3723"
VERSION_WINRES="1,15,129,0"
VERSION_MSIPRODUCT_AMD64="89C96952-1FB8-5A4D-B02E-16A8060C56AA"
VERSION_MSIPRODUCT_ARM64="DB1A2E86-66C4-5CEC-8F4C-7DB805370F3A"
VERSION_MSIPRODUCT_X86="DC57C0C3-5164-5C92-86B3-2800CEFF0540"`},
{mkInfo("abcdef", "", otherDate, 0, 99, 5, 0), ""}, // unstable, patch number not allowed
{mkInfo("abcdef", "", otherDate, 0, 99, 5, 123), ""}, // unstable, patch number not allowed
{mkInfo("abcdef", "defghi", "", 1, 15, 0, 129), ""}, // missing otherDate
}
for _, test := range tests {
want := strings.ReplaceAll(strings.TrimSpace(test.want), " ", "")
info, err := mkOutput(test.in)
if err != nil {
if test.want != "" {
t.Errorf("%#v got unexpected error %v", test.in, err)
}
continue
}
got := strings.TrimSpace(info.String())
if diff := cmp.Diff(got, want); want != "" && diff != "" {
t.Errorf("%#v wrong output (-got+want):\n%s", test.in, diff)
}
}
}

View File

@@ -197,3 +197,124 @@ macaroni
vibe
vibes
banana
ulmer
bortle
palermo
torino
forel
ule
pyruvate
kardashev
ionian
aeolian
sunfish
gar
pike
muskellunge
pickerel
ruffe
walleye
bowfin
burbot
goldeye
mooneye
dace
quillback
stonecat
albacore
alewife
amberjack
codlet
char
searobin
arowana
bonito
saury
ayu
silverside
banjo
barb
barbel
bangus
banfish
ray
danio
betta
bigeye
bicolor
bitterling
bleak
blenny
boga
duck
brill
brotula
buri
goby
catla
chimaera
cobia
dab
darter
discus
duckbill
drum
elver
featherback
garibaldi
ghost
ghoul
dojo
hake
halfmoon
halfbeak
hamlet
halibut
halosaur
hoki
huchen
ide
inanga
ilish
inconnu
dory
koi
kanyu
kokanue
lenok
ling
manta
marlin
mora
mulley
stargazer
nase
neon
daggertooth
noodlefish
notothen
tetra
orfe
opah
opaleye
pancake
panga
paradise
parore
pirarucu
pirate
platy
pleco
powan
pomano
paridae
porgy
rohu
rudd
skate
squeaker
tailor
uaru
vimba
wahoo
zebra

View File

@@ -364,3 +364,179 @@ zorse
pumapard
pizzly
coydog
whydah
pheasant
lyrebird
peafowl
tayra
zorilla
mara
galago
tenrec
bettong
tamandua
cusimanse
polecat
degu
coatimundi
stringray
diplodocus
stegosaurus
zuul
allosaurus
baryonyx
edmontosaurus
iguanodon
minmi
triceratops
troodon
trex
tyrannosarus
shetland
pinto
appaloosa
auxois
shire
brumby
fell
java
pony
welara
mammoth
burro
poitou
spotted
snowy
barn
barred
boreal
elf
barking
buru
chaco
chestnut
cinnebar
cloudforest
dusky
tawny
berylline
calliope
rufous
xantu
violetear
mango
bumblebee
emerald
cinnamon
golden
pumpkinseed
ruffe
walleye
perch
bowfin
burbot
goldeye
mooneye
dace
quillback
stonecat
albacore
alewife
amberjack
sole
codlet
char
searobin
arowana
bonito
saury
ayu
silverside
banjo
barb
barbel
bangus
batfish
danio
betta
bigeye
bicolor
bitterling
bleak
blenny
tuna
boga
duck
brill
brotula
buri
goby
catla
chimaera
cobia
coho
dab
darter
discus
duckbill
drum
elver
featherback
garibaldi
ghost
ghoul
goblin
dojo
hake
halfmoon
halfbeak
hamlet
halibut
halosaur
hoki
huchen
ide
inanga
ilish
inconnu
dory
koi
kanyu
kokanee
lenok
ling
mahi
marlin
mora
morray
mulley
stargazer
nase
neon
daggertooth
noodlefish
notothen
tetra
orfe
opah
opaleye
pancake
panga
paradise
parore
piarucu
pirate
platy
pleco
powan
pompano
sparidae
porgy
rohu
rudd
skate
squeaker
tailor
uaru
vimba
wahoo