Compare commits
49 Commits
will/vizer
...
aaron/migr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ffb37f54c8 | ||
|
|
780c56e119 | ||
|
|
e484e1c0fc | ||
|
|
bf7573c9ee | ||
|
|
9ab992e7a1 | ||
|
|
0582829e00 | ||
|
|
e851d134cf | ||
|
|
04be5ea725 | ||
|
|
d4122c9f0a | ||
|
|
b0eba129e6 | ||
|
|
0ab6a7e7f5 | ||
|
|
587eb32a83 | ||
|
|
cf74ee49ee | ||
|
|
fc4b25d9fd | ||
|
|
44e027abca | ||
|
|
46467e39c2 | ||
|
|
daa2f1c66e | ||
|
|
64181e17c8 | ||
|
|
66621ab38e | ||
|
|
7444dabb68 | ||
|
|
abc874b04e | ||
|
|
61a345c8e1 | ||
|
|
06a10125fc | ||
|
|
7e65a11df5 | ||
|
|
499d82af8a | ||
|
|
860734aed9 | ||
|
|
0b8f89c79c | ||
|
|
f9b746846f | ||
|
|
e220fa65dd | ||
|
|
cd18bb68a4 | ||
|
|
d38abe90be | ||
|
|
5a2fa3aa95 | ||
|
|
5787989d74 | ||
|
|
6dabb34c7f | ||
|
|
093139fafd | ||
|
|
3db894b78c | ||
|
|
306c8a713c | ||
|
|
149de5e6d6 | ||
|
|
45d9784f9d | ||
|
|
303048a7d5 | ||
|
|
e8a028cf82 | ||
|
|
a7eab788e4 | ||
|
|
1ba0b7fd79 | ||
|
|
7ca54c890e | ||
|
|
8ed27d65ef | ||
|
|
1dadbbb72a | ||
|
|
d811c5a7f0 | ||
|
|
4a99481a11 | ||
|
|
8b9ee7a558 |
2
.github/workflows/codeql-analysis.yml
vendored
2
.github/workflows/codeql-analysis.yml
vendored
@@ -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'
|
||||
|
||||
|
||||
48
.github/workflows/test.yml
vendored
48
.github/workflows/test.yml
vendored
@@ -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) }}
|
||||
|
||||
1
.github/workflows/tsconnect-pkg-publish.yml
vendored
1
.github/workflows/tsconnect-pkg-publish.yml
vendored
@@ -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
9
.gitignore
vendored
@@ -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
|
||||
|
||||
45
Makefile
45
Makefile
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
28
cmd/dist/dist.go
vendored
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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 == "" {
|
||||
|
||||
44
cmd/mkversion/mkversion.go
Normal file
44
cmd/mkversion/mkversion.go
Normal 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())
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
4
go.mod
@@ -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
|
||||
|
||||
@@ -1 +1 @@
|
||||
sha256-y6WQxNJQeqKzmG/n1f5EkH7pxdqDyMmBlyfwhSK4wJE=
|
||||
sha256-zyyqBRFPNPzPYCMgnbnOy5rb3fkn4XEHZlTlJvwqunM=
|
||||
|
||||
4
go.sum
4
go.sum
@@ -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=
|
||||
|
||||
@@ -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>")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
13
release/deb/debian.postinst.sh
Executable 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
17
release/deb/debian.postrm.sh
Executable 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
7
release/deb/debian.prerm.sh
Executable 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
140
release/dist/cli/cli.go
vendored
Normal 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
271
release/dist/dist.go
vendored
Normal 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
86
release/dist/memoize.go
vendored
Normal 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
378
release/dist/unixpkgs/pkgs.go
vendored
Normal 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
119
release/dist/unixpkgs/targets.go
vendored
Normal 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
41
release/rpm/rpm.postinst.sh
Executable 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
8
release/rpm/rpm.postrm.sh
Executable 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
8
release/rpm/rpm.prerm.sh
Executable 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
|
||||
@@ -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
|
||||
|
||||
@@ -16,4 +16,4 @@
|
||||
) {
|
||||
src = ./.;
|
||||
}).shellNix
|
||||
# nix-direnv cache busting line: sha256-y6WQxNJQeqKzmG/n1f5EkH7pxdqDyMmBlyfwhSK4wJE=
|
||||
# nix-direnv cache busting line: sha256-zyyqBRFPNPzPYCMgnbnOy5rb3fkn4XEHZlTlJvwqunM=
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
21
tka/sig.go
21
tka/sig.go
@@ -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
79
tool/go
@@ -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
183
tool/gocross/autoflags.go
Normal 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()
|
||||
}
|
||||
409
tool/gocross/autoflags_test.go
Normal file
409
tool/gocross/autoflags_test.go
Normal 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
131
tool/gocross/env.go
Normal 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
99
tool/gocross/env_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
20
tool/gocross/exec_other.go
Normal file
20
tool/gocross/exec_other.go
Normal 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
12
tool/gocross/exec_unix.go
Normal 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
85
tool/gocross/gocross-wrapper.sh
Executable 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
129
tool/gocross/gocross.go
Normal 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
90
tool/gocross/goroot.go
Normal 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
182
tool/gocross/toolchain.go
Normal 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
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
72
util/ringbuffer/ringbuffer.go
Normal file
72
util/ringbuffer/ringbuffer.go
Normal 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
|
||||
}
|
||||
55
util/ringbuffer/ringbuffer_test.go
Normal file
55
util/ringbuffer/ringbuffer_test.go
Normal 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")
|
||||
}
|
||||
})
|
||||
}
|
||||
484
version/mkversion/mkversion.go
Normal file
484
version/mkversion/mkversion.go
Normal 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
|
||||
}
|
||||
131
version/mkversion/mkversion_test.go
Normal file
131
version/mkversion/mkversion_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
121
words/scales.txt
121
words/scales.txt
@@ -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
|
||||
|
||||
176
words/tails.txt
176
words/tails.txt
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user