Compare commits

..

1 Commits

Author SHA1 Message Date
Denton Gentry
c41c4f9fb5 debugging azure
Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2023-03-19 06:07:45 -07:00
480 changed files with 10071 additions and 39158 deletions

View File

@@ -1,15 +0,0 @@
name: "Dockerfile build"
on:
push:
branches:
- main
pull_request:
branches:
- "*"
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: "Build Docker image"
run: docker build .

View File

@@ -25,7 +25,7 @@ jobs:
uses: actions/checkout@v3
- name: Set up Go
uses: actions/setup-go@v4
uses: actions/setup-go@v3
with:
go-version-file: go.mod
@@ -50,11 +50,11 @@ jobs:
private_key: ${{ secrets.LICENSING_APP_PRIVATE_KEY }}
- name: Send pull request
uses: peter-evans/create-pull-request@284f54f989303d2699d373481a0cfa13ad5a6666 #v5.0.1
uses: peter-evans/create-pull-request@ad43dccb4d726ca8514126628bec209b8354b6dd #v4.1.4
with:
token: ${{ steps.generate-token.outputs.token }}
author: License Updater <noreply+license-updater@tailscale.com>
committer: License Updater <noreply+license-updater@tailscale.com>
author: License Updater <noreply@tailscale.com>
committer: License Updater <noreply@tailscale.com>
branch: licenses/cli
commit-message: "licenses: update tailscale{,d} licenses"
title: "licenses: update tailscale{,d} licenses"

View File

@@ -1,40 +0,0 @@
name: golangci-lint
on:
# For now, only lint pull requests, not the main branches.
pull_request:
# TODO(andrew): enable for main branch after an initial waiting period.
#push:
# branches:
# - main
workflow_dispatch:
permissions:
contents: read
pull-requests: read
concurrency:
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
golangci:
name: lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v4
with:
go-version-file: go.mod
cache: false
- name: golangci-lint
# Note: this is the 'v3' tag as of 2023-04-17
uses: golangci/golangci-lint-action@639cd343e1d3b897ff35927a75193d57cfcba299
with:
version: v1.52.2
# Show only new issues if it's a pull request.
only-new-issues: true

View File

@@ -1,37 +0,0 @@
name: govulncheck
on:
schedule:
- cron: "0 12 * * *" # 8am EST / 10am PST / 12pm UTC
workflow_dispatch: # allow manual trigger for testing
pull_request:
paths:
- ".github/workflows/govulncheck.yml"
jobs:
source-scan:
runs-on: ubuntu-latest
steps:
- name: Check out code into the Go module directory
uses: actions/checkout@v3
- name: Install govulncheck
run: ./tool/go install golang.org/x/vuln/cmd/govulncheck@latest
- name: Scan source code for known vulnerabilities
run: PATH=$PWD/tool/:$PATH "$(./tool/go env GOPATH)/bin/govulncheck" -test ./...
- uses: ruby/action-slack@v3.2.1
with:
payload: >
{
"attachments": [{
"text": "${{ job.status }}: ${{ github.workflow }} <https://github.com/${{ github.repository }}/commit/${{ github.sha }}/checks>
(<https://github.com/${{ github.repository }}/commit/${{ github.sha }}|commit>) of ${{ github.repository }}@${{ github.ref_name }} by ${{ github.event.head_commit.committer.name }}",
"color": "danger"
}]
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
if: failure() && github.event_name == 'schedule'

View File

@@ -1,102 +0,0 @@
name: test installer.sh
on:
push:
branches:
- "main"
paths:
- scripts/installer.sh
pull_request:
branches:
- "*"
paths:
- scripts/installer.sh
jobs:
test:
strategy:
# Don't abort the entire matrix if one element fails.
fail-fast: false
# Don't start all of these at once, which could saturate Github workers.
max-parallel: 4
matrix:
image:
# This is a list of Docker images against which we test our installer.
# If you find that some of these no longer exist, please feel free
# to remove them from the list.
# When adding new images, please only use official ones.
- "debian:oldstable-slim"
- "debian:stable-slim"
- "debian:testing-slim"
- "debian:sid-slim"
- "ubuntu:18.04"
- "ubuntu:20.04"
- "ubuntu:22.04"
- "ubuntu:22.10"
- "ubuntu:23.04"
- "elementary/docker:stable"
- "elementary/docker:unstable"
- "parrotsec/core:lts-amd64"
- "parrotsec/core:latest"
- "kalilinux/kali-rolling"
- "kalilinux/kali-dev"
- "oraclelinux:9"
- "oraclelinux:8"
- "fedora:latest"
- "rockylinux:8.7"
- "rockylinux:9"
- "amazonlinux:latest"
- "opensuse/leap:latest"
- "opensuse/tumbleweed:latest"
- "archlinux:latest"
- "alpine:3.14"
- "alpine:latest"
- "alpine:edge"
deps:
# Run all images installing curl as a dependency.
- curl
include:
# Check a few images with wget rather than curl.
- { image: "debian:oldstable-slim", deps: "wget" }
- { image: "debian:sid-slim", deps: "wget" }
- { image: "ubuntu:23.04", deps: "wget" }
# Ubuntu 16.04 also needs apt-transport-https installed.
- { image: "ubuntu:16.04", deps: "curl apt-transport-https" }
- { image: "ubuntu:16.04", deps: "wget apt-transport-https" }
runs-on: ubuntu-latest
container:
image: ${{ matrix.image }}
options: --user root
steps:
- name: install dependencies (yum)
# tar and gzip are needed by the actions/checkout below.
run: yum install -y --allowerasing tar gzip ${{ matrix.deps }}
if: |
contains(matrix.image, 'centos')
|| contains(matrix.image, 'oraclelinux')
|| contains(matrix.image, 'fedora')
|| contains(matrix.image, 'amazonlinux')
- name: install dependencies (zypper)
# tar and gzip are needed by the actions/checkout below.
run: zypper --non-interactive install tar gzip
if: contains(matrix.image, 'opensuse')
- name: install dependencies (apt-get)
run: |
apt-get update
apt-get install -y ${{ matrix.deps }}
if: |
contains(matrix.image, 'debian')
|| contains(matrix.image, 'ubuntu')
|| contains(matrix.image, 'elementary')
|| contains(matrix.image, 'parrotsec')
|| contains(matrix.image, 'kalilinux')
- name: checkout
uses: actions/checkout@v3
- name: run installer
run: scripts/installer.sh
# Package installation can fail in docker because systemd is not running
# as PID 1, so ignore errors at this step. The real check is the
# `tailscale --version` command below.
continue-on-error: true
- name: check tailscale version
run: tailscale --version

View File

@@ -46,31 +46,14 @@ jobs:
include:
- goarch: amd64
- goarch: amd64
buildflags: "-race"
variant: race
- goarch: "386" # thanks yaml
runs-on: ubuntu-22.04
steps:
- name: checkout
uses: actions/checkout@v3
- name: Restore Cache
uses: actions/cache@v3
with:
# Note: unlike the other setups, this is only grabbing the mod download
# cache, rather than the whole mod directory, as the download cache
# contains zips that can be unpacked in parallel faster than they can be
# fetched and extracted by tar
path: |
~/.cache/go-build
~/go/pkg/mod/cache
~\AppData\Local\go-build
# The -2- here should be incremented when the scheme of data to be
# cached changes (e.g. path above changes).
key: ${{ github.job }}-${{ runner.os }}-${{ matrix.goarch }}-${{ matrix.buildflags }}-go-2-${{ hashFiles('**/go.sum') }}-${{ github.run_id }}
restore-keys: |
${{ github.job }}-${{ runner.os }}-${{ matrix.goarch }}-${{ matrix.buildflags }}-go-2-${{ hashFiles('**/go.sum') }}
${{ github.job }}-${{ runner.os }}-${{ matrix.goarch }}-${{ matrix.buildflags }}-go-2-
- name: build all
run: ./tool/go build ${{matrix.buildflags}} ./...
run: ./tool/go build ./...
env:
GOARCH: ${{ matrix.goarch }}
- name: build variant CLIs
@@ -90,11 +73,13 @@ jobs:
- name: build test wrapper
run: ./tool/go build -o /tmp/testwrapper ./cmd/testwrapper
- name: test all
run: PATH=$PWD/tool:$PATH /tmp/testwrapper ./... ${{matrix.buildflags}}
if: matrix.variant != 'race'
run: ./tool/go test -exec=/tmp/testwrapper -bench=. -benchtime=1x ./...
env:
GOARCH: ${{ matrix.goarch }}
- name: bench all
run: PATH=$PWD/tool:$PATH /tmp/testwrapper ./... ${{matrix.buildflags}} -bench=. -benchtime=1x -run=^$
- name: test all (race)
if: matrix.variant == 'race'
run: ./tool/go test -race -exec=/tmp/testwrapper -bench=. -benchtime=1x ./...
env:
GOARCH: ${{ matrix.goarch }}
- name: check that no tracked files changed
@@ -116,13 +101,6 @@ jobs:
steps:
- name: checkout
uses: actions/checkout@v3
- name: Install Go
uses: actions/setup-go@v4
with:
go-version-file: go.mod
cache: false
- name: Restore Cache
uses: actions/cache@v3
with:
@@ -131,20 +109,17 @@ jobs:
# contains zips that can be unpacked in parallel faster than they can be
# fetched and extracted by tar
path: |
~/.cache/go-build
~/go/pkg/mod/cache
~\AppData\Local\go-build
# The -2- here should be incremented when the scheme of data to be
# cached changes (e.g. path above changes).
key: ${{ github.job }}-${{ runner.os }}-go-2-${{ hashFiles('**/go.sum') }}-${{ github.run_id }}
restore-keys: |
${{ github.job }}-${{ runner.os }}-go-2-${{ hashFiles('**/go.sum') }}
${{ github.job }}-${{ runner.os }}-go-2-
# TODO(raggi): add a go version here.
key: ${{ runner.os }}-go-2-${{ hashFiles('**/go.sum') }}
- name: test
# Don't use -bench=. -benchtime=1x.
# Somewhere in the layers (powershell?)
# the equals signs cause great confusion.
run: go test -bench . -benchtime 1x ./...
run: ./tool/go test -bench . -benchtime 1x ./...
vm:
runs-on: ["self-hosted", "linux", "vm"]
@@ -199,23 +174,6 @@ jobs:
steps:
- name: checkout
uses: actions/checkout@v3
- name: Restore Cache
uses: actions/cache@v3
with:
# Note: unlike the other setups, this is only grabbing the mod download
# cache, rather than the whole mod directory, as the download cache
# contains zips that can be unpacked in parallel faster than they can be
# fetched and extracted by tar
path: |
~/.cache/go-build
~/go/pkg/mod/cache
~\AppData\Local\go-build
# The -2- here should be incremented when the scheme of data to be
# cached changes (e.g. path above changes).
key: ${{ github.job }}-${{ runner.os }}-${{ matrix.goos }}-${{ matrix.goarch }}-go-2-${{ hashFiles('**/go.sum') }}-${{ github.run_id }}
restore-keys: |
${{ github.job }}-${{ runner.os }}-${{ matrix.goos }}-${{ matrix.goarch }}-go-2-${{ hashFiles('**/go.sum') }}
${{ github.job }}-${{ runner.os }}-${{ matrix.goos }}-${{ matrix.goarch }}-go-2-
- name: build all
run: ./tool/go build ./cmd/...
env:
@@ -265,23 +223,6 @@ jobs:
steps:
- name: checkout
uses: actions/checkout@v3
- name: Restore Cache
uses: actions/cache@v3
with:
# Note: unlike the other setups, this is only grabbing the mod download
# cache, rather than the whole mod directory, as the download cache
# contains zips that can be unpacked in parallel faster than they can be
# fetched and extracted by tar
path: |
~/.cache/go-build
~/go/pkg/mod/cache
~\AppData\Local\go-build
# The -2- here should be incremented when the scheme of data to be
# cached changes (e.g. path above changes).
key: ${{ github.job }}-${{ runner.os }}-go-2-${{ hashFiles('**/go.sum') }}-${{ github.run_id }}
restore-keys: |
${{ github.job }}-${{ runner.os }}-go-2-${{ hashFiles('**/go.sum') }}
${{ github.job }}-${{ runner.os }}-go-2-
- name: build tsconnect client
run: ./tool/go build ./cmd/tsconnect/wasm ./cmd/tailscale/cli
env:
@@ -294,15 +235,6 @@ jobs:
./tool/go run ./cmd/tsconnect --fast-compression build
./tool/go run ./cmd/tsconnect --fast-compression build-pkg
tailscale_go: # Subset of tests that depend on our custom Go toolchain.
runs-on: ubuntu-22.04
steps:
- name: checkout
uses: actions/checkout@v3
- name: test tailscale_go
run: ./tool/go test -tags=tailscale_go,ts_enable_sockstats ./net/sockstats/...
fuzz:
# This target periodically breaks (see TS_FUZZ_CURRENTLY_BROKEN at the top
# of the file), so it's more complex than usual: the 'build fuzzers' step
@@ -440,7 +372,6 @@ jobs:
- cross
- ios
- wasm
- tailscale_go
- fuzz
- depaware
- go_generate
@@ -458,7 +389,7 @@ jobs:
# 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.2.1
uses: ruby/action-slack@v3.0.0
with:
payload: |
{
@@ -485,7 +416,6 @@ jobs:
- cross
- ios
- wasm
- tailscale_go
- fuzz
- depaware
- go_generate

View File

@@ -0,0 +1,31 @@
name: "@tailscale/connect npm publish"
on: workflow_dispatch
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Set up node
uses: actions/setup-node@v3
with:
node-version: "16.x"
registry-url: "https://registry.npmjs.org"
- name: Build package
# Build with build_dist.sh to ensure that version information is embedded.
# 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
- name: Publish
env:
NODE_AUTH_TOKEN: ${{ secrets.TSCONNECT_NPM_PUBLISH_AUTH_TOKEN }}
run: ./tool/yarn --cwd ./cmd/tsconnect/pkg publish --access public

View File

@@ -35,11 +35,11 @@ jobs:
private_key: ${{ secrets.LICENSING_APP_PRIVATE_KEY }}
- name: Send pull request
uses: peter-evans/create-pull-request@284f54f989303d2699d373481a0cfa13ad5a6666 #v5.0.1
uses: peter-evans/create-pull-request@ad43dccb4d726ca8514126628bec209b8354b6dd #v4.1.4
with:
token: ${{ steps.generate-token.outputs.token }}
author: Flakes Updater <noreply+flakes-updater@tailscale.com>
committer: Flakes Updater <noreply+flakes-updater@tailscale.com>
author: Flakes Updater <noreply@tailscale.com>
committer: Flakes Updater <noreply@tailscale.com>
branch: flakes
commit-message: "go.mod.sri: update SRI hash for go.mod changes"
title: "go.mod.sri: update SRI hash for go.mod changes"

3
.gitignore vendored
View File

@@ -35,8 +35,5 @@ cmd/tailscaled/tailscaled
# Ignore direnv nix-shell environment cache
.direnv/
.vite/
webui/node_modules
/gocross
/dist

View File

@@ -1,61 +0,0 @@
linters:
# Don't enable any linters by default; just the ones that we explicitly
# enable in the list below.
disable-all: true
enable:
- bidichk
- gofmt
- goimports
- misspell
- revive
# Configuration for how we run golangci-lint
run:
timeout: 5m
issues:
# Excluding configuration per-path, per-linter, per-text and per-source
exclude-rules:
# These are forks of an upstream package and thus are exempt from stylistic
# changes that would make pulling in upstream changes harder.
- path: tempfork/.*\.go
text: "File is not `gofmt`-ed with `-s` `-r 'interface{} -> any'`"
- path: util/singleflight/.*\.go
text: "File is not `gofmt`-ed with `-s` `-r 'interface{} -> any'`"
# Per-linter settings are contained in this top-level key
linters-settings:
# Enable all rules by default; we don't use invisible unicode runes.
bidichk:
gofmt:
rewrite-rules:
- pattern: 'interface{}'
replacement: 'any'
goimports:
misspell:
revive:
enable-all-rules: false
ignore-generated-header: true
rules:
- name: atomic
- name: context-keys-type
- name: defer
arguments: [[
# Calling 'recover' at the time a defer is registered (i.e. "defer recover()") has no effect.
"immediate-recover",
# Calling 'recover' outside of a deferred function has no effect
"recover",
# Returning values from a deferred function has no effect
"return",
]]
- name: duplicated-imports
- name: errorf
- name: string-of-int
- name: time-equal
- name: unconditional-recursion
- name: useless-break
- name: waitgroup-by-value

View File

@@ -47,7 +47,8 @@ RUN go install \
golang.org/x/crypto/ssh \
golang.org/x/crypto/acme \
nhooyr.io/websocket \
github.com/mdlayher/netlink
github.com/mdlayher/netlink \
golang.zx2c4.com/wireguard/device
COPY . .
@@ -72,4 +73,4 @@ RUN apk add --no-cache ca-certificates iptables iproute2 ip6tables
COPY --from=build-env /go/bin/* /usr/local/bin/
# For compat with the previous run.sh, although ideally you should be
# using build_docker.sh which sets an entrypoint for the image.
RUN mkdir /tailscale && ln -s /usr/local/bin/containerboot /tailscale/run.sh
RUN ln -s /usr/local/bin/containerboot /tailscale/run.sh

View File

@@ -2,4 +2,4 @@
# SPDX-License-Identifier: BSD-3-Clause
FROM alpine:3.16
RUN apk add --no-cache ca-certificates iptables iproute2 ip6tables iputils
RUN apk add --no-cache ca-certificates iptables iproute2 ip6tables

View File

@@ -48,10 +48,11 @@ staticcheck: ## Run staticcheck.io checks
./tool/go run honnef.co/go/tools/cmd/staticcheck -- $$(./tool/go list ./... | grep -v tempfork)
spk: ## Build synology package for ${SYNO_ARCH} architecture and ${SYNO_DSM} DSM version
./tool/go run ./cmd/dist build synology/dsm${SYNO_DSM}/${SYNO_ARCH}
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: ## Build synology packages for all architectures and DSM versions
./tool/go run ./cmd/dist build synology
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 ## Push and install synology package on ${SYNO_HOST} host
echo "Pushing SPK to root@${SYNO_HOST} (env var SYNO_HOST) ..."

View File

@@ -1 +1 @@
1.47.0
1.39.0

46
api.md
View File

@@ -101,8 +101,8 @@ You can also [list all devices in the tailnet](#list-tailnet-devices) to get the
``` jsonc
{
// addresses (array of strings) is a list of Tailscale IP
// addresses for the device, including both IPv4 (formatted as 100.x.y.z)
// and IPv6 (formatted as fd7a:115c:a1e0:a:b:c:d:e) addresses.
// addresses for the device, including both ipv4 (formatted as 100.x.y.z)
// and ipv6 (formatted as fd7a:115c:a1e0:a:b:c:d:e) addresses.
"addresses": [
"100.87.74.78",
"fd7a:115c:a1e0:ac82:4843:ca90:697d:c36e"
@@ -503,8 +503,7 @@ Returns the enabled and advertised subnet routes for a device.
POST /api/v2/device/{deviceID}/authorized
```
Authorize a device.
This call marks a device as authorized or revokes its authorization for tailnets where device authorization is required, according to the `authorized` field in the payload.
Authorize a device. This call marks a device as authorized for tailnets where device authorization is required.
This returns a successful 2xx response with an empty JSON object in the response body.
@@ -516,8 +515,7 @@ The ID of the device.
#### `authorized` (required in `POST` body)
Specify whether the device is authorized. False to deauthorize an authorized device, and true to authorize a new device or to re-authorize a previously deauthorized device.
Specify whether the device is authorized. Only 'true' is currently supported.
``` jsonc
{
@@ -1115,21 +1113,6 @@ Look at the response body to determine whether there was a problem within your A
}
```
If your tailnet has [user and group provisioning](https://tailscale.com/kb/1180/sso-okta-scim/) turned on, we will also warn you about
any groups that are used in the policy file that are not being synced from SCIM. Explicitly defined groups will not trigger this warning.
```jsonc
{
"message":"warning(s) found",
"data":[
{
"user": "group:unknown@example.com",
"warnings":["group is not syncing from SCIM and will be ignored by rules in the policy file"]
}
]
}
```
<a href="tailnet-devices"></a>
## List tailnet devices
@@ -1238,11 +1221,6 @@ The remaining three methods operate on auth keys and API access tokens.
// expirySeconds (int) is the duration in seconds a new key is valid.
"expirySeconds": 86400
// description (string) is an optional short phrase that describes what
// this key is used for. It can be a maximum of 50 alphanumeric characters.
// Hyphens and underscores are also allowed.
"description": "short description of key purpose"
}
```
@@ -1329,9 +1307,6 @@ Note the following about required vs. optional values:
Specifies the duration in seconds until the key should expire.
Defaults to 90 days if not supplied.
- **`description`:** Optional in `POST` body.
A short string specifying the purpose of the key. Can be a maximum of 50 alphanumeric characters. Hyphens and spaces are also allowed.
### Request example
``` jsonc
@@ -1349,8 +1324,7 @@ curl "https://api.tailscale.com/api/v2/tailnet/example.com/keys" \
}
}
},
"expirySeconds": 86400,
"description": "dev access"
"expirySeconds": 86400
}'
```
@@ -1362,8 +1336,8 @@ It holds the capabilities specified in the request and can no longer be retrieve
``` jsonc
{
"id": "k123456CNTRL",
"key": "tskey-auth-k123456CNTRL-abcdefghijklmnopqrstuvwxyz",
"id": "XXXX456CNTRL",
"key": "tskey-k123456CNTRL-abcdefghijklmnopqrstuvwxyz",
"created": "2021-12-09T23:22:39Z",
"expires": "2022-03-09T23:22:39Z",
"revoked": "2022-03-12T23:22:39Z",
@@ -1374,10 +1348,9 @@ It holds the capabilities specified in the request and can no longer be retrieve
"ephemeral": false,
"preauthorized": false,
"tags": [ "tag:example" ]
}
}
}
},
"description": "dev access"
}
```
@@ -1429,8 +1402,7 @@ The response is a JSON object with information about the key supplied.
]
}
}
},
"description": "dev access"
}
}
```

View File

@@ -8,20 +8,14 @@
package atomicfile // import "tailscale.com/atomicfile"
import (
"fmt"
"os"
"path/filepath"
"runtime"
)
// WriteFile writes data to filename+some suffix, then renames it into filename.
// The perm argument is ignored on Windows. If the target filename already
// exists but is not a regular file, WriteFile returns an error.
// WriteFile writes data to filename+some suffix, then renames it
// into filename. The perm argument is ignored on Windows.
func WriteFile(filename string, data []byte, perm os.FileMode) (err error) {
fi, err := os.Stat(filename)
if err == nil && !fi.Mode().IsRegular() {
return fmt.Errorf("%s already exists and is not a regular file", filename)
}
f, err := os.CreateTemp(filepath.Dir(filename), filepath.Base(filename)+".tmp")
if err != nil {
return err

View File

@@ -1,47 +0,0 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build !js && !windows
package atomicfile
import (
"net"
"os"
"path/filepath"
"runtime"
"strings"
"testing"
)
func TestDoesNotOverwriteIrregularFiles(t *testing.T) {
// Per tailscale/tailscale#7658 as one example, almost any imagined use of
// atomicfile.Write should likely not attempt to overwrite an irregular file
// such as a device node, socket, or named pipe.
const filename = "TestDoesNotOverwriteIrregularFiles"
var path string
// macOS private temp does not allow unix socket creation, but /tmp does.
if runtime.GOOS == "darwin" {
path = filepath.Join("/tmp", filename)
t.Cleanup(func() { os.Remove(path) })
} else {
path = filepath.Join(t.TempDir(), filename)
}
// The least troublesome thing to make that is not a file is a unix socket.
// Making a null device sadly requires root.
l, err := net.ListenUnix("unix", &net.UnixAddr{Name: path, Net: "unix"})
if err != nil {
t.Fatal(err)
}
defer l.Close()
err = WriteFile(path, []byte("hello"), 0644)
if err == nil {
t.Fatal("expected error, got nil")
}
if !strings.Contains(err.Error(), "is not a regular file") {
t.Fatalf("unexpected error: %v", err)
}
}

View File

@@ -16,7 +16,7 @@ if [ -n "${TS_USE_TOOLCHAIN:-}" ]; then
go="./tool/go"
fi
eval `CGO_ENABLED=0 GOOS=$($go env GOHOSTOS) GOARCH=$($go env GOHOSTARCH) $go run ./cmd/mkversion`
eval `GOOS=$($go env GOHOSTOS) GOARCH=$($go env GOHOSTARCH) $go run ./cmd/mkversion`
if [ "$1" = "shellvars" ]; then
cat <<EOF
@@ -49,4 +49,4 @@ while [ "$#" -gt 1 ]; do
esac
done
exec $go build ${tags:+-tags=$tags} -ldflags "$ldflags" "$@"
exec ./tool/go build ${tags:+-tags=$tags} -ldflags "$ldflags" "$@"

View File

@@ -150,9 +150,8 @@ func (c *Client) ACLHuJSON(ctx context.Context) (acl *ACLHuJSON, err error) {
// ACLTestFailureSummary specifies the JSON format sent to the
// JavaScript client to be rendered in the HTML.
type ACLTestFailureSummary struct {
User string `json:"user,omitempty"`
Errors []string `json:"errors,omitempty"`
Warnings []string `json:"warnings,omitempty"`
User string `json:"user"`
Errors []string `json:"errors"`
}
// ACLTestError is ErrResponse but with an extra field to account for ACLTestFailureSummary.
@@ -437,7 +436,7 @@ func (c *Client) ValidateACLJSON(ctx context.Context, source, dest string) (test
}
}()
tests := []ACLTest{{User: source, Allow: []string{dest}}}
tests := []ACLTest{ACLTest{User: source, Allow: []string{dest}}}
postData, err := json.Marshal(tests)
if err != nil {
return nil, err

View File

@@ -10,14 +10,12 @@ import "tailscale.com/tailcfg"
const LocalAPIHost = "local-tailscaled.sock"
// WhoIsResponse is the JSON type returned by tailscaled debug server's /whois?ip=$IP handler.
// In successful whois responses, Node and UserProfile are never nil.
type WhoIsResponse struct {
Node *tailcfg.Node
UserProfile *tailcfg.UserProfile
// CapMap is a map of capabilities to their values.
// See tailcfg.PeerCapMap and tailcfg.PeerCapability for details.
CapMap tailcfg.PeerCapMap
// Caps are extra capabilities that the remote Node has to this node.
Caps []string `json:",omitempty"`
}
// FileTarget is a node to which files can be sent, and the PeerAPI

View File

@@ -12,6 +12,7 @@ import (
"fmt"
"net/http"
"net/url"
"strings"
"tailscale.com/types/opt"
)
@@ -212,20 +213,8 @@ func (c *Client) DeleteDevice(ctx context.Context, deviceID string) (err error)
// AuthorizeDevice marks a device as authorized.
func (c *Client) AuthorizeDevice(ctx context.Context, deviceID string) error {
return c.SetAuthorized(ctx, deviceID, true)
}
// SetAuthorized marks a device as authorized or not.
func (c *Client) SetAuthorized(ctx context.Context, deviceID string, authorized bool) error {
params := &struct {
Authorized bool `json:"authorized"`
}{Authorized: authorized}
data, err := json.Marshal(params)
if err != nil {
return err
}
path := fmt.Sprintf("%s/api/v2/device/%s/authorized", c.baseURL(), url.PathEscape(deviceID))
req, err := http.NewRequestWithContext(ctx, "POST", path, bytes.NewBuffer(data))
req, err := http.NewRequestWithContext(ctx, "POST", path, strings.NewReader(`{"authorized":true}`))
if err != nil {
return err
}

View File

@@ -63,7 +63,7 @@ func (c *Client) dnsGETRequest(ctx context.Context, endpoint string) ([]byte, er
return b, nil
}
func (c *Client) dnsPOSTRequest(ctx context.Context, endpoint string, postData any) ([]byte, error) {
func (c *Client) dnsPOSTRequest(ctx context.Context, endpoint string, postData interface{}) ([]byte, error) {
path := fmt.Sprintf("%s/api/v2/tailnet/%s/dns/%s", c.baseURL(), c.tailnet, endpoint)
data, err := json.Marshal(&postData)
if err != nil {

View File

@@ -68,32 +68,12 @@ func (c *Client) Keys(ctx context.Context) ([]string, error) {
}
// CreateKey creates a new key for the current user. Currently, only auth keys
// can be created. It returns the secret key itself, which cannot be retrieved again
// can be created. Returns the key itself, which cannot be retrieved again
// later, and the key metadata.
//
// To create a key with a specific expiry, use CreateKeyWithExpiry.
func (c *Client) CreateKey(ctx context.Context, caps KeyCapabilities) (keySecret string, keyMeta *Key, _ error) {
return c.CreateKeyWithExpiry(ctx, caps, 0)
}
// CreateKeyWithExpiry is like CreateKey, but allows specifying a expiration time.
//
// The time is truncated to a whole number of seconds. If zero, that means no expiration.
func (c *Client) CreateKeyWithExpiry(ctx context.Context, caps KeyCapabilities, expiry time.Duration) (keySecret string, keyMeta *Key, _ error) {
// convert expirySeconds to an int64 (seconds)
expirySeconds := int64(expiry.Seconds())
if expirySeconds < 0 {
return "", nil, fmt.Errorf("expiry must be positive")
}
if expirySeconds == 0 && expiry != 0 {
return "", nil, fmt.Errorf("non-zero expiry must be at least one second")
}
func (c *Client) CreateKey(ctx context.Context, caps KeyCapabilities) (string, *Key, error) {
keyRequest := struct {
Capabilities KeyCapabilities `json:"capabilities"`
ExpirySeconds int64 `json:"expirySeconds,omitempty"`
}{caps, int64(expirySeconds)}
Capabilities KeyCapabilities `json:"capabilities"`
}{caps}
bs, err := json.Marshal(keyRequest)
if err != nil {
return "", nil, err

View File

@@ -96,9 +96,8 @@ func (lc *LocalClient) defaultDialer(ctx context.Context, network, addr string)
// a TCP server on a random port, find the random port. For HTTP connections,
// we don't send the token. It gets added in an HTTP Basic-Auth header.
if port, _, err := safesocket.LocalTCPPortAndToken(); err == nil {
// We use 127.0.0.1 and not "localhost" (issue 7851).
var d net.Dialer
return d.DialContext(ctx, "tcp", "127.0.0.1:"+strconv.Itoa(port))
return d.DialContext(ctx, "tcp", "localhost:"+strconv.Itoa(port))
}
}
s := safesocket.DefaultConnectionStrategy(lc.socket())
@@ -946,57 +945,6 @@ func (lc *LocalClient) NetworkLockForceLocalDisable(ctx context.Context) error {
return nil
}
// NetworkLockVerifySigningDeeplink verifies the network lock deeplink contained
// in url and returns information extracted from it.
func (lc *LocalClient) NetworkLockVerifySigningDeeplink(ctx context.Context, url string) (*tka.DeeplinkValidationResult, error) {
vr := struct {
URL string
}{url}
body, err := lc.send(ctx, "POST", "/localapi/v0/tka/verify-deeplink", 200, jsonBody(vr))
if err != nil {
return nil, fmt.Errorf("sending verify-deeplink: %w", err)
}
return decodeJSON[*tka.DeeplinkValidationResult](body)
}
// NetworkLockGenRecoveryAUM generates an AUM for recovering from a tailnet-lock key compromise.
func (lc *LocalClient) NetworkLockGenRecoveryAUM(ctx context.Context, removeKeys []tkatype.KeyID, forkFrom tka.AUMHash) ([]byte, error) {
vr := struct {
Keys []tkatype.KeyID
ForkFrom string
}{removeKeys, forkFrom.String()}
body, err := lc.send(ctx, "POST", "/localapi/v0/tka/generate-recovery-aum", 200, jsonBody(vr))
if err != nil {
return nil, fmt.Errorf("sending generate-recovery-aum: %w", err)
}
return body, nil
}
// NetworkLockCosignRecoveryAUM co-signs a recovery AUM using the node's tailnet lock key.
func (lc *LocalClient) NetworkLockCosignRecoveryAUM(ctx context.Context, aum tka.AUM) ([]byte, error) {
r := bytes.NewReader(aum.Serialize())
body, err := lc.send(ctx, "POST", "/localapi/v0/tka/cosign-recovery-aum", 200, r)
if err != nil {
return nil, fmt.Errorf("sending cosign-recovery-aum: %w", err)
}
return body, nil
}
// NetworkLockSubmitRecoveryAUM submits a recovery AUM to the control plane.
func (lc *LocalClient) NetworkLockSubmitRecoveryAUM(ctx context.Context, aum tka.AUM) error {
r := bytes.NewReader(aum.Serialize())
_, err := lc.send(ctx, "POST", "/localapi/v0/tka/submit-recovery-aum", 200, r)
if err != nil {
return fmt.Errorf("sending cosign-recovery-aum: %w", err)
}
return nil
}
// SetServeConfig sets or replaces the serving settings.
// If config is nil, settings are cleared and serving is disabled.
func (lc *LocalClient) SetServeConfig(ctx context.Context, config *ipn.ServeConfig) error {
@@ -1124,27 +1072,6 @@ func (lc *LocalClient) DeleteProfile(ctx context.Context, profile ipn.ProfileID)
return err
}
// QueryFeature makes a request for instructions on how to enable a
// feature, such as Funnel, for the node's tailnet.
//
// This request itself does not directly enable the feature on behalf
// of the node, but rather returns information that can be presented
// to the acting user about where/how to enable the feature.
//
// If relevant, this includes a control URL the user can visit to
// explicitly consent to using the feature. LocalClient.WatchIPNBus
// can be used to block on the feature being enabled.
//
// 2023-08-02: Valid feature values are "serve" and "funnel".
func (lc *LocalClient) QueryFeature(ctx context.Context, feature string) (*tailcfg.QueryFeatureResponse, error) {
v := url.Values{"feature": {feature}}
body, err := lc.send(ctx, "POST", "/localapi/v0/query-feature?"+v.Encode(), 200, nil)
if err != nil {
return nil, fmt.Errorf("error %w: %s", err, body)
}
return decodeJSON[*tailcfg.QueryFeatureResponse](body)
}
func (lc *LocalClient) DebugDERPRegion(ctx context.Context, regionIDOrCode string) (*ipnstate.DebugDERPRegionReport, error) {
v := url.Values{"region": {regionIDOrCode}}
body, err := lc.send(ctx, "POST", "/localapi/v0/debug-derp-region?"+v.Encode(), 200, nil)
@@ -1174,6 +1101,7 @@ func (lc *LocalClient) StreamDebugCapture(ctx context.Context) (io.ReadCloser, e
}
res, err := lc.doLocalRequestNiceError(req)
if err != nil {
res.Body.Close()
return nil, err
}
if res.StatusCode != 200 {

View File

@@ -131,8 +131,6 @@ func gen(buf *bytes.Buffer, it *codegen.ImportTracker, typ *types.Named) {
} else {
writef("\tdst.%s[i] = src.%s[i].Clone()", fname, fname)
}
} else if ft.Elem().String() == "encoding/json.RawMessage" {
writef("\tdst.%s[i] = append(src.%s[i][:0:0], src.%s[i]...)", fname, fname, fname)
} else {
writef("\tdst.%s[i] = *src.%s[i].Clone()", fname, fname)
}

View File

@@ -81,7 +81,7 @@ func (m *manualCertManager) TLSConfig() *tls.Config {
return &tls.Config{
Certificates: nil,
NextProtos: []string{
"http/1.1",
"h2", "http/1.1", // enable HTTP/2
},
GetCertificate: m.getCertificate,
}

View File

@@ -3,80 +3,26 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
filippo.io/edwards25519 from github.com/hdevalence/ed25519consensus
filippo.io/edwards25519/field from filippo.io/edwards25519
W 💣 github.com/Microsoft/go-winio from tailscale.com/safesocket
W 💣 github.com/Microsoft/go-winio/internal/fs from github.com/Microsoft/go-winio
W 💣 github.com/Microsoft/go-winio/internal/socket from github.com/Microsoft/go-winio
W github.com/Microsoft/go-winio/internal/stringbuffer from github.com/Microsoft/go-winio/internal/fs
W github.com/Microsoft/go-winio/pkg/guid from github.com/Microsoft/go-winio+
W 💣 github.com/alexbrainman/sspi from github.com/alexbrainman/sspi/internal/common+
W github.com/alexbrainman/sspi/internal/common from github.com/alexbrainman/sspi/negotiate
W 💣 github.com/alexbrainman/sspi/negotiate from tailscale.com/net/tshttpproxy
github.com/beorn7/perks/quantile from github.com/prometheus/client_golang/prometheus
💣 github.com/cespare/xxhash/v2 from github.com/prometheus/client_golang/prometheus
L github.com/coreos/go-iptables/iptables from tailscale.com/util/linuxfw
github.com/fxamacker/cbor/v2 from tailscale.com/tka
github.com/golang/groupcache/lru from tailscale.com/net/dnscache
github.com/golang/protobuf/proto from github.com/matttproud/golang_protobuf_extensions/pbutil+
L github.com/google/nftables from tailscale.com/util/linuxfw
L 💣 github.com/google/nftables/alignedbuff from github.com/google/nftables/xt
L 💣 github.com/google/nftables/binaryutil from github.com/google/nftables+
L github.com/google/nftables/expr from github.com/google/nftables+
L github.com/google/nftables/internal/parseexprfunc from github.com/google/nftables+
L github.com/google/nftables/xt from github.com/google/nftables/expr+
github.com/hdevalence/ed25519consensus from tailscale.com/tka
L github.com/josharian/native from github.com/mdlayher/netlink+
L 💣 github.com/jsimonetti/rtnetlink from tailscale.com/net/interfaces+
L 💣 github.com/jsimonetti/rtnetlink from tailscale.com/net/interfaces
L github.com/jsimonetti/rtnetlink/internal/unix from github.com/jsimonetti/rtnetlink
github.com/klauspost/compress/flate from nhooyr.io/websocket
github.com/matttproud/golang_protobuf_extensions/pbutil from github.com/prometheus/common/expfmt
L 💣 github.com/mdlayher/netlink from github.com/jsimonetti/rtnetlink+
L 💣 github.com/mdlayher/netlink/nlenc from github.com/jsimonetti/rtnetlink+
L github.com/mdlayher/netlink/nltest from github.com/google/nftables
L 💣 github.com/mdlayher/socket from github.com/mdlayher/netlink
💣 github.com/mitchellh/go-ps from tailscale.com/safesocket
💣 github.com/prometheus/client_golang/prometheus from tailscale.com/tsweb/promvarz
github.com/prometheus/client_golang/prometheus/internal from github.com/prometheus/client_golang/prometheus
github.com/prometheus/client_model/go from github.com/prometheus/client_golang/prometheus+
github.com/prometheus/common/expfmt from github.com/prometheus/client_golang/prometheus+
github.com/prometheus/common/internal/bitbucket.org/ww/goautoneg from github.com/prometheus/common/expfmt
github.com/prometheus/common/model from github.com/prometheus/client_golang/prometheus+
LD github.com/prometheus/procfs from github.com/prometheus/client_golang/prometheus
LD github.com/prometheus/procfs/internal/fs from github.com/prometheus/procfs
LD github.com/prometheus/procfs/internal/util from github.com/prometheus/procfs
L 💣 github.com/tailscale/netlink from tailscale.com/util/linuxfw
L 💣 github.com/vishvananda/netlink/nl from github.com/tailscale/netlink
L github.com/vishvananda/netns from github.com/tailscale/netlink+
github.com/x448/float16 from github.com/fxamacker/cbor/v2
💣 go4.org/mem from tailscale.com/client/tailscale+
go4.org/netipx from tailscale.com/wgengine/filter+
go4.org/netipx from tailscale.com/wgengine/filter
W 💣 golang.zx2c4.com/wireguard/windows/tunnel/winipcfg from tailscale.com/net/interfaces+
google.golang.org/protobuf/encoding/prototext from github.com/golang/protobuf/proto+
google.golang.org/protobuf/encoding/protowire from github.com/golang/protobuf/proto+
google.golang.org/protobuf/internal/descfmt from google.golang.org/protobuf/internal/filedesc
google.golang.org/protobuf/internal/descopts from google.golang.org/protobuf/internal/filedesc+
google.golang.org/protobuf/internal/detrand from google.golang.org/protobuf/internal/descfmt+
google.golang.org/protobuf/internal/encoding/defval from google.golang.org/protobuf/internal/encoding/tag+
google.golang.org/protobuf/internal/encoding/messageset from google.golang.org/protobuf/encoding/prototext+
google.golang.org/protobuf/internal/encoding/tag from google.golang.org/protobuf/internal/impl
google.golang.org/protobuf/internal/encoding/text from google.golang.org/protobuf/encoding/prototext+
google.golang.org/protobuf/internal/errors from google.golang.org/protobuf/encoding/prototext+
google.golang.org/protobuf/internal/filedesc from google.golang.org/protobuf/internal/encoding/tag+
google.golang.org/protobuf/internal/filetype from google.golang.org/protobuf/runtime/protoimpl
google.golang.org/protobuf/internal/flags from google.golang.org/protobuf/encoding/prototext+
google.golang.org/protobuf/internal/genid from google.golang.org/protobuf/encoding/prototext+
💣 google.golang.org/protobuf/internal/impl from google.golang.org/protobuf/internal/filetype+
google.golang.org/protobuf/internal/order from google.golang.org/protobuf/encoding/prototext+
google.golang.org/protobuf/internal/pragma from google.golang.org/protobuf/encoding/prototext+
google.golang.org/protobuf/internal/set from google.golang.org/protobuf/encoding/prototext
💣 google.golang.org/protobuf/internal/strs from google.golang.org/protobuf/encoding/prototext+
google.golang.org/protobuf/internal/version from google.golang.org/protobuf/runtime/protoimpl
google.golang.org/protobuf/proto from github.com/golang/protobuf/proto+
google.golang.org/protobuf/reflect/protodesc from github.com/golang/protobuf/proto
💣 google.golang.org/protobuf/reflect/protoreflect from github.com/golang/protobuf/proto+
google.golang.org/protobuf/reflect/protoregistry from github.com/golang/protobuf/proto+
google.golang.org/protobuf/runtime/protoiface from github.com/golang/protobuf/proto+
google.golang.org/protobuf/runtime/protoimpl from github.com/golang/protobuf/proto+
google.golang.org/protobuf/types/descriptorpb from google.golang.org/protobuf/reflect/protodesc
google.golang.org/protobuf/types/known/timestamppb from github.com/prometheus/client_golang/prometheus+
nhooyr.io/websocket from tailscale.com/cmd/derper+
nhooyr.io/websocket/internal/errd from nhooyr.io/websocket
nhooyr.io/websocket/internal/xsync from nhooyr.io/websocket
@@ -98,13 +44,11 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
💣 tailscale.com/net/interfaces from tailscale.com/net/netns+
tailscale.com/net/netaddr from tailscale.com/ipn+
tailscale.com/net/netknob from tailscale.com/net/netns
tailscale.com/net/netmon from tailscale.com/net/sockstats+
tailscale.com/net/netns from tailscale.com/derp/derphttp
tailscale.com/net/netutil from tailscale.com/client/tailscale
tailscale.com/net/packet from tailscale.com/wgengine/filter
tailscale.com/net/sockstats from tailscale.com/derp/derphttp
tailscale.com/net/stun from tailscale.com/cmd/derper
L tailscale.com/net/tcpinfo from tailscale.com/derp
tailscale.com/net/tlsdial from tailscale.com/derp/derphttp
tailscale.com/net/tsaddr from tailscale.com/ipn+
💣 tailscale.com/net/tshttpproxy from tailscale.com/derp/derphttp+
@@ -115,12 +59,9 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
tailscale.com/tailcfg from tailscale.com/client/tailscale+
tailscale.com/tka from tailscale.com/client/tailscale+
W tailscale.com/tsconst from tailscale.com/net/interfaces
tailscale.com/tstime from tailscale.com/derp+
💣 tailscale.com/tstime/mono from tailscale.com/tstime/rate
tailscale.com/tstime/rate from tailscale.com/wgengine/filter+
tailscale.com/tstime/rate from tailscale.com/wgengine/filter
tailscale.com/tsweb from tailscale.com/cmd/derper
tailscale.com/tsweb/promvarz from tailscale.com/tsweb
tailscale.com/tsweb/varz from tailscale.com/tsweb+
tailscale.com/types/dnstype from tailscale.com/tailcfg
tailscale.com/types/empty from tailscale.com/ipn
tailscale.com/types/ipproto from tailscale.com/net/flowtrack+
@@ -138,15 +79,13 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
W tailscale.com/util/clientmetric from tailscale.com/net/tshttpproxy
tailscale.com/util/cloudenv from tailscale.com/hostinfo+
W tailscale.com/util/cmpver from tailscale.com/net/tshttpproxy
tailscale.com/util/cmpx from tailscale.com/cmd/derper+
L 💣 tailscale.com/util/dirwalk from tailscale.com/metrics
tailscale.com/util/dnsname from tailscale.com/hostinfo+
tailscale.com/util/httpm from tailscale.com/client/tailscale
tailscale.com/util/lineread from tailscale.com/hostinfo+
L tailscale.com/util/linuxfw from tailscale.com/net/netns
tailscale.com/util/mak from tailscale.com/syncs+
tailscale.com/util/multierr from tailscale.com/health+
tailscale.com/util/set from tailscale.com/health+
tailscale.com/util/multierr from tailscale.com/health
tailscale.com/util/set from tailscale.com/health
tailscale.com/util/singleflight from tailscale.com/net/dnscache
tailscale.com/util/slicesx from tailscale.com/cmd/derper+
tailscale.com/util/vizerror from tailscale.com/tsweb
@@ -169,12 +108,11 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
golang.org/x/crypto/nacl/secretbox from golang.org/x/crypto/nacl/box
golang.org/x/crypto/salsa20/salsa from golang.org/x/crypto/nacl/box+
golang.org/x/exp/constraints from golang.org/x/exp/slices
golang.org/x/exp/maps from tailscale.com/types/views
golang.org/x/exp/slices from tailscale.com/net/tsaddr+
L golang.org/x/net/bpf from github.com/mdlayher/netlink+
golang.org/x/net/dns/dnsmessage from net+
golang.org/x/net/http/httpguts from net/http
golang.org/x/net/http/httpproxy from net/http+
golang.org/x/net/http/httpproxy from net/http
golang.org/x/net/http2/hpack from net/http
golang.org/x/net/idna from golang.org/x/crypto/acme/autocert+
golang.org/x/net/proxy from tailscale.com/net/netns
@@ -231,17 +169,14 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
expvar from tailscale.com/cmd/derper+
flag from tailscale.com/cmd/derper
fmt from compress/flate+
go/token from google.golang.org/protobuf/internal/strs
hash from crypto+
hash/crc32 from compress/gzip+
hash/fnv from google.golang.org/protobuf/internal/detrand
hash/maphash from go4.org/mem
html from net/http/pprof+
io from bufio+
io/fs from crypto/x509+
io/ioutil from github.com/mitchellh/go-ps+
log from expvar+
log/internal from log
math from compress/flate+
math/big from crypto/dsa+
math/bits from compress/flate+
@@ -253,7 +188,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
net/http from expvar+
net/http/httptrace from net/http+
net/http/internal from net/http
net/http/pprof from tailscale.com/tsweb+
net/http/pprof from tailscale.com/tsweb
net/netip from go4.org/netipx+
net/textproto from golang.org/x/net/http/httpguts+
net/url from crypto/x509+
@@ -266,7 +201,6 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
regexp from internal/profile+
regexp/syntax from regexp
runtime/debug from golang.org/x/crypto/acme+
runtime/metrics from github.com/prometheus/client_golang/prometheus+
runtime/pprof from net/http/pprof
runtime/trace from net/http/pprof
sort from compress/flate+

View File

@@ -33,12 +33,11 @@ import (
"tailscale.com/net/stun"
"tailscale.com/tsweb"
"tailscale.com/types/key"
"tailscale.com/util/cmpx"
)
var (
dev = flag.Bool("dev", false, "run in localhost development mode (overrides -a)")
addr = flag.String("a", ":443", "server HTTP/HTTPS listen address, in form \":port\", \"ip:port\", or for IPv6 \"[ip]:port\". If the IP is omitted, it defaults to all interfaces. Serves HTTPS if the port is 443 and/or -certmode is manual, otherwise HTTP.")
dev = flag.Bool("dev", false, "run in localhost development mode")
addr = flag.String("a", ":443", "server HTTPS listen address, in form \":port\", \"ip:port\", or for IPv6 \"[ip]:port\". If the IP is omitted, it defaults to all interfaces.")
httpPort = flag.Int("http-port", 80, "The port on which to serve HTTP. Set to -1 to disable. The listener is bound to the same IP (if any) as specified in the -a flag.")
stunPort = flag.Int("stun-port", 3478, "The UDP port on which to serve STUN. The listener is bound to the same IP (if any) as specified in the -a flag.")
configPath = flag.String("c", "", "config file path")
@@ -182,9 +181,8 @@ func main() {
}
mux.HandleFunc("/derp/probe", probeHandler)
go refreshBootstrapDNSLoop()
mux.HandleFunc("/bootstrap-dns", tsweb.BrowserHeaderHandlerFunc(handleBootstrapDNS))
mux.HandleFunc("/bootstrap-dns", handleBootstrapDNS)
mux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
tsweb.AddBrowserHeaders(w)
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(200)
io.WriteString(w, `<html><body>
@@ -204,7 +202,6 @@ func main() {
}
}))
mux.Handle("/robots.txt", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
tsweb.AddBrowserHeaders(w)
io.WriteString(w, "User-agent: *\nDisallow: /\n")
}))
mux.Handle("/generate_204", http.HandlerFunc(serveNoContent))
@@ -279,6 +276,18 @@ func main() {
defer tlsActiveVersion.Add(label, -1)
}
// Set HTTP headers to appease automated security scanners.
//
// Security automation gets cranky when HTTPS sites don't
// set HSTS, and when they don't specify a content
// security policy for XSS mitigation.
//
// DERP's HTTP interface is only ever used for debug
// access (for which trivial safe policies work just
// fine), and by DERP clients which don't obey any of
// these browser-centric headers anyway.
w.Header().Set("Strict-Transport-Security", "max-age=63072000; includeSubDomains")
w.Header().Set("Content-Security-Policy", "default-src 'none'; frame-ancestors 'none'; form-action 'none'; base-uri 'self'; block-all-mixed-content; plugin-types 'none'")
mux.ServeHTTP(w, r)
})
if *httpPort > -1 {
@@ -427,7 +436,11 @@ func defaultMeshPSKFile() string {
}
func rateLimitedListenAndServeTLS(srv *http.Server) error {
ln, err := net.Listen("tcp", cmpx.Or(srv.Addr, ":https"))
addr := srv.Addr
if addr == "" {
addr = ":https"
}
ln, err := net.Listen("tcp", addr)
if err != nil {
return err
}

View File

@@ -5,6 +5,7 @@
package main
import (
"expvar"
"flag"
"fmt"
"html"
@@ -22,14 +23,13 @@ var (
derpMapURL = flag.String("derp-map", "https://login.tailscale.com/derpmap/default", "URL to DERP map (https:// or file://)")
listen = flag.String("listen", ":8030", "HTTP listen address")
probeOnce = flag.Bool("once", false, "probe once and print results, then exit; ignores the listen flag")
spread = flag.Bool("spread", true, "whether to spread probing over time")
interval = flag.Duration("interval", 15*time.Second, "probe interval")
)
func main() {
flag.Parse()
p := prober.New().WithSpread(*spread).WithOnce(*probeOnce).WithMetricNamespace("derpprobe")
p := prober.New().WithSpread(true).WithOnce(*probeOnce)
dp, err := prober.DERP(p, *derpMapURL, *interval, *interval, *interval)
if err != nil {
log.Fatal(err)
@@ -52,6 +52,7 @@ func main() {
mux := http.NewServeMux()
tsweb.Debugger(mux)
expvar.Publish("derpprobe", p.Expvar())
mux.HandleFunc("/", http.HandlerFunc(serveFunc(p)))
log.Fatal(http.ListenAndServe(*listen, mux))
}

27
cmd/dist/dist.go vendored
View File

@@ -13,38 +13,15 @@ import (
"tailscale.com/release/dist"
"tailscale.com/release/dist/cli"
"tailscale.com/release/dist/synology"
"tailscale.com/release/dist/unixpkgs"
)
var synologyPackageCenter bool
func getTargets(signers unixpkgs.Signers) ([]dist.Target, error) {
var ret []dist.Target
ret = append(ret, unixpkgs.Targets(signers)...)
// Synology packages can be built either for sideloading, or for
// distribution by Synology in their package center. When
// distributed through the package center, apps can request
// additional permissions to use a tuntap interface and control
// the NAS's network stack, rather than be forced to run in
// userspace mode.
//
// Since only we can provide packages to Synology for
// distribution, we default to building the "sideload" variant of
// packages that we distribute on pkgs.tailscale.com.
ret = append(ret, synology.Targets(synologyPackageCenter)...)
return ret, nil
func getTargets() ([]dist.Target, error) {
return unixpkgs.Targets(), nil
}
func main() {
cmd := cli.CLI(getTargets)
for _, subcmd := range cmd.Subcommands {
if subcmd.Name == "build" {
subcmd.FlagSet.BoolVar(&synologyPackageCenter, "synology-package-center", false, "build synology packages with extra metadata for the official package center")
}
}
if err := cmd.ParseAndRun(context.Background(), os.Args[1:]); err != nil && !errors.Is(err, flag.ErrHelp) {
log.Fatal(err)
}

View File

@@ -16,7 +16,6 @@ import (
"golang.org/x/oauth2/clientcredentials"
"tailscale.com/client/tailscale"
"tailscale.com/util/cmpx"
)
func main() {
@@ -30,9 +29,9 @@ func main() {
tags := flag.String("tags", "", "comma-separated list of tags to apply to the authkey")
flag.Parse()
clientID := os.Getenv("TS_API_CLIENT_ID")
clientId := os.Getenv("TS_API_CLIENT_ID")
clientSecret := os.Getenv("TS_API_CLIENT_SECRET")
if clientID == "" || clientSecret == "" {
if clientId == "" || clientSecret == "" {
log.Fatal("TS_API_CLIENT_ID and TS_API_CLIENT_SECRET must be set")
}
@@ -40,19 +39,22 @@ func main() {
log.Fatal("at least one tag must be specified")
}
baseURL := cmpx.Or(os.Getenv("TS_BASE_URL"), "https://api.tailscale.com")
baseUrl := os.Getenv("TS_BASE_URL")
if baseUrl == "" {
baseUrl = "https://api.tailscale.com"
}
credentials := clientcredentials.Config{
ClientID: clientID,
ClientID: clientId,
ClientSecret: clientSecret,
TokenURL: baseURL + "/api/v2/oauth/token",
TokenURL: baseUrl + "/api/v2/oauth/token",
Scopes: []string{"device"},
}
ctx := context.Background()
tsClient := tailscale.NewClient("-", nil)
tsClient.HTTPClient = credentials.Client(ctx)
tsClient.BaseURL = baseURL
tsClient.BaseURL = baseUrl
caps := tailscale.KeyCapabilities{
Devices: tailscale.KeyDeviceCapabilities{

View File

@@ -23,7 +23,6 @@ import (
"github.com/peterbourgon/ff/v3/ffcli"
"github.com/tailscale/hujson"
"golang.org/x/oauth2/clientcredentials"
"tailscale.com/client/tailscale"
"tailscale.com/util/httpm"
)
@@ -271,7 +270,7 @@ func applyNewACL(ctx context.Context, client *http.Client, tailnet, apiKey, poli
got := resp.StatusCode
want := http.StatusOK
if got != want {
var ate ACLGitopsTestError
var ate ACLTestError
err := json.NewDecoder(resp.Body).Decode(&ate)
if err != nil {
return err
@@ -307,7 +306,7 @@ func testNewACLs(ctx context.Context, client *http.Client, tailnet, apiKey, poli
}
defer resp.Body.Close()
var ate ACLGitopsTestError
var ate ACLTestError
err = json.NewDecoder(resp.Body).Decode(&ate)
if err != nil {
return err
@@ -328,12 +327,12 @@ func testNewACLs(ctx context.Context, client *http.Client, tailnet, apiKey, poli
var lineColMessageSplit = regexp.MustCompile(`line ([0-9]+), column ([0-9]+): (.*)$`)
// ACLGitopsTestError is redefined here so we can add a custom .Error() response
type ACLGitopsTestError struct {
tailscale.ACLTestError
type ACLTestError struct {
Message string `json:"message"`
Data []ACLTestErrorDetail `json:"data"`
}
func (ate ACLGitopsTestError) Error() string {
func (ate ACLTestError) Error() string {
var sb strings.Builder
if *githubSyntax && lineColMessageSplit.MatchString(ate.Message) {
@@ -350,28 +349,20 @@ func (ate ACLGitopsTestError) Error() string {
fmt.Fprintln(&sb)
for _, data := range ate.Data {
if data.User != "" {
fmt.Fprintf(&sb, "For user %s:\n", data.User)
}
if len(data.Errors) > 0 {
fmt.Fprint(&sb, "Errors found:\n")
for _, err := range data.Errors {
fmt.Fprintf(&sb, "- %s\n", err)
}
}
if len(data.Warnings) > 0 {
fmt.Fprint(&sb, "Warnings found:\n")
for _, err := range data.Warnings {
fmt.Fprintf(&sb, "- %s\n", err)
}
fmt.Fprintf(&sb, "For user %s:\n", data.User)
for _, err := range data.Errors {
fmt.Fprintf(&sb, "- %s\n", err)
}
}
return sb.String()
}
type ACLTestErrorDetail struct {
User string `json:"user"`
Errors []string `json:"errors"`
}
func getACLETag(ctx context.Context, client *http.Client, tailnet, apiKey string) (string, error) {
req, err := http.NewRequestWithContext(ctx, httpm.GET, fmt.Sprintf("https://%s/api/v2/tailnet/%s/acl", *apiServer, tailnet), nil)
if err != nil {

View File

@@ -1,55 +0,0 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package main
import (
"encoding/json"
"strings"
"testing"
"tailscale.com/client/tailscale"
)
func TestEmbeddedTypeUnmarshal(t *testing.T) {
var gitopsErr ACLGitopsTestError
gitopsErr.Message = "gitops response error"
gitopsErr.Data = []tailscale.ACLTestFailureSummary{
{
User: "GitopsError",
Errors: []string{"this was initially created as a gitops error"},
},
}
var aclTestErr tailscale.ACLTestError
aclTestErr.Message = "native ACL response error"
aclTestErr.Data = []tailscale.ACLTestFailureSummary{
{
User: "ACLError",
Errors: []string{"this was initially created as an ACL error"},
},
}
t.Run("unmarshal gitops type from acl type", func(t *testing.T) {
b, _ := json.Marshal(aclTestErr)
var e ACLGitopsTestError
err := json.Unmarshal(b, &e)
if err != nil {
t.Fatal(err)
}
if !strings.Contains(e.Error(), "For user ACLError") { // the gitops error prints out the user, the acl error doesn't
t.Fatalf("user heading for 'ACLError' not found in gitops error: %v", e.Error())
}
})
t.Run("unmarshal acl type from gitops type", func(t *testing.T) {
b, _ := json.Marshal(gitopsErr)
var e tailscale.ACLTestError
err := json.Unmarshal(b, &e)
if err != nil {
t.Fatal(err)
}
expectedErr := `Status: 0, Message: "gitops response error", Data: [{User:GitopsError Errors:[this was initially created as a gitops error] Warnings:[]}]`
if e.Error() != expectedErr {
t.Fatalf("got %v\n, expected %v", e.Error(), expectedErr)
}
})
}

View File

@@ -7,10 +7,8 @@ package main
import (
"context"
"crypto/tls"
_ "embed"
"fmt"
"net/http"
"os"
"strings"
"time"
@@ -25,8 +23,9 @@ import (
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/transport"
"k8s.io/client-go/rest"
"sigs.k8s.io/controller-runtime/pkg/builder"
"sigs.k8s.io/controller-runtime/pkg/cache"
"sigs.k8s.io/controller-runtime/pkg/client"
@@ -37,6 +36,7 @@ import (
"sigs.k8s.io/controller-runtime/pkg/manager"
"sigs.k8s.io/controller-runtime/pkg/manager/signals"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
"sigs.k8s.io/controller-runtime/pkg/source"
"sigs.k8s.io/yaml"
"tailscale.com/client/tailscale"
"tailscale.com/hostinfo"
@@ -46,7 +46,6 @@ import (
"tailscale.com/types/logger"
"tailscale.com/types/opt"
"tailscale.com/util/dnsname"
"tailscale.com/version"
)
func main() {
@@ -63,7 +62,6 @@ func main() {
clientIDPath = defaultEnv("CLIENT_ID_FILE", "")
clientSecretPath = defaultEnv("CLIENT_SECRET_FILE", "")
image = defaultEnv("PROXY_IMAGE", "tailscale/tailscale:latest")
priorityClassName = defaultEnv("PROXY_PRIORITY_CLASS_NAME", "")
tags = defaultEnv("PROXY_TAGS", "tag:k8s")
shouldRunAuthProxy = defaultBool("AUTH_PROXY", false)
)
@@ -183,33 +181,32 @@ waitOnline:
// the cache that sits a few layers below the builder stuff, which will
// implicitly filter what parts of the world the builder code gets to see at
// all.
nsFilter := cache.ByObject{
Field: client.InNamespace(tsNamespace).AsSelector(),
nsFilter := cache.ObjectSelector{
Field: fields.SelectorFromSet(fields.Set{"metadata.namespace": tsNamespace}),
}
restConfig := config.GetConfigOrDie()
mgr, err := manager.New(restConfig, manager.Options{
Cache: cache.Options{
ByObject: map[client.Object]cache.ByObject{
NewCache: cache.BuilderWithOptions(cache.Options{
SelectorsByObject: map[client.Object]cache.ObjectSelector{
&corev1.Secret{}: nsFilter,
&appsv1.StatefulSet{}: nsFilter,
},
},
}),
})
if err != nil {
startlog.Fatalf("could not create manager: %v", err)
}
sr := &ServiceReconciler{
Client: mgr.GetClient(),
tsClient: tsClient,
defaultTags: strings.Split(tags, ","),
operatorNamespace: tsNamespace,
proxyImage: image,
proxyPriorityClassName: priorityClassName,
logger: zlog.Named("service-reconciler"),
Client: mgr.GetClient(),
tsClient: tsClient,
defaultTags: strings.Split(tags, ","),
operatorNamespace: tsNamespace,
proxyImage: image,
logger: zlog.Named("service-reconciler"),
}
reconcileFilter := handler.EnqueueRequestsFromMapFunc(func(_ context.Context, o client.Object) []reconcile.Request {
reconcileFilter := handler.EnqueueRequestsFromMapFunc(func(o client.Object) []reconcile.Request {
ls := o.GetLabels()
if ls[LabelManaged] != "true" {
return nil
@@ -229,32 +226,18 @@ waitOnline:
err = builder.
ControllerManagedBy(mgr).
For(&corev1.Service{}).
Watches(&appsv1.StatefulSet{}, reconcileFilter).
Watches(&corev1.Secret{}, reconcileFilter).
Watches(&source.Kind{Type: &appsv1.StatefulSet{}}, reconcileFilter).
Watches(&source.Kind{Type: &corev1.Secret{}}, reconcileFilter).
Complete(sr)
if err != nil {
startlog.Fatalf("could not create controller: %v", err)
}
startlog.Infof("Startup complete, operator running, version: %s", version.Long())
startlog.Infof("Startup complete, operator running")
if shouldRunAuthProxy {
cfg, err := restConfig.TransportConfig()
rt, err := rest.TransportFor(restConfig)
if err != nil {
startlog.Fatalf("could not get rest.TransportConfig(): %v", err)
}
// Kubernetes uses SPDY for exec and port-forward, however SPDY is
// incompatible with HTTP/2; so disable HTTP/2 in the proxy.
tr := http.DefaultTransport.(*http.Transport).Clone()
tr.TLSClientConfig, err = transport.TLSConfigFor(cfg)
if err != nil {
startlog.Fatalf("could not get transport.TLSConfigFor(): %v", err)
}
tr.TLSNextProto = make(map[string]func(authority string, c *tls.Conn) http.RoundTripper)
rt, err := transport.HTTPWrappersForConfig(cfg, tr)
if err != nil {
startlog.Fatalf("could not get rest.TransportConfig(): %v", err)
startlog.Fatalf("could not get rest transport: %v", err)
}
go runAuthProxy(s, rt, zlog.Named("auth-proxy").Infof)
}
@@ -279,12 +262,11 @@ const (
// ServiceReconciler is a simple ControllerManagedBy example implementation.
type ServiceReconciler struct {
client.Client
tsClient tsClient
defaultTags []string
operatorNamespace string
proxyImage string
proxyPriorityClassName string
logger *zap.SugaredLogger
tsClient tsClient
defaultTags []string
operatorNamespace string
proxyImage string
logger *zap.SugaredLogger
}
type tsClient interface {
@@ -568,9 +550,6 @@ func (a *ServiceReconciler) getDeviceInfo(ctx context.Context, svc *corev1.Servi
if err != nil {
return "", "", err
}
if sec == nil {
return "", "", nil
}
id = string(sec.Data["device_id"])
if id == "" {
return "", "", nil
@@ -594,7 +573,6 @@ func (a *ServiceReconciler) newAuthKey(ctx context.Context, tags []string) (stri
},
},
}
key, _, err := a.tsClient.CreateKey(ctx, caps)
if err != nil {
return "", err
@@ -639,7 +617,6 @@ func (a *ServiceReconciler) reconcileSTS(ctx context.Context, logger *zap.Sugare
ss.Spec.Template.ObjectMeta.Labels = map[string]string{
"app": string(parentSvc.UID),
}
ss.Spec.Template.Spec.PriorityClassName = a.proxyPriorityClassName
logger.Debugf("reconciling statefulset %s/%s", ss.GetNamespace(), ss.GetName())
return createOrUpdate(ctx, a.Client, a.operatorNamespace, &ss, func(s *appsv1.StatefulSet) { s.Spec = ss.Spec })
}

View File

@@ -14,6 +14,7 @@ import (
"go.uber.org/zap"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
v1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
@@ -64,7 +65,7 @@ func TestLoadBalancerClass(t *testing.T) {
expectEqual(t, fc, expectedSecret(fullName))
expectEqual(t, fc, expectedHeadlessService(shortName))
expectEqual(t, fc, expectedSTS(shortName, fullName, "default-test", ""))
expectEqual(t, fc, expectedSTS(shortName, fullName, "default-test"))
// Normally the Tailscale proxy pod would come up here and write its info
// into the secret. Simulate that, then verify reconcile again and verify
@@ -110,8 +111,6 @@ func TestLoadBalancerClass(t *testing.T) {
mustUpdate(t, fc, "default", "test", func(s *corev1.Service) {
s.Spec.Type = corev1.ServiceTypeClusterIP
s.Spec.LoadBalancerClass = nil
})
mustUpdateStatus(t, fc, "default", "test", func(s *corev1.Service) {
// Fake client doesn't automatically delete the LoadBalancer status when
// changing away from the LoadBalancer type, we have to do
// controller-manager's work by hand.
@@ -187,7 +186,7 @@ func TestAnnotations(t *testing.T) {
expectEqual(t, fc, expectedSecret(fullName))
expectEqual(t, fc, expectedHeadlessService(shortName))
expectEqual(t, fc, expectedSTS(shortName, fullName, "default-test", ""))
expectEqual(t, fc, expectedSTS(shortName, fullName, "default-test"))
want := &corev1.Service{
TypeMeta: metav1.TypeMeta{
Kind: "Service",
@@ -284,7 +283,7 @@ func TestAnnotationIntoLB(t *testing.T) {
expectEqual(t, fc, expectedSecret(fullName))
expectEqual(t, fc, expectedHeadlessService(shortName))
expectEqual(t, fc, expectedSTS(shortName, fullName, "default-test", ""))
expectEqual(t, fc, expectedSTS(shortName, fullName, "default-test"))
// Normally the Tailscale proxy pod would come up here and write its info
// into the secret. Simulate that, since it would have normally happened at
@@ -328,7 +327,7 @@ func TestAnnotationIntoLB(t *testing.T) {
expectReconciled(t, sr, "default", "test")
// None of the proxy machinery should have changed...
expectEqual(t, fc, expectedHeadlessService(shortName))
expectEqual(t, fc, expectedSTS(shortName, fullName, "default-test", ""))
expectEqual(t, fc, expectedSTS(shortName, fullName, "default-test"))
// ... but the service should have a LoadBalancer status.
want = &corev1.Service{
@@ -400,7 +399,7 @@ func TestLBIntoAnnotation(t *testing.T) {
expectEqual(t, fc, expectedSecret(fullName))
expectEqual(t, fc, expectedHeadlessService(shortName))
expectEqual(t, fc, expectedSTS(shortName, fullName, "default-test", ""))
expectEqual(t, fc, expectedSTS(shortName, fullName, "default-test"))
// Normally the Tailscale proxy pod would come up here and write its info
// into the secret. Simulate that, then verify reconcile again and verify
@@ -449,8 +448,6 @@ func TestLBIntoAnnotation(t *testing.T) {
}
s.Spec.Type = corev1.ServiceTypeClusterIP
s.Spec.LoadBalancerClass = nil
})
mustUpdateStatus(t, fc, "default", "test", func(s *corev1.Service) {
// Fake client doesn't automatically delete the LoadBalancer status when
// changing away from the LoadBalancer type, we have to do
// controller-manager's work by hand.
@@ -459,7 +456,7 @@ func TestLBIntoAnnotation(t *testing.T) {
expectReconciled(t, sr, "default", "test")
expectEqual(t, fc, expectedHeadlessService(shortName))
expectEqual(t, fc, expectedSTS(shortName, fullName, "default-test", ""))
expectEqual(t, fc, expectedSTS(shortName, fullName, "default-test"))
want = &corev1.Service{
TypeMeta: metav1.TypeMeta{
@@ -526,7 +523,7 @@ func TestCustomHostname(t *testing.T) {
expectEqual(t, fc, expectedSecret(fullName))
expectEqual(t, fc, expectedHeadlessService(shortName))
expectEqual(t, fc, expectedSTS(shortName, fullName, "reindeer-flotilla", ""))
expectEqual(t, fc, expectedSTS(shortName, fullName, "reindeer-flotilla"))
want := &corev1.Service{
TypeMeta: metav1.TypeMeta{
Kind: "Service",
@@ -585,51 +582,6 @@ func TestCustomHostname(t *testing.T) {
expectEqual(t, fc, want)
}
func TestCustomPriorityClassName(t *testing.T) {
fc := fake.NewFakeClient()
ft := &fakeTSClient{}
zl, err := zap.NewDevelopment()
if err != nil {
t.Fatal(err)
}
sr := &ServiceReconciler{
Client: fc,
tsClient: ft,
defaultTags: []string{"tag:k8s"},
operatorNamespace: "operator-ns",
proxyImage: "tailscale/tailscale",
proxyPriorityClassName: "tailscale-critical",
logger: zl.Sugar(),
}
// Create a service that we should manage, and check that the initial round
// of objects looks right.
mustCreate(t, fc, &corev1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: "test",
Namespace: "default",
// The apiserver is supposed to set the UID, but the fake client
// doesn't. So, set it explicitly because other code later depends
// on it being set.
UID: types.UID("1234-UID"),
Annotations: map[string]string{
"tailscale.com/expose": "true",
"tailscale.com/hostname": "custom-priority-class-name",
},
},
Spec: corev1.ServiceSpec{
ClusterIP: "10.20.30.40",
Type: corev1.ServiceTypeClusterIP,
},
})
expectReconciled(t, sr, "default", "test")
fullName, shortName := findGenName(t, fc, "default", "test")
expectEqual(t, fc, expectedSTS(shortName, fullName, "custom-priority-class-name", "tailscale-critical"))
}
func expectedSecret(name string) *corev1.Secret {
return &corev1.Secret{
TypeMeta: metav1.TypeMeta{
@@ -678,7 +630,7 @@ func expectedHeadlessService(name string) *corev1.Service {
}
}
func expectedSTS(stsName, secretName, hostname, priorityClassName string) *appsv1.StatefulSet {
func expectedSTS(stsName, secretName, hostname string) *appsv1.StatefulSet {
return &appsv1.StatefulSet{
TypeMeta: metav1.TypeMeta{
Kind: "StatefulSet",
@@ -707,7 +659,6 @@ func expectedSTS(stsName, secretName, hostname, priorityClassName string) *appsv
},
Spec: corev1.PodSpec{
ServiceAccountName: "proxies",
PriorityClassName: priorityClassName,
InitContainers: []corev1.Container{
{
Name: "sysctler",
@@ -719,11 +670,11 @@ func expectedSTS(stsName, secretName, hostname, priorityClassName string) *appsv
},
},
},
Containers: []corev1.Container{
Containers: []v1.Container{
{
Name: "tailscale",
Image: "tailscale/tailscale",
Env: []corev1.EnvVar{
Env: []v1.EnvVar{
{Name: "TS_USERSPACE", Value: "false"},
{Name: "TS_AUTH_ONCE", Value: "true"},
{Name: "TS_DEST_IP", Value: "10.20.30.40"},
@@ -781,21 +732,6 @@ func mustUpdate[T any, O ptrObject[T]](t *testing.T, client client.Client, ns, n
}
}
func mustUpdateStatus[T any, O ptrObject[T]](t *testing.T, client client.Client, ns, name string, update func(O)) {
t.Helper()
obj := O(new(T))
if err := client.Get(context.Background(), types.NamespacedName{
Name: name,
Namespace: ns,
}, obj); err != nil {
t.Fatalf("getting %q: %v", name, err)
}
update(obj)
if err := client.Status().Update(context.Background(), obj); err != nil {
t.Fatalf("updating %q: %v", name, err)
}
}
func expectEqual[T any, O ptrObject[T]](t *testing.T, client client.Client, want O) {
t.Helper()
got := O(new(T))
@@ -879,6 +815,7 @@ func (c *fakeTSClient) CreateKey(ctx context.Context, caps tailscale.KeyCapabili
k := &tailscale.Key{
ID: "key",
Created: time.Now(),
Expires: time.Now().Add(24 * time.Hour),
Capabilities: caps,
}
return "secret-authkey", k, nil

View File

@@ -5,7 +5,6 @@ package main
import (
"context"
"crypto/tls"
"fmt"
"log"
"net/http"
@@ -49,7 +48,7 @@ func (h *authProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
//
// It never returns.
func runAuthProxy(s *tsnet.Server, rt http.RoundTripper, logf logger.Logf) {
ln, err := s.Listen("tcp", ":443")
ln, err := s.ListenTLS("tcp", ":443")
if err != nil {
log.Fatalf("could not listen on :443: %v", err)
}
@@ -104,17 +103,7 @@ func runAuthProxy(s *tsnet.Server, rt http.RoundTripper, logf logger.Logf) {
Transport: rt,
},
}
hs := &http.Server{
// Kubernetes uses SPDY for exec and port-forward, however SPDY is
// incompatible with HTTP/2; so disable HTTP/2 in the proxy.
TLSConfig: &tls.Config{
GetCertificate: lc.GetCertificate,
NextProtos: []string{"http/1.1"},
},
TLSNextProto: make(map[string]func(*http.Server, *tls.Conn, http.Handler)),
Handler: ap,
}
if err := hs.ServeTLS(ln, "", ""); err != nil {
if err := http.Serve(ln, ap); err != nil {
log.Fatalf("runAuthProxy: failed to serve %v", err)
}
}

View File

@@ -11,40 +11,35 @@ import (
"os"
"strings"
"github.com/goreleaser/nfpm/v2"
_ "github.com/goreleaser/nfpm/v2/deb"
"github.com/goreleaser/nfpm/v2/files"
_ "github.com/goreleaser/nfpm/v2/rpm"
"github.com/goreleaser/nfpm"
_ "github.com/goreleaser/nfpm/deb"
_ "github.com/goreleaser/nfpm/rpm"
)
// parseFiles parses a comma-separated list of colon-separated pairs
// into files.Contents format.
func parseFiles(s string, typ string) (files.Contents, error) {
// into a map of filePathOnDisk -> filePathInPackage.
func parseFiles(s string) (map[string]string, error) {
ret := map[string]string{}
if len(s) == 0 {
return nil, nil
return ret, nil
}
var contents files.Contents
for _, f := range strings.Split(s, ",") {
fs := strings.Split(f, ":")
if len(fs) != 2 {
return nil, fmt.Errorf("unparseable file field %q", f)
}
contents = append(contents, &files.Content{Type: files.TypeFile, Source: fs[0], Destination: fs[1]})
ret[fs[0]] = fs[1]
}
return contents, nil
return ret, nil
}
func parseEmptyDirs(s string) files.Contents {
func parseEmptyDirs(s string) []string {
// strings.Split("", ",") would return []string{""}, which is not suitable:
// this would create an empty dir record with path "", breaking the package
if s == "" {
return nil
}
var contents files.Contents
for _, d := range strings.Split(s, ",") {
contents = append(contents, &files.Content{Type: files.TypeDir, Destination: d})
}
return contents
return strings.Split(s, ",")
}
func main() {
@@ -53,7 +48,7 @@ func main() {
description := flag.String("description", "The easiest, most secure, cross platform way to use WireGuard + oauth2 + 2FA/SSO", "package description")
goarch := flag.String("arch", "amd64", "GOARCH this package is for")
pkgType := flag.String("type", "deb", "type of package to build (deb or rpm)")
regularFiles := flag.String("files", "", "comma-separated list of files in src:dst form")
files := flag.String("files", "", "comma-separated list of files in src:dst form")
configFiles := flag.String("configs", "", "like --files, but for files marked as user-editable config files")
emptyDirs := flag.String("emptydirs", "", "comma-separated list of empty directories")
version := flag.String("version", "0.0.0", "version of the package")
@@ -65,20 +60,15 @@ func main() {
recommends := flag.String("recommends", "", "comma-separated list of packages this package recommends")
flag.Parse()
filesList, err := parseFiles(*regularFiles, files.TypeFile)
filesMap, err := parseFiles(*files)
if err != nil {
log.Fatalf("Parsing --files: %v", err)
}
configsList, err := parseFiles(*configFiles, files.TypeConfig)
configsMap, err := parseFiles(*configFiles)
if err != nil {
log.Fatalf("Parsing --configs: %v", err)
}
emptyDirList := parseEmptyDirs(*emptyDirs)
contents := append(filesList, append(configsList, emptyDirList...)...)
contents, err = files.PrepareForPackager(contents, 0, *pkgType, false)
if err != nil {
log.Fatalf("Building package contents: %v", err)
}
info := nfpm.WithDefaults(&nfpm.Info{
Name: *name,
Arch: *goarch,
@@ -89,7 +79,9 @@ func main() {
Homepage: "https://www.tailscale.com",
License: "MIT",
Overridables: nfpm.Overridables{
Contents: contents,
EmptyFolders: emptyDirList,
Files: filesMap,
ConfigFiles: configsMap,
Scripts: nfpm.Scripts{
PostInstall: *postinst,
PreRemove: *prerm,

View File

@@ -45,7 +45,6 @@ import (
"golang.org/x/exp/slices"
"tailscale.com/types/logid"
"tailscale.com/types/netlogtype"
"tailscale.com/util/cmpx"
"tailscale.com/util/must"
)
@@ -152,10 +151,10 @@ func printMessage(msg message) {
if len(traffic) == 0 {
return
}
slices.SortFunc(traffic, func(x, y netlogtype.ConnectionCounts) int {
slices.SortFunc(traffic, func(x, y netlogtype.ConnectionCounts) bool {
nx := x.TxPackets + x.TxBytes + x.RxPackets + x.RxBytes
ny := y.TxPackets + y.TxBytes + y.RxPackets + y.RxBytes
return cmpx.Compare(ny, nx)
return nx > ny
})
var sum netlogtype.Counts
for _, cc := range traffic {

View File

@@ -272,7 +272,7 @@ func (p *proxy) serve(sessionID int64, c net.Conn) error {
}
if buf[0] != 'S' {
p.errors.Add("upstream-bad-protocol", 1)
return fmt.Errorf("upstream didn't acknowledge start-ssl, said %q", buf[0])
return fmt.Errorf("upstream didn't acknowldge start-ssl, said %q", buf[0])
}
tlsConf := &tls.Config{
ServerName: p.upstreamHost,

View File

@@ -18,39 +18,25 @@ import (
"golang.org/x/net/dns/dnsmessage"
"inet.af/tcpproxy"
"tailscale.com/client/tailscale"
"tailscale.com/hostinfo"
"tailscale.com/net/netutil"
"tailscale.com/tsnet"
"tailscale.com/types/nettype"
"tailscale.com/util/clientmetric"
)
var (
ports = flag.String("ports", "443", "comma-separated list of ports to proxy")
wgPort = flag.Int("wg-listen-port", 0, "UDP port to listen on for WireGuard and peer-to-peer traffic; 0 means automatically select")
promoteHTTPS = flag.Bool("promote-https", true, "promote HTTP to HTTPS")
)
var tsMBox = dnsmessage.MustNewName("support.tailscale.com.")
var (
numSessions = clientmetric.NewCounter("sniproxy_sessions")
numBadAddrPort = clientmetric.NewCounter("sniproxy_bad_addrport")
dnsResponses = clientmetric.NewCounter("sniproxy_dns_responses")
dnsFailures = clientmetric.NewCounter("sniproxy_dns_failed")
httpPromoted = clientmetric.NewCounter("sniproxy_http_promoted")
)
func main() {
flag.Parse()
if *ports == "" {
log.Fatal("no ports")
}
hostinfo.SetApp("sniproxy")
var s server
s.ts.Port = uint16(*wgPort)
defer s.ts.Close()
lc, err := s.ts.LocalClient()
@@ -118,7 +104,6 @@ func (s *server) serveDNSConn(c nettype.ConnPacketConn) {
n, err := c.Read(buf)
if err != nil {
log.Printf("c.Read failed: %v\n ", err)
dnsFailures.Add(1)
return
}
@@ -126,25 +111,20 @@ func (s *server) serveDNSConn(c nettype.ConnPacketConn) {
err = msg.Unpack(buf[:n])
if err != nil {
log.Printf("dnsmessage unpack failed: %v\n ", err)
dnsFailures.Add(1)
return
}
buf, err = s.dnsResponse(&msg)
if err != nil {
log.Printf("s.dnsResponse failed: %v\n", err)
dnsFailures.Add(1)
return
}
_, err = c.Write(buf)
if err != nil {
log.Printf("c.Write failed: %v\n", err)
dnsFailures.Add(1)
return
}
dnsResponses.Add(1)
}
func (s *server) serveConn(c net.Conn) {
@@ -152,7 +132,6 @@ func (s *server) serveConn(c net.Conn) {
_, port, err := net.SplitHostPort(addrPortStr)
if err != nil {
log.Printf("bogus addrPort %q", addrPortStr)
numBadAddrPort.Add(1)
c.Close()
return
}
@@ -165,7 +144,6 @@ func (s *server) serveConn(c net.Conn) {
return netutil.NewOneConnListener(c, nil), nil
}
p.AddSNIRouteFunc(addrPortStr, func(ctx context.Context, sniName string) (t tcpproxy.Target, ok bool) {
numSessions.Add(1)
return &tcpproxy.DialProxy{
Addr: net.JoinHostPort(sniName, port),
DialContext: dialer.DialContext,
@@ -235,7 +213,6 @@ func (s *server) dnsResponse(req *dnsmessage.Message) (buf []byte, err error) {
func (s *server) promoteHTTPS(ln net.Listener) {
err := http.Serve(ln, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
httpPromoted.Add(1)
http.Redirect(w, r, "https://"+r.Host+r.RequestURI, http.StatusFound)
}))
log.Fatalf("promoteHTTPS http.Serve: %v", err)

View File

@@ -6,15 +6,33 @@
package cli
import (
"tailscale.com/util/winutil/authenticode"
"unsafe"
"golang.org/x/sys/windows"
)
func init() {
verifyAuthenticode = verifyTailscale
verifyAuthenticode = verifyAuthenticodeWindows
}
const certSubjectTailscale = "Tailscale Inc."
func verifyTailscale(path string) error {
return authenticode.Verify(path, certSubjectTailscale)
func verifyAuthenticodeWindows(path string) error {
path16, err := windows.UTF16PtrFromString(path)
if err != nil {
return err
}
data := &windows.WinTrustData{
Size: uint32(unsafe.Sizeof(windows.WinTrustData{})),
UIChoice: windows.WTD_UI_NONE,
RevocationChecks: windows.WTD_REVOKE_WHOLECHAIN, // Full revocation checking, as this is called with network connectivity.
UnionChoice: windows.WTD_CHOICE_FILE,
StateAction: windows.WTD_STATEACTION_VERIFY,
FileOrCatalogOrBlobOrSgnrOrCert: unsafe.Pointer(&windows.WinTrustFileInfo{
Size: uint32(unsafe.Sizeof(windows.WinTrustFileInfo{})),
FilePath: path16,
}),
}
err = windows.WinVerifyTrustEx(windows.InvalidHWND, &windows.WINTRUST_ACTION_GENERIC_VERIFY_V2, data)
data.StateAction = windows.WTD_STATEACTION_CLOSE
windows.WinVerifyTrustEx(windows.InvalidHWND, &windows.WINTRUST_ACTION_GENERIC_VERIFY_V2, data)
return err
}

View File

@@ -120,8 +120,6 @@ change in the future.
pingCmd,
ncCmd,
sshCmd,
funnelCmd,
serveCmd,
versionCmd,
webCmd,
fileCmd,
@@ -129,12 +127,16 @@ change in the future.
certCmd,
netlockCmd,
licensesCmd,
exitNodeCmd,
},
FlagSet: rootfs,
Exec: func(context.Context, []string) error { return flag.ErrHelp },
UsageFunc: usageFunc,
}
for _, c := range rootCmd.Subcommands {
if c.UsageFunc == nil {
c.UsageFunc = usageFunc
}
}
if envknob.UseWIPCode() {
rootCmd.Subcommands = append(rootCmd.Subcommands,
idTokenCmd,
@@ -145,6 +147,10 @@ change in the future.
switch {
case slices.Contains(args, "debug"):
rootCmd.Subcommands = append(rootCmd.Subcommands, debugCmd)
case slices.Contains(args, "funnel"):
rootCmd.Subcommands = append(rootCmd.Subcommands, funnelCmd)
case slices.Contains(args, "serve"):
rootCmd.Subcommands = append(rootCmd.Subcommands, serveCmd)
case slices.Contains(args, "update"):
rootCmd.Subcommands = append(rootCmd.Subcommands, updateCmd)
}
@@ -152,12 +158,6 @@ change in the future.
rootCmd.Subcommands = append(rootCmd.Subcommands, configureHostCmd)
}
for _, c := range rootCmd.Subcommands {
if c.UsageFunc == nil {
c.UsageFunc = usageFunc
}
}
if err := rootCmd.Parse(args); err != nil {
if errors.Is(err, flag.ErrHelp) {
return nil

View File

@@ -22,7 +22,6 @@ import (
"tailscale.com/tstest"
"tailscale.com/types/persist"
"tailscale.com/types/preftype"
"tailscale.com/util/cmpx"
"tailscale.com/version/distro"
)
@@ -622,16 +621,9 @@ func TestPrefsFromUpArgs(t *testing.T) {
{
name: "error_long_hostname",
args: upArgsT{
hostname: strings.Repeat(strings.Repeat("a", 63)+".", 4),
hostname: strings.Repeat("a", 300),
},
wantErr: `"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" is too long to be a DNS name`,
},
{
name: "error_long_label",
args: upArgsT{
hostname: strings.Repeat("a", 64) + ".example.com",
},
wantErr: `"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" is not a valid DNS label`,
wantErr: `hostname too long: 300 bytes (max 256)`,
},
{
name: "error_linux_netfilter_empty",
@@ -720,7 +712,10 @@ func TestPrefsFromUpArgs(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var warnBuf tstest.MemLogger
goos := cmpx.Or(tt.goos, "linux")
goos := tt.goos
if goos == "" {
goos = "linux"
}
st := tt.st
if st == nil {
st = new(ipnstate.Status)

View File

@@ -66,7 +66,7 @@ func isSystemdSystem() bool {
return false
}
switch distro.Get() {
case distro.QNAP, distro.Gokrazy, distro.Synology, distro.Unraid:
case distro.QNAP, distro.Gokrazy, distro.Synology:
return false
}
_, err := exec.LookPath("systemctl")

View File

@@ -1,248 +0,0 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package cli
import (
"context"
"errors"
"flag"
"fmt"
"os"
"strings"
"text/tabwriter"
"github.com/peterbourgon/ff/v3/ffcli"
"golang.org/x/exp/maps"
"golang.org/x/exp/slices"
"tailscale.com/ipn/ipnstate"
"tailscale.com/tailcfg"
"tailscale.com/util/cmpx"
)
var exitNodeCmd = &ffcli.Command{
Name: "exit-node",
ShortUsage: "exit-node [flags]",
Subcommands: []*ffcli.Command{
{
Name: "list",
ShortUsage: "exit-node list [flags]",
ShortHelp: "Show exit nodes",
Exec: runExitNodeList,
FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("list")
fs.StringVar(&exitNodeArgs.filter, "filter", "", "filter exit nodes by country")
return fs
})(),
},
},
Exec: func(context.Context, []string) error {
return errors.New("exit-node subcommand required; run 'tailscale exit-node -h' for details")
},
}
var exitNodeArgs struct {
filter string
}
// runExitNodeList returns a formatted list of exit nodes for a tailnet.
// If the exit node has location and priority data, only the highest
// priority node for each city location is shown to the user.
// If the country location has more than one city, an 'Any' city
// is returned for the country, which lists the highest priority
// node in that country.
// For countries without location data, each exit node is displayed.
func runExitNodeList(ctx context.Context, args []string) error {
if len(args) > 0 {
return errors.New("unexpected non-flag arguments to 'tailscale exit-node list'")
}
getStatus := localClient.Status
st, err := getStatus(ctx)
if err != nil {
return fixTailscaledConnectError(err)
}
var peers []*ipnstate.PeerStatus
for _, ps := range st.Peer {
if !ps.ExitNodeOption {
// We only show location based exit nodes.
continue
}
peers = append(peers, ps)
}
if len(peers) == 0 {
return errors.New("no exit nodes found")
}
filteredPeers := filterFormatAndSortExitNodes(peers, exitNodeArgs.filter)
if len(filteredPeers.Countries) == 0 && exitNodeArgs.filter != "" {
return fmt.Errorf("no exit nodes found for %q", exitNodeArgs.filter)
}
w := tabwriter.NewWriter(os.Stdout, 10, 5, 5, ' ', 0)
defer w.Flush()
fmt.Fprintf(w, "\n %s\t%s\t%s\t%s\t%s\t", "IP", "HOSTNAME", "COUNTRY", "CITY", "STATUS")
for _, country := range filteredPeers.Countries {
for _, city := range country.Cities {
for _, peer := range city.Peers {
fmt.Fprintf(w, "\n %s\t%s\t%s\t%s\t%s\t", peer.TailscaleIPs[0], strings.Trim(peer.DNSName, "."), country.Name, city.Name, peerStatus(peer))
}
}
}
fmt.Fprintln(w)
fmt.Fprintln(w)
fmt.Fprintln(w, "# To use an exit node, use `tailscale set --exit-node=` followed by the hostname or IP")
return nil
}
// peerStatus returns a string representing the current state of
// a peer. If there is no notable state, a - is returned.
func peerStatus(peer *ipnstate.PeerStatus) string {
if !peer.Active {
if peer.ExitNode {
return "selected but offline"
}
if !peer.Online {
return "offline"
}
}
if peer.ExitNode {
return "selected"
}
return "-"
}
type filteredExitNodes struct {
Countries []*filteredCountry
}
type filteredCountry struct {
Name string
Cities []*filteredCity
}
type filteredCity struct {
Name string
Peers []*ipnstate.PeerStatus
}
const noLocationData = "-"
// filterFormatAndSortExitNodes filters and sorts exit nodes into
// alphabetical order, by country, city and then by priority if
// present.
// If an exit node has location data, and the country has more than
// once city, an `Any` city is added to the country that contains the
// highest priority exit node within that country.
// For exit nodes without location data, their country fields are
// defined as '-' to indicate that the data is not available.
func filterFormatAndSortExitNodes(peers []*ipnstate.PeerStatus, filterBy string) filteredExitNodes {
countries := make(map[string]*filteredCountry)
cities := make(map[string]*filteredCity)
for _, ps := range peers {
if ps.Location == nil {
ps.Location = &tailcfg.Location{
Country: noLocationData,
CountryCode: noLocationData,
City: noLocationData,
CityCode: noLocationData,
}
}
if filterBy != "" && ps.Location.Country != filterBy {
continue
}
co, coOK := countries[ps.Location.CountryCode]
if !coOK {
co = &filteredCountry{
Name: ps.Location.Country,
}
countries[ps.Location.CountryCode] = co
}
ci, ciOK := cities[ps.Location.CityCode]
if !ciOK {
ci = &filteredCity{
Name: ps.Location.City,
}
cities[ps.Location.CityCode] = ci
co.Cities = append(co.Cities, ci)
}
ci.Peers = append(ci.Peers, ps)
}
filteredExitNodes := filteredExitNodes{
Countries: maps.Values(countries),
}
for _, country := range filteredExitNodes.Countries {
if country.Name == noLocationData {
// Countries without location data should not
// be filtered further.
continue
}
var countryANYPeer []*ipnstate.PeerStatus
for _, city := range country.Cities {
sortPeersByPriority(city.Peers)
countryANYPeer = append(countryANYPeer, city.Peers...)
var reducedCityPeers []*ipnstate.PeerStatus
for i, peer := range city.Peers {
if i == 0 || peer.ExitNode {
// We only return the highest priority peer and any peer that
// is currently the active exit node.
reducedCityPeers = append(reducedCityPeers, peer)
}
}
city.Peers = reducedCityPeers
}
sortByCityName(country.Cities)
sortPeersByPriority(countryANYPeer)
if len(country.Cities) > 1 {
// For countries with more than one city, we want to return the
// option of the best peer for that country.
country.Cities = append([]*filteredCity{
{
Name: "Any",
Peers: []*ipnstate.PeerStatus{countryANYPeer[0]},
},
}, country.Cities...)
}
}
sortByCountryName(filteredExitNodes.Countries)
return filteredExitNodes
}
// sortPeersByPriority sorts a slice of PeerStatus
// by location.Priority, in order of highest priority.
func sortPeersByPriority(peers []*ipnstate.PeerStatus) {
slices.SortStableFunc(peers, func(a, b *ipnstate.PeerStatus) int {
return cmpx.Compare(b.Location.Priority, a.Location.Priority)
})
}
// sortByCityName sorts a slice of filteredCity alphabetically
// by name. The '-' used to indicate no location data will always
// be sorted to the front of the slice.
func sortByCityName(cities []*filteredCity) {
slices.SortStableFunc(cities, func(a, b *filteredCity) int { return strings.Compare(a.Name, b.Name) })
}
// sortByCountryName sorts a slice of filteredCountry alphabetically
// by name. The '-' used to indicate no location data will always
// be sorted to the front of the slice.
func sortByCountryName(countries []*filteredCountry) {
slices.SortStableFunc(countries, func(a, b *filteredCountry) int { return strings.Compare(a.Name, b.Name) })
}

View File

@@ -1,308 +0,0 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package cli
import (
"testing"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"tailscale.com/ipn/ipnstate"
"tailscale.com/tailcfg"
"tailscale.com/types/key"
)
func TestFilterFormatAndSortExitNodes(t *testing.T) {
t.Run("without filter", func(t *testing.T) {
ps := []*ipnstate.PeerStatus{
{
HostName: "everest-1",
Location: &tailcfg.Location{
Country: "Everest",
CountryCode: "evr",
City: "Hillary",
CityCode: "hil",
Priority: 100,
},
},
{
HostName: "lhotse-1",
Location: &tailcfg.Location{
Country: "Lhotse",
CountryCode: "lho",
City: "Fritz",
CityCode: "fri",
Priority: 200,
},
},
{
HostName: "lhotse-2",
Location: &tailcfg.Location{
Country: "Lhotse",
CountryCode: "lho",
City: "Fritz",
CityCode: "fri",
Priority: 100,
},
},
{
HostName: "nuptse-1",
Location: &tailcfg.Location{
Country: "Nuptse",
CountryCode: "nup",
City: "Walmsley",
CityCode: "wal",
Priority: 200,
},
},
{
HostName: "nuptse-2",
Location: &tailcfg.Location{
Country: "Nuptse",
CountryCode: "nup",
City: "Bonington",
CityCode: "bon",
Priority: 10,
},
},
{
HostName: "Makalu",
},
}
want := filteredExitNodes{
Countries: []*filteredCountry{
{
Name: noLocationData,
Cities: []*filteredCity{
{
Name: noLocationData,
Peers: []*ipnstate.PeerStatus{
ps[5],
},
},
},
},
{
Name: "Everest",
Cities: []*filteredCity{
{
Name: "Hillary",
Peers: []*ipnstate.PeerStatus{
ps[0],
},
},
},
},
{
Name: "Lhotse",
Cities: []*filteredCity{
{
Name: "Fritz",
Peers: []*ipnstate.PeerStatus{
ps[1],
},
},
},
},
{
Name: "Nuptse",
Cities: []*filteredCity{
{
Name: "Any",
Peers: []*ipnstate.PeerStatus{
ps[3],
},
},
{
Name: "Bonington",
Peers: []*ipnstate.PeerStatus{
ps[4],
},
},
{
Name: "Walmsley",
Peers: []*ipnstate.PeerStatus{
ps[3],
},
},
},
},
},
}
result := filterFormatAndSortExitNodes(ps, "")
if res := cmp.Diff(result.Countries, want.Countries, cmpopts.IgnoreUnexported(key.NodePublic{})); res != "" {
t.Fatalf(res)
}
})
t.Run("with country filter", func(t *testing.T) {
ps := []*ipnstate.PeerStatus{
{
HostName: "baker-1",
Location: &tailcfg.Location{
Country: "Pacific",
CountryCode: "pst",
City: "Baker",
CityCode: "col",
Priority: 100,
},
},
{
HostName: "hood-1",
Location: &tailcfg.Location{
Country: "Pacific",
CountryCode: "pst",
City: "Hood",
CityCode: "hoo",
Priority: 500,
},
},
{
HostName: "rainier-1",
Location: &tailcfg.Location{
Country: "Pacific",
CountryCode: "pst",
City: "Rainier",
CityCode: "rai",
Priority: 100,
},
},
{
HostName: "rainier-2",
Location: &tailcfg.Location{
Country: "Pacific",
CountryCode: "pst",
City: "Rainier",
CityCode: "rai",
Priority: 10,
},
},
{
HostName: "mitchell-1",
Location: &tailcfg.Location{
Country: "Atlantic",
CountryCode: "atl",
City: "Mitchell",
CityCode: "mit",
Priority: 200,
},
},
}
want := filteredExitNodes{
Countries: []*filteredCountry{
{
Name: "Pacific",
Cities: []*filteredCity{
{
Name: "Any",
Peers: []*ipnstate.PeerStatus{
ps[1],
},
},
{
Name: "Baker",
Peers: []*ipnstate.PeerStatus{
ps[0],
},
},
{
Name: "Hood",
Peers: []*ipnstate.PeerStatus{
ps[1],
},
},
{
Name: "Rainier",
Peers: []*ipnstate.PeerStatus{
ps[2],
},
},
},
},
},
}
result := filterFormatAndSortExitNodes(ps, "Pacific")
if res := cmp.Diff(result.Countries, want.Countries, cmpopts.IgnoreUnexported(key.NodePublic{})); res != "" {
t.Fatalf(res)
}
})
}
func TestSortPeersByPriority(t *testing.T) {
ps := []*ipnstate.PeerStatus{
{
Location: &tailcfg.Location{
Priority: 100,
},
},
{
Location: &tailcfg.Location{
Priority: 200,
},
},
{
Location: &tailcfg.Location{
Priority: 300,
},
},
}
sortPeersByPriority(ps)
if ps[0].Location.Priority != 300 {
t.Fatalf("sortPeersByPriority did not order PeerStatus with highest priority as index 0, got %v, want %v", ps[0].Location.Priority, 300)
}
}
func TestSortByCountryName(t *testing.T) {
fc := []*filteredCountry{
{
Name: "Albania",
},
{
Name: "Sweden",
},
{
Name: "Zimbabwe",
},
{
Name: noLocationData,
},
}
sortByCountryName(fc)
if fc[0].Name != noLocationData {
t.Fatalf("sortByCountryName did not order countries by alphabetical order, got %v, want %v", fc[0].Name, noLocationData)
}
}
func TestSortByCityName(t *testing.T) {
fc := []*filteredCity{
{
Name: "Kingston",
},
{
Name: "Goteborg",
},
{
Name: "Squamish",
},
{
Name: noLocationData,
},
}
sortByCityName(fc)
if fc[0].Name != noLocationData {
t.Fatalf("sortByCityName did not order cities by alphabetical order, got %v, want %v", fc[0].Name, noLocationData)
}
}

View File

@@ -29,11 +29,11 @@ var funnelCmd = newFunnelCommand(&serveEnv{lc: &localClient})
func newFunnelCommand(e *serveEnv) *ffcli.Command {
return &ffcli.Command{
Name: "funnel",
ShortHelp: "Turn on/off Funnel service",
ShortUsage: strings.Join([]string{
"funnel <serve-port> {on|off}",
"funnel status [--json]",
}, "\n "),
ShortHelp: "[ALPHA] turn Tailscale Funnel on or off",
ShortUsage: strings.TrimSpace(`
funnel <serve-port> {on|off}
funnel status [--json]
`),
LongHelp: strings.Join([]string{
"Funnel allows you to publish a 'tailscale serve'",
"server publicly, open to the entire internet.",

View File

@@ -19,7 +19,6 @@ import (
"tailscale.com/envknob"
"tailscale.com/ipn"
"tailscale.com/net/netcheck"
"tailscale.com/net/netmon"
"tailscale.com/net/portmapper"
"tailscale.com/tailcfg"
"tailscale.com/types/logger"
@@ -46,15 +45,9 @@ var netcheckArgs struct {
}
func runNetcheck(ctx context.Context, args []string) error {
logf := logger.WithPrefix(log.Printf, "portmap: ")
netMon, err := netmon.New(logf)
if err != nil {
return err
}
c := &netcheck.Client{
UDPBindAddr: envknob.String("TS_DEBUG_NETCHECK_UDP_BIND"),
PortMapper: portmapper.NewClient(logf, netMon, nil, nil),
UseDNSCache: false, // always resolve, don't cache
PortMapper: portmapper.NewClient(logger.WithPrefix(log.Printf, "portmap: "), nil, nil),
}
if netcheckArgs.verbose {
c.Logf = logger.WithPrefix(log.Printf, "netcheck: ")
@@ -103,6 +96,7 @@ func printReport(dm *tailcfg.DERPMap, report *netcheck.Report) error {
var err error
switch netcheckArgs.format {
case "":
break
case "json":
j, err = json.MarshalIndent(report, "", "\t")
case "json-line":

View File

@@ -23,7 +23,6 @@ import (
"tailscale.com/ipn/ipnstate"
"tailscale.com/tka"
"tailscale.com/types/key"
"tailscale.com/types/tkatype"
)
var netlockCmd = &ffcli.Command{
@@ -41,18 +40,9 @@ var netlockCmd = &ffcli.Command{
nlDisablementKDFCmd,
nlLogCmd,
nlLocalDisableCmd,
nlRevokeKeysCmd,
nlTskeyWrapCmd,
},
Exec: runNetworkLockNoSubcommand,
}
func runNetworkLockNoSubcommand(ctx context.Context, args []string) error {
// Detect & handle the deprecated command 'lock tskey-wrap'.
if len(args) >= 2 && args[0] == "tskey-wrap" {
return runTskeyWrapCmd(ctx, args[1:])
}
return runNetworkLockStatus(ctx, args)
Exec: runNetworkLockStatus,
}
var nlInitArgs struct {
@@ -437,19 +427,13 @@ func runNetworkLockModify(ctx context.Context, addArgs, removeArgs []string) err
var nlSignCmd = &ffcli.Command{
Name: "sign",
ShortUsage: "sign <node-key> [<rotation-key>] or sign <auth-key>",
ShortHelp: "Signs a node or pre-approved auth key",
LongHelp: `Either:
- signs a node key and transmits the signature to the coordination server, or
- signs a pre-approved auth key, printing it in a form that can be used to bring up nodes under tailnet lock`,
Exec: runNetworkLockSign,
ShortUsage: "sign <node-key> [<rotation-key>]",
ShortHelp: "Signs a node key and transmits the signature to the coordination server",
LongHelp: "Signs a node key and transmits the signature to the coordination server",
Exec: runNetworkLockSign,
}
func runNetworkLockSign(ctx context.Context, args []string) error {
if len(args) > 0 && strings.HasPrefix(args[0], "tskey-auth-") {
return runTskeyWrapCmd(ctx, args)
}
var (
nodeKey key.NodePublic
rotationKey key.NLPublic
@@ -467,16 +451,7 @@ func runNetworkLockSign(ctx context.Context, args []string) error {
}
}
err := localClient.NetworkLockSign(ctx, nodeKey, []byte(rotationKey.Verifier()))
// Provide a better help message for when someone clicks through the signing flow
// on the wrong device.
if err != nil && strings.Contains(err.Error(), "this node is not trusted by network lock") {
fmt.Fprintln(os.Stderr, "Error: Signing is not available on this device because it does not have a trusted tailnet lock key.")
fmt.Fprintln(os.Stderr)
fmt.Fprintln(os.Stderr, "Try again on a signing device instead. Tailnet admins can see signing devices on the admin panel.")
fmt.Fprintln(os.Stderr)
}
return err
return localClient.NetworkLockSign(ctx, nodeKey, []byte(rotationKey.Verifier()))
}
var nlDisableCmd = &ffcli.Command{
@@ -661,6 +636,14 @@ func runNetworkLockLog(ctx context.Context, args []string) error {
return nil
}
var nlTskeyWrapCmd = &ffcli.Command{
Name: "tskey-wrap",
ShortUsage: "tskey-wrap <tailscale pre-auth key>",
ShortHelp: "Modifies a pre-auth key from the admin panel to work with tailnet lock",
LongHelp: "Modifies a pre-auth key from the admin panel to work with tailnet lock",
Exec: runTskeyWrapCmd,
}
func runTskeyWrapCmd(ctx context.Context, args []string) error {
if len(args) != 1 {
return errors.New("usage: lock tskey-wrap <tailscale pre-auth key>")
@@ -674,25 +657,21 @@ func runTskeyWrapCmd(ctx context.Context, args []string) error {
return fixTailscaledConnectError(err)
}
return wrapAuthKey(ctx, args[0], st)
}
func wrapAuthKey(ctx context.Context, keyStr string, status *ipnstate.Status) error {
// Generate a separate tailnet-lock key just for the credential signature.
// We use the free-form meta strings to mark a little bit of metadata about this
// key.
priv := key.NewNLPrivate()
m := map[string]string{
"purpose": "pre-auth key",
"wrapper_stableid": string(status.Self.ID),
"wrapper_stableid": string(st.Self.ID),
"wrapper_createtime": fmt.Sprint(time.Now().Unix()),
}
if strings.HasPrefix(keyStr, "tskey-auth-") && strings.Index(keyStr[len("tskey-auth-"):], "-") > 0 {
if strings.HasPrefix(args[0], "tskey-auth-") && strings.Index(args[0][len("tskey-auth-"):], "-") > 0 {
// We don't want to accidentally embed the nonce part of the authkey in
// the event the format changes. As such, we make sure its in the format we
// expect (tskey-auth-<stableID, inc CNTRL suffix>-nonce) before we parse
// out and embed the stableID.
s := strings.TrimPrefix(keyStr, "tskey-auth-")
s := strings.TrimPrefix(args[0], "tskey-auth-")
m["authkey_stableid"] = s[:strings.Index(s, "-")]
}
k := tka.Key{
@@ -702,7 +681,7 @@ func wrapAuthKey(ctx context.Context, keyStr string, status *ipnstate.Status) er
Meta: m,
}
wrapped, err := localClient.NetworkLockWrapPreauthKey(ctx, keyStr, priv)
wrapped, err := localClient.NetworkLockWrapPreauthKey(ctx, args[0], priv)
if err != nil {
return fmt.Errorf("wrapping failed: %w", err)
}
@@ -713,114 +692,3 @@ func wrapAuthKey(ctx context.Context, keyStr string, status *ipnstate.Status) er
fmt.Println(wrapped)
return nil
}
var nlRevokeKeysArgs struct {
cosign bool
finish bool
forkFrom string
}
var nlRevokeKeysCmd = &ffcli.Command{
Name: "revoke-keys",
ShortUsage: "revoke-keys <tailnet-lock-key>...\n revoke-keys [--cosign] [--finish] <recovery-blob>",
ShortHelp: "Revoke compromised tailnet-lock keys",
LongHelp: `Retroactively revoke the specified tailnet lock keys (tlpub:abc).
Revoked keys are prevented from being used in the future. Any nodes previously signed
by revoked keys lose their authorization and must be signed again.
Revocation is a multi-step process that requires several signing nodes to ` + "`--cosign`" + ` the revocation. Use ` + "`tailscale lock remove`" + ` instead if the key has not been compromised.
1. To start, run ` + "`tailscale revoke-keys <tlpub-keys>`" + ` with the tailnet lock keys to revoke.
2. Re-run the ` + "`--cosign`" + ` command output by ` + "`revoke-keys`" + ` on other signing nodes. Use the
most recent command output on the next signing node in sequence.
3. Once the number of ` + "`--cosign`" + `s is greater than the number of keys being revoked,
run the command one final time with ` + "`--finish`" + ` instead of ` + "`--cosign`" + `.`,
Exec: runNetworkLockRevokeKeys,
FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("lock revoke-keys")
fs.BoolVar(&nlRevokeKeysArgs.cosign, "cosign", false, "continue generating the recovery using the tailnet lock key on this device and the provided recovery blob")
fs.BoolVar(&nlRevokeKeysArgs.finish, "finish", false, "finish the recovery process by transmitting the revocation")
fs.StringVar(&nlRevokeKeysArgs.forkFrom, "fork-from", "", "parent AUM hash to rewrite from (advanced users only)")
return fs
})(),
}
func runNetworkLockRevokeKeys(ctx context.Context, args []string) error {
// First step in the process
if !nlRevokeKeysArgs.cosign && !nlRevokeKeysArgs.finish {
removeKeys, _, err := parseNLArgs(args, true, false)
if err != nil {
return err
}
keyIDs := make([]tkatype.KeyID, len(removeKeys))
for i, k := range removeKeys {
keyIDs[i], err = k.ID()
if err != nil {
return fmt.Errorf("generating keyID: %v", err)
}
}
var forkFrom tka.AUMHash
if nlRevokeKeysArgs.forkFrom != "" {
if len(nlRevokeKeysArgs.forkFrom) == (len(forkFrom) * 2) {
// Hex-encoded: like the output of the lock log command.
b, err := hex.DecodeString(nlRevokeKeysArgs.forkFrom)
if err != nil {
return fmt.Errorf("invalid fork-from hash: %v", err)
}
copy(forkFrom[:], b)
} else {
if err := forkFrom.UnmarshalText([]byte(nlRevokeKeysArgs.forkFrom)); err != nil {
return fmt.Errorf("invalid fork-from hash: %v", err)
}
}
}
aumBytes, err := localClient.NetworkLockGenRecoveryAUM(ctx, keyIDs, forkFrom)
if err != nil {
return fmt.Errorf("generation of recovery AUM failed: %w", err)
}
fmt.Printf(`Run the following command on another machine with a trusted tailnet lock key:
%s lock recover-compromised-key --cosign %X
`, os.Args[0], aumBytes)
return nil
}
// If we got this far, we need to co-sign the AUM and/or transmit it for distribution.
b, err := hex.DecodeString(args[0])
if err != nil {
return fmt.Errorf("parsing hex: %v", err)
}
var recoveryAUM tka.AUM
if err := recoveryAUM.Unserialize(b); err != nil {
return fmt.Errorf("decoding recovery AUM: %v", err)
}
if nlRevokeKeysArgs.cosign {
aumBytes, err := localClient.NetworkLockCosignRecoveryAUM(ctx, recoveryAUM)
if err != nil {
return fmt.Errorf("co-signing recovery AUM failed: %w", err)
}
fmt.Printf(`Co-signing completed successfully.
To accumulate an additional signature, run the following command on another machine with a trusted tailnet lock key:
%s lock recover-compromised-key --cosign %X
Alternatively if you are done with co-signing, complete recovery by running the following command:
%s lock recover-compromised-key --finish %X
`, os.Args[0], aumBytes, os.Args[0], aumBytes)
}
if nlRevokeKeysArgs.finish {
if err := localClient.NetworkLockSubmitRecoveryAUM(ctx, recoveryAUM); err != nil {
return fmt.Errorf("submitting recovery AUM failed: %w", err)
}
fmt.Println("Recovery completed.")
}
return nil
}

View File

@@ -51,7 +51,7 @@ relay node.
fs.BoolVar(&pingArgs.tsmp, "tsmp", false, "do a TSMP-level ping (through WireGuard, but not either host OS stack)")
fs.BoolVar(&pingArgs.icmp, "icmp", false, "do a ICMP-level ping (through WireGuard, but not the local host OS stack)")
fs.BoolVar(&pingArgs.peerAPI, "peerapi", false, "try hitting the peer's peerapi HTTP server")
fs.IntVar(&pingArgs.num, "c", 10, "max number of pings to send. 0 for infinity.")
fs.IntVar(&pingArgs.num, "c", 10, "max number of pings to send")
fs.DurationVar(&pingArgs.timeout, "timeout", 5*time.Second, "timeout before giving up on a ping")
return fs
})(),

View File

@@ -16,7 +16,6 @@ import (
"path"
"path/filepath"
"reflect"
"runtime"
"sort"
"strconv"
"strings"
@@ -24,7 +23,6 @@ import (
"github.com/peterbourgon/ff/v3/ffcli"
"tailscale.com/ipn"
"tailscale.com/ipn/ipnstate"
"tailscale.com/tailcfg"
"tailscale.com/util/mak"
"tailscale.com/version"
)
@@ -35,21 +33,19 @@ var serveCmd = newServeCommand(&serveEnv{lc: &localClient})
func newServeCommand(e *serveEnv) *ffcli.Command {
return &ffcli.Command{
Name: "serve",
ShortHelp: "Serve content and local servers",
ShortUsage: strings.Join([]string{
"serve http:<port> <mount-point> <source> [off]",
"serve https:<port> <mount-point> <source> [off]",
"serve tcp:<port> tcp://localhost:<local-port> [off]",
"serve tls-terminated-tcp:<port> tcp://localhost:<local-port> [off]",
"serve status [--json]",
"serve reset",
}, "\n "),
ShortHelp: "[ALPHA] Serve from your Tailscale node",
ShortUsage: strings.TrimSpace(`
serve https:<port> <mount-point> <source> [off]
serve tcp:<port> tcp://localhost:<local-port> [off]
serve tls-terminated-tcp:<port> tcp://localhost:<local-port> [off]
serve status [--json]
`),
LongHelp: strings.TrimSpace(`
*** BETA; all of this is subject to change ***
*** ALPHA; all of this is subject to change ***
The 'tailscale serve' set of commands allows you to serve
content and local servers from your Tailscale node to
your tailnet.
your tailnet.
You can also choose to enable the Tailscale Funnel with:
'tailscale funnel on'. Funnel allows you to publish
@@ -60,8 +56,8 @@ EXAMPLES
- To proxy requests to a web server at 127.0.0.1:3000:
$ tailscale serve https:443 / http://127.0.0.1:3000
Or, using the default port (443):
$ tailscale serve https / http://127.0.0.1:3000
Or, using the default port:
$ tailscale serve https / http://127.0.0.1:3000
- To serve a single file or a directory of files:
$ tailscale serve https / /home/alice/blog/index.html
@@ -70,18 +66,10 @@ EXAMPLES
- To serve simple static text:
$ tailscale serve https:8080 / text:"Hello, world!"
- To serve over HTTP (tailnet only):
$ tailscale serve http:80 / http://127.0.0.1:3000
Or, using the default port (80):
$ tailscale serve http / http://127.0.0.1:3000
- To forward incoming TCP connections on port 2222 to a local TCP server on
port 22 (e.g. to run OpenSSH in parallel with Tailscale SSH):
- To forward raw TCP packets to a local TCP server on port 5432:
$ tailscale serve tcp:2222 tcp://localhost:22
- To accept TCP TLS connections (terminated within tailscaled) proxied to a
local plaintext server on port 80:
- To forward raw, TLS-terminated TCP packets to a local TCP server on port 80:
$ tailscale serve tls-terminated-tcp:443 tcp://localhost:80
`),
Exec: e.runServe,
@@ -96,13 +84,6 @@ EXAMPLES
}),
UsageFunc: usageFunc,
},
{
Name: "reset",
Exec: e.runServeReset,
ShortHelp: "reset current serve/funnel config",
FlagSet: e.newFlags("serve-reset", nil),
UsageFunc: usageFunc,
},
},
}
}
@@ -129,7 +110,6 @@ type localServeClient interface {
Status(context.Context) (*ipnstate.Status, error)
GetServeConfig(context.Context) (*ipn.ServeConfig, error)
SetServeConfig(context.Context, *ipn.ServeConfig) error
QueryFeature(context.Context, string) (*tailcfg.QueryFeatureResponse, error)
}
// serveEnv is the environment the serve command runs within. All I/O should be
@@ -184,7 +164,6 @@ func (e *serveEnv) getLocalClientStatus(ctx context.Context) (*ipnstate.Status,
// serve config types like proxy, path, and text.
//
// Examples:
// - tailscale serve http / http://localhost:3000
// - tailscale serve https / http://localhost:3000
// - tailscale serve https /images/ /var/www/images/
// - tailscale serve https:10000 /motd.txt text:"Hello, world!"
@@ -209,14 +188,19 @@ func (e *serveEnv) runServe(ctx context.Context, args []string) error {
return e.lc.SetServeConfig(ctx, sc)
}
parsePort := func(portStr string) (uint16, error) {
port64, err := strconv.ParseUint(portStr, 10, 16)
if err != nil {
return 0, err
}
return uint16(port64), nil
}
srcType, srcPortStr, found := strings.Cut(args[0], ":")
if !found {
if srcType == "https" && srcPortStr == "" {
// Default https port to 443.
srcPortStr = "443"
} else if srcType == "http" && srcPortStr == "" {
// Default http port to 80.
srcPortStr = "80"
} else {
return flag.ErrHelp
}
@@ -224,18 +208,18 @@ func (e *serveEnv) runServe(ctx context.Context, args []string) error {
turnOff := "off" == args[len(args)-1]
if len(args) < 2 || ((srcType == "https" || srcType == "http") && !turnOff && len(args) < 3) {
if len(args) < 2 || (srcType == "https" && !turnOff && len(args) < 3) {
fmt.Fprintf(os.Stderr, "error: invalid number of arguments\n\n")
return flag.ErrHelp
}
srcPort, err := parseServePort(srcPortStr)
srcPort, err := parsePort(srcPortStr)
if err != nil {
return fmt.Errorf("invalid port %q: %w", srcPortStr, err)
return err
}
switch srcType {
case "https", "http":
case "https":
mount, err := cleanMountPoint(args[1])
if err != nil {
return err
@@ -243,8 +227,7 @@ func (e *serveEnv) runServe(ctx context.Context, args []string) error {
if turnOff {
return e.handleWebServeRemove(ctx, srcPort, mount)
}
useTLS := srcType == "https"
return e.handleWebServe(ctx, srcPort, useTLS, mount, args[2])
return e.handleWebServe(ctx, srcPort, mount, args[2])
case "tcp", "tls-terminated-tcp":
if turnOff {
return e.handleTCPServeRemove(ctx, srcPort)
@@ -252,20 +235,20 @@ func (e *serveEnv) runServe(ctx context.Context, args []string) error {
return e.handleTCPServe(ctx, srcType, srcPort, args[1])
default:
fmt.Fprintf(os.Stderr, "error: invalid serve type %q\n", srcType)
fmt.Fprint(os.Stderr, "must be one of: http:<port>, https:<port>, tcp:<port> or tls-terminated-tcp:<port>\n\n", srcType)
fmt.Fprint(os.Stderr, "must be one of: https:<port>, tcp:<port> or tls-terminated-tcp:<port>\n\n", srcType)
return flag.ErrHelp
}
}
// handleWebServe handles the "tailscale serve (http/https):..." subcommand. It
// configures the serve config to forward HTTPS connections to the given source.
// handleWebServe handles the "tailscale serve https:..." subcommand.
// It configures the serve config to forward HTTPS connections to the
// given source.
//
// Examples:
// - tailscale serve http / http://localhost:3000
// - tailscale serve https / http://localhost:3000
// - tailscale serve https:8443 /files/ /home/alice/shared-files/
// - tailscale serve https:10000 /motd.txt text:"Hello, world!"
func (e *serveEnv) handleWebServe(ctx context.Context, srvPort uint16, useTLS bool, mount, source string) error {
func (e *serveEnv) handleWebServe(ctx context.Context, srvPort uint16, mount, source string) error {
h := new(ipn.HTTPHandler)
ts, _, _ := strings.Cut(source, ":")
@@ -324,7 +307,7 @@ func (e *serveEnv) handleWebServe(ctx context.Context, srvPort uint16, useTLS bo
return flag.ErrHelp
}
mak.Set(&sc.TCP, srvPort, &ipn.TCPPortHandler{HTTPS: useTLS, HTTP: !useTLS})
mak.Set(&sc.TCP, srvPort, &ipn.TCPPortHandler{HTTPS: true})
if _, ok := sc.Web[hp]; !ok {
mak.Set(&sc.Web, hp, new(ipn.WebServerConfig))
@@ -427,7 +410,6 @@ func cleanMountPoint(mount string) (string, error) {
if mount == "" {
return "", errors.New("mount point cannot be empty")
}
mount = cleanMinGWPathConversionIfNeeded(mount)
if !strings.HasPrefix(mount, "/") {
mount = "/" + mount
}
@@ -438,26 +420,6 @@ func cleanMountPoint(mount string) (string, error) {
return "", fmt.Errorf("invalid mount point %q", mount)
}
// cleanMinGWPathConversionIfNeeded strips the EXEPATH prefix from the given
// path if the path is a MinGW(ish) (Windows) shell arg.
//
// MinGW(ish) (Windows) shells perform POSIX-to-Windows path conversion
// converting the leading "/" of any shell arg to the EXEPATH, which mangles the
// mount point. Strip the EXEPATH prefix if it exists. #7963
//
// "/C:/Program Files/Git/foo" -> "/foo"
func cleanMinGWPathConversionIfNeeded(path string) string {
// Only do this on Windows.
if runtime.GOOS != "windows" {
return path
}
if _, ok := os.LookupEnv("MSYSTEM"); ok {
exepath := filepath.ToSlash(os.Getenv("EXEPATH"))
path = strings.TrimPrefix(path, exepath)
}
return path
}
func expandProxyTarget(source string) (string, error) {
if !strings.Contains(source, "://") {
source = "http://" + source
@@ -489,7 +451,6 @@ func expandProxyTarget(source string) (string, error) {
if u.Port() != "" {
url += ":" + u.Port()
}
url += u.Path
return url, nil
}
@@ -632,10 +593,7 @@ func (e *serveEnv) runServeStatus(ctx context.Context, args []string) error {
printf("\n")
}
for hp := range sc.Web {
err := e.printWebStatusTree(sc, hp)
if err != nil {
return err
}
printWebStatusTree(sc, hp)
printf("\n")
}
printFunnelWarning(sc)
@@ -674,37 +632,20 @@ func printTCPStatusTree(ctx context.Context, sc *ipn.ServeConfig, st *ipnstate.S
return nil
}
func (e *serveEnv) printWebStatusTree(sc *ipn.ServeConfig, hp ipn.HostPort) error {
// No-op if no serve config
func printWebStatusTree(sc *ipn.ServeConfig, hp ipn.HostPort) {
if sc == nil {
return nil
return
}
fStatus := "tailnet only"
if sc.AllowFunnel[hp] {
fStatus = "Funnel on"
}
host, portStr, _ := net.SplitHostPort(string(hp))
port, err := parseServePort(portStr)
if err != nil {
return fmt.Errorf("invalid port %q: %w", portStr, err)
if portStr == "443" {
printf("https://%s (%s)\n", host, fStatus)
} else {
printf("https://%s:%s (%s)\n", host, portStr, fStatus)
}
scheme := "https"
if sc.IsServingHTTP(port) {
scheme = "http"
}
portPart := ":" + portStr
if scheme == "http" && portStr == "80" ||
scheme == "https" && portStr == "443" {
portPart = ""
}
if scheme == "http" {
hostname, _, _ := strings.Cut(host, ".")
printf("%s://%s%s (%s)\n", scheme, hostname, portPart, fStatus)
}
printf("%s://%s%s (%s)\n", scheme, host, portPart, fStatus)
srvTypeAndDesc := func(h *ipn.HTTPHandler) (string, string) {
switch {
case h.Path != "":
@@ -731,8 +672,6 @@ func (e *serveEnv) printWebStatusTree(sc *ipn.ServeConfig, hp ipn.HostPort) erro
t, d := srvTypeAndDesc(h)
printf("%s %s%s %-5s %s\n", "|--", m, strings.Repeat(" ", maxLen-len(m)), t, d)
}
return nil
}
func elipticallyTruncate(s string, max int) string {
@@ -741,28 +680,3 @@ func elipticallyTruncate(s string, max int) string {
}
return s[:max-3] + "..."
}
// runServeReset clears out the current serve config.
//
// Usage:
// - tailscale serve reset
func (e *serveEnv) runServeReset(ctx context.Context, args []string) error {
if len(args) != 0 {
return flag.ErrHelp
}
sc := new(ipn.ServeConfig)
return e.lc.SetServeConfig(ctx, sc)
}
// parseServePort parses a port number from a string and returns it as a
// uint16. It returns an error if the port number is invalid or zero.
func parseServePort(s string) (uint16, error) {
p, err := strconv.ParseUint(s, 10, 16)
if err != nil {
return 0, err
}
if p == 0 {
return 0, errors.New("port number must be non-zero")
}
return uint16(p), nil
}

View File

@@ -89,59 +89,6 @@ func TestServeConfigMutations(t *testing.T) {
wantErr: exactErr(flag.ErrHelp, "flag.ErrHelp"),
})
// https
add(step{reset: true})
add(step{ // allow omitting port (default to 80)
command: cmd("http / http://localhost:3000"),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{80: {HTTP: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"foo.test.ts.net:80": {Handlers: map[string]*ipn.HTTPHandler{
"/": {Proxy: "http://127.0.0.1:3000"},
}},
},
},
})
add(step{ // support non Funnel port
command: cmd("http:9999 /abc http://localhost:3001"),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{80: {HTTP: true}, 9999: {HTTP: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"foo.test.ts.net:80": {Handlers: map[string]*ipn.HTTPHandler{
"/": {Proxy: "http://127.0.0.1:3000"},
}},
"foo.test.ts.net:9999": {Handlers: map[string]*ipn.HTTPHandler{
"/abc": {Proxy: "http://127.0.0.1:3001"},
}},
},
},
})
add(step{
command: cmd("http:9999 /abc off"),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{80: {HTTP: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"foo.test.ts.net:80": {Handlers: map[string]*ipn.HTTPHandler{
"/": {Proxy: "http://127.0.0.1:3000"},
}},
},
},
})
add(step{
command: cmd("http:8080 /abc http://127.0.0.1:3001"),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{80: {HTTP: true}, 8080: {HTTP: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"foo.test.ts.net:80": {Handlers: map[string]*ipn.HTTPHandler{
"/": {Proxy: "http://127.0.0.1:3000"},
}},
"foo.test.ts.net:8080": {Handlers: map[string]*ipn.HTTPHandler{
"/abc": {Proxy: "http://127.0.0.1:3001"},
}},
},
},
})
// https
add(step{reset: true})
add(step{
@@ -277,10 +224,7 @@ func TestServeConfigMutations(t *testing.T) {
command: cmd("https:443 bar https://127.0.0.1:8443"),
want: nil, // nothing to save
})
add(step{ // try resetting using reset command
command: cmd("reset"),
want: &ipn.ServeConfig{},
})
add(step{reset: true})
add(step{
command: cmd("https:443 / https+insecure://127.0.0.1:3001"),
want: &ipn.ServeConfig{
@@ -318,18 +262,6 @@ func TestServeConfigMutations(t *testing.T) {
},
},
})
add(step{reset: true})
add(step{ // support path in proxy
command: cmd("https / http://127.0.0.1:3000/foo/bar"),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
"/": {Proxy: "http://127.0.0.1:3000/foo/bar"},
}},
},
},
})
// tcp
add(step{reset: true})
@@ -782,10 +714,6 @@ func (lc *fakeLocalServeClient) SetServeConfig(ctx context.Context, config *ipn.
return nil
}
func (lc *fakeLocalServeClient) QueryFeature(context.Context, string) (*tailcfg.QueryFeatureResponse, error) {
return nil, nil
}
// exactError returns an error checker that wants exactly the provided want error.
// If optName is non-empty, it's used in the error message.
func exactErr(want error, optName ...string) func(error) string {

View File

@@ -200,8 +200,6 @@ func runStatus(ctx context.Context, args []string) error {
if statusArgs.self && st.Self != nil {
printPS(st.Self)
}
locBasedExitNode := false
if statusArgs.peers {
var peers []*ipnstate.PeerStatus
for _, peer := range st.Peers() {
@@ -209,12 +207,6 @@ func runStatus(ctx context.Context, args []string) error {
if ps.ShareeNode {
continue
}
if ps.Location != nil && ps.ExitNodeOption && !ps.ExitNode {
// Location based exit nodes are only shown with the
// `exit-node list` command.
locBasedExitNode = true
continue
}
peers = append(peers, ps)
}
ipnstate.SortPeers(peers)
@@ -226,10 +218,6 @@ func runStatus(ctx context.Context, args []string) error {
}
}
Stdout.Write(buf.Bytes())
if locBasedExitNode {
println()
println("# To see the full list of exit nodes, including location-based exit nodes, run `tailscale exit-node list` \n")
}
if len(st.Health) > 0 {
outln()
printHealth()

View File

@@ -13,13 +13,11 @@ import (
"fmt"
"log"
"net/netip"
"net/url"
"os"
"os/signal"
"reflect"
"runtime"
"sort"
"strconv"
"strings"
"sync"
"syscall"
@@ -28,8 +26,6 @@ import (
shellquote "github.com/kballard/go-shellquote"
"github.com/peterbourgon/ff/v3/ffcli"
qrcode "github.com/skip2/go-qrcode"
"golang.org/x/oauth2/clientcredentials"
"tailscale.com/client/tailscale"
"tailscale.com/health/healthmsg"
"tailscale.com/ipn"
"tailscale.com/ipn/ipnstate"
@@ -38,7 +34,6 @@ import (
"tailscale.com/tailcfg"
"tailscale.com/types/logger"
"tailscale.com/types/preftype"
"tailscale.com/util/dnsname"
"tailscale.com/version"
"tailscale.com/version/distro"
)
@@ -325,8 +320,8 @@ func prefsFromUpArgs(upArgs upArgsT, warnf logger.Logf, st *ipnstate.Status, goo
}
}
if err := dnsname.ValidHostname(upArgs.hostname); upArgs.hostname != "" && err != nil {
return nil, err
if len(upArgs.hostname) > 256 {
return nil, fmt.Errorf("hostname too long: %d bytes (max 256)", len(upArgs.hostname))
}
prefs := ipn.NewPrefs()
@@ -667,10 +662,6 @@ func runUp(ctx context.Context, cmd string, args []string, upArgs upArgsT) (retE
if err != nil {
return err
}
authKey, err = resolveAuthKey(ctx, authKey, upArgs.advertiseTags)
if err != nil {
return err
}
if err := localClient.Start(ctx, ipn.Options{
AuthKey: authKey,
UpdatePrefs: prefs,
@@ -725,8 +716,7 @@ func runUp(ctx context.Context, cmd string, args []string, upArgs upArgsT) (retE
// the health check, rather than just a string.
func upWorthyWarning(s string) bool {
return strings.Contains(s, healthmsg.TailscaleSSHOnBut) ||
strings.Contains(s, healthmsg.WarnAcceptRoutesOff) ||
strings.Contains(s, healthmsg.LockedOut)
strings.Contains(s, healthmsg.WarnAcceptRoutesOff)
}
func checkUpWarnings(ctx context.Context) {
@@ -1111,93 +1101,3 @@ func anyPeerAdvertisingRoutes(st *ipnstate.Status) bool {
}
return false
}
func init() {
// Required to use our client API. We're fine with the instability since the
// client lives in the same repo as this code.
tailscale.I_Acknowledge_This_API_Is_Unstable = true
}
// resolveAuthKey either returns v unchanged (in the common case) or, if it
// starts with "tskey-client-" (as Tailscale OAuth secrets do) parses it like
//
// tskey-client-xxxx[?ephemeral=false&bar&preauthorized=BOOL&baseURL=...]
//
// and does the OAuth2 dance to get and return an authkey. The "ephemeral"
// property defaults to true if unspecified. The "preauthorized" defaults to
// false. The "baseURL" defaults to https://api.tailscale.com.
// The passed in tags are required, and must be non-empty. These will be
// set on the authkey generated by the OAuth2 dance.
func resolveAuthKey(ctx context.Context, v, tags string) (string, error) {
if !strings.HasPrefix(v, "tskey-client-") {
return v, nil
}
if tags == "" {
return "", errors.New("oauth authkeys require --advertise-tags")
}
clientSecret, named, _ := strings.Cut(v, "?")
attrs, err := url.ParseQuery(named)
if err != nil {
return "", err
}
for k := range attrs {
switch k {
case "ephemeral", "preauthorized", "baseURL":
default:
return "", fmt.Errorf("unknown attribute %q", k)
}
}
getBool := func(name string, def bool) (bool, error) {
v := attrs.Get(name)
if v == "" {
return def, nil
}
ret, err := strconv.ParseBool(v)
if err != nil {
return false, fmt.Errorf("invalid attribute boolean attribute %s value %q", name, v)
}
return ret, nil
}
ephemeral, err := getBool("ephemeral", true)
if err != nil {
return "", err
}
preauth, err := getBool("preauthorized", false)
if err != nil {
return "", err
}
baseURL := "https://api.tailscale.com"
if v := attrs.Get("baseURL"); v != "" {
baseURL = v
}
credentials := clientcredentials.Config{
ClientID: "some-client-id", // ignored
ClientSecret: clientSecret,
TokenURL: baseURL + "/api/v2/oauth/token",
Scopes: []string{"device"},
}
tsClient := tailscale.NewClient("-", nil)
tsClient.HTTPClient = credentials.Client(ctx)
tsClient.BaseURL = baseURL
caps := tailscale.KeyCapabilities{
Devices: tailscale.KeyDeviceCapabilities{
Create: tailscale.KeyDeviceCreateCapabilities{
Reusable: false,
Ephemeral: ephemeral,
Preauthorized: preauth,
Tags: strings.Split(tags, ","),
},
},
}
authkey, _, err := tsClient.CreateKey(ctx, caps)
if err != nil {
return "", err
}
return authkey, nil
}

View File

@@ -44,27 +44,17 @@ var updateCmd = &ffcli.Command{
fs := newFlagSet("update")
fs.BoolVar(&updateArgs.yes, "yes", false, "update without interactive prompts")
fs.BoolVar(&updateArgs.dryRun, "dry-run", false, "print what update would do without doing it, or prompts")
fs.BoolVar(&updateArgs.appStore, "app-store", false, "HIDDEN: check the App Store for updates, even if this is not an App Store install (for testing only)")
// These flags are not supported on several systems that only provide
// the latest version of Tailscale:
//
// - Arch (and other pacman-based distros)
// - Alpine (and other apk-based distros)
// - FreeBSD (and other pkg-based distros)
if distro.Get() != distro.Arch && distro.Get() != distro.Alpine && runtime.GOOS != "freebsd" {
fs.StringVar(&updateArgs.track, "track", "", `which track to check for updates: "stable" or "unstable" (dev); empty means same as current`)
fs.StringVar(&updateArgs.version, "version", "", `explicit version to update/downgrade to`)
}
fs.StringVar(&updateArgs.track, "track", "", `which track to check for updates: "stable" or "unstable" (dev); empty means same as current`)
fs.StringVar(&updateArgs.version, "version", "", `explicit version to update/downgrade to`)
return fs
})(),
}
var updateArgs struct {
yes bool
dryRun bool
appStore bool
track string // explicit track; empty means same as current
version string // explicit version; empty means auto
yes bool
dryRun bool
track string // explicit track; empty means same as current
version string // explicit version; empty means auto
}
// winMSIEnv is the environment variable that, if set, is the MSI file for the
@@ -147,37 +137,16 @@ func newUpdater() (*updater, error) {
up.update = up.updateSynology
case distro.Debian: // includes Ubuntu
up.update = up.updateDebLike
case distro.Arch:
up.update = up.updateArchLike
case distro.Alpine:
up.update = up.updateAlpineLike
}
// TODO(awly): add support for Alpine
switch {
case haveExecutable("pacman"):
up.update = up.updateArchLike
case haveExecutable("apt-get"): // TODO(awly): add support for "apt"
// The distro.Debian switch case above should catch most apt-based
// systems, but add this fallback just in case.
up.update = up.updateDebLike
case haveExecutable("dnf"):
up.update = up.updateFedoraLike("dnf")
case haveExecutable("yum"):
up.update = up.updateFedoraLike("yum")
case haveExecutable("apk"):
up.update = up.updateAlpineLike
}
case "darwin":
switch {
case !updateArgs.appStore && !version.IsSandboxedMacOS():
case !version.IsSandboxedMacOS():
return nil, errors.New("The 'update' command is not yet supported on this platform; see https://github.com/tailscale/tailscale/wiki/Tailscaled-on-macOS/ for now")
case !updateArgs.appStore && strings.HasSuffix(os.Getenv("HOME"), "/io.tailscale.ipn.macsys/Data"):
case strings.HasSuffix(os.Getenv("HOME"), "/io.tailscale.ipn.macsys/Data"):
up.update = up.updateMacSys
default:
up.update = up.updateMacAppStore
return nil, errors.New("This is the macOS App Store version of Tailscale; update in the App Store, or see https://tailscale.com/s/unstable-clients to use TestFlight or to install the non-App Store version")
}
case "freebsd":
up.update = up.updateFreeBSD
}
if up.update == nil {
return nil, errors.New("The 'update' command is not supported on this platform; see https://tailscale.com/s/client-updates")
@@ -202,8 +171,6 @@ func (up *updater) currentOrDryRun(ver string) bool {
return false
}
var errUserAborted = errors.New("aborting update")
func (up *updater) confirm(ver string) error {
if updateArgs.yes {
log.Printf("Updating Tailscale from %v to %v; --yes given, continuing without prompts.\n", version.Short(), ver)
@@ -218,7 +185,7 @@ func (up *updater) confirm(ver string) error {
case "y", "yes", "sure":
return nil
}
return errUserAborted
return errors.New("aborting update")
}
func (up *updater) updateSynology() error {
@@ -230,22 +197,48 @@ func (up *updater) updateSynology() error {
}
func (up *updater) updateDebLike() error {
ver, err := requestedTailscaleVersion(updateArgs.version, up.track)
if err != nil {
return err
ver := updateArgs.version
if ver == "" {
res, err := http.Get("https://pkgs.tailscale.com/" + up.track + "/?mode=json")
if err != nil {
return err
}
var latest struct {
Tarballs map[string]string // ~goarch (ignoring "geode") => "tailscale_1.34.2_mips.tgz"
}
err = json.NewDecoder(res.Body).Decode(&latest)
res.Body.Close()
if err != nil {
return fmt.Errorf("decoding JSON: %v: %w", res.Status, err)
}
f, ok := latest.Tarballs[runtime.GOARCH]
if !ok {
return fmt.Errorf("can't update architecture %q", runtime.GOARCH)
}
ver, _, ok = strings.Cut(strings.TrimPrefix(f, "tailscale_"), "_")
if !ok {
return fmt.Errorf("can't parse version from %q", f)
}
}
if up.currentOrDryRun(ver) {
return nil
}
if err := requireRoot(); err != nil {
return err
track := "unstable"
if stable, ok := versionIsStable(ver); !ok {
return fmt.Errorf("malformed version %q", ver)
} else if stable {
track = "stable"
}
if updated, err := updateDebianAptSourcesList(up.track); err != nil {
if os.Geteuid() != 0 {
return errors.New("must be root; use sudo")
}
if updated, err := updateDebianAptSourcesList(track); err != nil {
return err
} else if updated {
fmt.Printf("Updated %s to use the %s track\n", aptSourcesFile, up.track)
fmt.Printf("Updated %s to use the %s track\n", aptSourcesFile, track)
}
cmd := exec.Command("apt-get", "update",
@@ -331,204 +324,6 @@ func updateDebianAptSourcesListBytes(was []byte, dstTrack string) (newContent []
return buf.Bytes(), nil
}
func (up *updater) updateArchLike() (err error) {
if err := requireRoot(); err != nil {
return err
}
defer func() {
if err != nil && !errors.Is(err, errUserAborted) {
err = fmt.Errorf(`%w; you can try updating using "pacman --sync --refresh tailscale"`, err)
}
}()
out, err := exec.Command("pacman", "--sync", "--refresh", "--info", "tailscale").CombinedOutput()
if err != nil {
return fmt.Errorf("failed checking pacman for latest tailscale version: %w, output: %q", err, out)
}
ver, err := parsePacmanVersion(out)
if err != nil {
return err
}
if up.currentOrDryRun(ver) {
return nil
}
if err := up.confirm(ver); err != nil {
return err
}
cmd := exec.Command("pacman", "--sync", "--noconfirm", "tailscale")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return fmt.Errorf("failed tailscale update using pacman: %w", err)
}
return nil
}
func parsePacmanVersion(out []byte) (string, error) {
for _, line := range strings.Split(string(out), "\n") {
// The line we're looking for looks like this:
// Version : 1.44.2-1
if !strings.HasPrefix(line, "Version") {
continue
}
parts := strings.SplitN(line, ":", 2)
if len(parts) != 2 {
return "", fmt.Errorf("version output from pacman is malformed: %q, cannot determine upgrade version", line)
}
ver := strings.TrimSpace(parts[1])
// Trim the Arch patch version.
ver = strings.Split(ver, "-")[0]
if ver == "" {
return "", fmt.Errorf("version output from pacman is malformed: %q, cannot determine upgrade version", line)
}
return ver, nil
}
return "", fmt.Errorf("could not find latest version of tailscale via pacman")
}
const yumRepoConfigFile = "/etc/yum.repos.d/tailscale.repo"
// updateFedoraLike updates tailscale on any distros in the Fedora family,
// specifically anything that uses "dnf" or "yum" package managers. The actual
// package manager is passed via packageManager.
func (up *updater) updateFedoraLike(packageManager string) func() error {
return func() (err error) {
if err := requireRoot(); err != nil {
return err
}
defer func() {
if err != nil && !errors.Is(err, errUserAborted) {
err = fmt.Errorf(`%w; you can try updating using "%s upgrade tailscale"`, err, packageManager)
}
}()
ver, err := requestedTailscaleVersion(updateArgs.version, up.track)
if err != nil {
return err
}
if up.currentOrDryRun(ver) {
return nil
}
if err := up.confirm(ver); err != nil {
return err
}
if updated, err := updateYUMRepoTrack(yumRepoConfigFile, up.track); err != nil {
return err
} else if updated {
fmt.Printf("Updated %s to use the %s track\n", yumRepoConfigFile, up.track)
}
cmd := exec.Command(packageManager, "install", "--assumeyes", fmt.Sprintf("tailscale-%s-1", ver))
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return err
}
return nil
}
}
// updateYUMRepoTrack updates the repoFile file to make sure it has the
// provided track (stable or unstable) in it.
func updateYUMRepoTrack(repoFile, dstTrack string) (rewrote bool, err error) {
was, err := os.ReadFile(repoFile)
if err != nil {
return false, err
}
urlRe := regexp.MustCompile(`^(baseurl|gpgkey)=https://pkgs\.tailscale\.com/(un)?stable/`)
urlReplacement := fmt.Sprintf("$1=https://pkgs.tailscale.com/%s/", dstTrack)
s := bufio.NewScanner(bytes.NewReader(was))
newContent := bytes.NewBuffer(make([]byte, 0, len(was)))
for s.Scan() {
line := s.Text()
// Handle repo section name, like "[tailscale-stable]".
if len(line) > 0 && line[0] == '[' {
if !strings.HasPrefix(line, "[tailscale-") {
return false, fmt.Errorf("%q does not look like a tailscale repo file, it contains an unexpected %q section", repoFile, line)
}
fmt.Fprintf(newContent, "[tailscale-%s]\n", dstTrack)
continue
}
// Update the track mentioned in repo name.
if strings.HasPrefix(line, "name=") {
fmt.Fprintf(newContent, "name=Tailscale %s\n", dstTrack)
continue
}
// Update the actual repo URLs.
if strings.HasPrefix(line, "baseurl=") || strings.HasPrefix(line, "gpgkey=") {
fmt.Fprintln(newContent, urlRe.ReplaceAllString(line, urlReplacement))
continue
}
fmt.Fprintln(newContent, line)
}
if bytes.Equal(was, newContent.Bytes()) {
return false, nil
}
return true, os.WriteFile(repoFile, newContent.Bytes(), 0644)
}
func (up *updater) updateAlpineLike() (err error) {
if err := requireRoot(); err != nil {
return err
}
defer func() {
if err != nil && !errors.Is(err, errUserAborted) {
err = fmt.Errorf(`%w; you can try updating using "apk upgrade tailscale"`, err)
}
}()
out, err := exec.Command("apk", "update").CombinedOutput()
if err != nil {
return fmt.Errorf("failed refresh apk repository indexes: %w, output: %q", err, out)
}
out, err = exec.Command("apk", "info", "tailscale").CombinedOutput()
if err != nil {
return fmt.Errorf("failed checking apk for latest tailscale version: %w, output: %q", err, out)
}
ver, err := parseAlpinePackageVersion(out)
if err != nil {
return fmt.Errorf(`failed to parse latest version from "apk info tailscale": %w`, err)
}
if up.currentOrDryRun(ver) {
return nil
}
if err := up.confirm(ver); err != nil {
return err
}
cmd := exec.Command("apk", "upgrade", "tailscale")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return fmt.Errorf("failed tailscale update using apk: %w", err)
}
return nil
}
func parseAlpinePackageVersion(out []byte) (string, error) {
s := bufio.NewScanner(bytes.NewReader(out))
for s.Scan() {
// The line should look like this:
// tailscale-1.44.2-r0 description:
line := strings.TrimSpace(s.Text())
if !strings.HasPrefix(line, "tailscale-") {
continue
}
parts := strings.SplitN(line, "-", 3)
if len(parts) < 3 {
return "", fmt.Errorf("malformed info line: %q", line)
}
return parts[1], nil
}
return "", errors.New("tailscale version not found in output")
}
func (up *updater) updateMacSys() error {
// use sparkle? do we have permissions from this context? does sudo help?
// We can at least fail with a command they can run to update from the shell.
@@ -538,68 +333,30 @@ func (up *updater) updateMacSys() error {
return errors.New("The 'update' command is not yet implemented on macOS.")
}
func (up *updater) updateMacAppStore() error {
out, err := exec.Command("defaults", "read", "/Library/Preferences/com.apple.commerce.plist", "AutoUpdate").CombinedOutput()
if err != nil {
return fmt.Errorf("can't check App Store auto-update setting: %w, output: %q", err, string(out))
}
const on = "1\n"
if string(out) != on {
fmt.Fprintln(os.Stderr, "NOTE: Automatic updating for App Store apps is turned off. You can change this setting in System Settings (search for update).")
}
out, err = exec.Command("softwareupdate", "--list").CombinedOutput()
if err != nil {
return fmt.Errorf("can't check App Store for available updates: %w, output: %q", err, string(out))
}
newTailscale := parseSoftwareupdateList(out)
if newTailscale == "" {
fmt.Println("no Tailscale update available")
return nil
}
newTailscaleVer := strings.TrimPrefix(newTailscale, "Tailscale-")
if up.currentOrDryRun(newTailscaleVer) {
return nil
}
if err := up.confirm(newTailscaleVer); err != nil {
return err
}
cmd := exec.Command("sudo", "softwareupdate", "--install", newTailscale)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return fmt.Errorf("can't install App Store update for Tailscale: %w", err)
}
return nil
}
var macOSAppStoreListPattern = regexp.MustCompile(`(?m)^\s+\*\s+Label:\s*(Tailscale-\d[\d\.]+)`)
// parseSoftwareupdateList searches the output of `softwareupdate --list` on
// Darwin and returns the matching Tailscale package label. If there is none,
// returns the empty string.
//
// See TestParseSoftwareupdateList for example inputs.
func parseSoftwareupdateList(stdout []byte) string {
matches := macOSAppStoreListPattern.FindSubmatch(stdout)
if len(matches) < 2 {
return ""
}
return string(matches[1])
}
var (
verifyAuthenticode func(string) error // or nil on non-Windows
markTempFileFunc func(string) error // or nil on non-Windows
)
func (up *updater) updateWindows() error {
ver, err := requestedTailscaleVersion(updateArgs.version, up.track)
if err != nil {
return err
ver := updateArgs.version
if ver == "" {
res, err := http.Get("https://pkgs.tailscale.com/" + up.track + "/?mode=json&os=windows")
if err != nil {
return err
}
var latest struct {
Version string
}
err = json.NewDecoder(res.Body).Decode(&latest)
res.Body.Close()
if err != nil {
return fmt.Errorf("decoding JSON: %v: %w", res.Status, err)
}
ver = latest.Version
if ver == "" {
return errors.New("no version found")
}
}
arch := runtime.GOARCH
if arch == "386" {
@@ -828,85 +585,3 @@ func (pw *progressWriter) print() {
pw.lastPrint = time.Now()
log.Printf("Downloaded %v/%v (%.1f%%)", pw.done, pw.total, float64(pw.done)/float64(pw.total)*100)
}
func (up *updater) updateFreeBSD() (err error) {
if err := requireRoot(); err != nil {
return err
}
defer func() {
if err != nil && !errors.Is(err, errUserAborted) {
err = fmt.Errorf(`%w; you can try updating using "pkg upgrade tailscale"`, err)
}
}()
out, err := exec.Command("pkg", "update").CombinedOutput()
if err != nil {
return fmt.Errorf("failed refresh pkg repository indexes: %w, output: %q", err, out)
}
out, err = exec.Command("pkg", "rquery", "%v", "tailscale").CombinedOutput()
if err != nil {
return fmt.Errorf("failed checking pkg for latest tailscale version: %w, output: %q", err, out)
}
ver := string(bytes.TrimSpace(out))
if up.currentOrDryRun(ver) {
return nil
}
if err := up.confirm(ver); err != nil {
return err
}
cmd := exec.Command("pkg", "upgrade", "tailscale")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return fmt.Errorf("failed tailscale update using pkg: %w", err)
}
return nil
}
func haveExecutable(name string) bool {
path, err := exec.LookPath(name)
return err == nil && path != ""
}
func requestedTailscaleVersion(ver, track string) (string, error) {
if ver != "" {
return ver, nil
}
return latestTailscaleVersion(track)
}
func latestTailscaleVersion(track string) (string, error) {
url := fmt.Sprintf("https://pkgs.tailscale.com/%s/?mode=json&os=%s", track, runtime.GOOS)
res, err := http.Get(url)
if err != nil {
return "", fmt.Errorf("fetching latest tailscale version: %w", err)
}
var latest struct {
Version string
}
err = json.NewDecoder(res.Body).Decode(&latest)
res.Body.Close()
if err != nil {
return "", fmt.Errorf("decoding JSON: %v: %w", res.Status, err)
}
if latest.Version == "" {
return "", fmt.Errorf("no version found at %q", url)
}
return latest.Version, nil
}
func requireRoot() error {
if os.Geteuid() == 0 {
return nil
}
switch runtime.GOOS {
case "linux":
return errors.New("must be root; use sudo")
case "freebsd", "openbsd":
return errors.New("must be root; use doas")
default:
return errors.New("must be root")
}
}

View File

@@ -3,11 +3,7 @@
package cli
import (
"os"
"path/filepath"
"testing"
)
import "testing"
func TestUpdateDebianAptSourcesListBytes(t *testing.T) {
tests := []struct {
@@ -77,366 +73,3 @@ func TestUpdateDebianAptSourcesListBytes(t *testing.T) {
})
}
}
func TestParseSoftwareupdateList(t *testing.T) {
tests := []struct {
name string
input []byte
want string
}{
{
name: "update-at-end-of-list",
input: []byte(`
Software Update Tool
Finding available software
Software Update found the following new or updated software:
* Label: MacBookAirEFIUpdate2.4-2.4
Title: MacBook Air EFI Firmware Update, Version: 2.4, Size: 3817K, Recommended: YES, Action: restart,
* Label: ProAppsQTCodecs-1.0
Title: ProApps QuickTime codecs, Version: 1.0, Size: 968K, Recommended: YES,
* Label: Tailscale-1.23.4
Title: The Tailscale VPN, Version: 1.23.4, Size: 1023K, Recommended: YES,
`),
want: "Tailscale-1.23.4",
},
{
name: "update-in-middle-of-list",
input: []byte(`
Software Update Tool
Finding available software
Software Update found the following new or updated software:
* Label: MacBookAirEFIUpdate2.4-2.4
Title: MacBook Air EFI Firmware Update, Version: 2.4, Size: 3817K, Recommended: YES, Action: restart,
* Label: Tailscale-1.23.5000
Title: The Tailscale VPN, Version: 1.23.4, Size: 1023K, Recommended: YES,
* Label: ProAppsQTCodecs-1.0
Title: ProApps QuickTime codecs, Version: 1.0, Size: 968K, Recommended: YES,
`),
want: "Tailscale-1.23.5000",
},
{
name: "update-not-in-list",
input: []byte(`
Software Update Tool
Finding available software
Software Update found the following new or updated software:
* Label: MacBookAirEFIUpdate2.4-2.4
Title: MacBook Air EFI Firmware Update, Version: 2.4, Size: 3817K, Recommended: YES, Action: restart,
* Label: ProAppsQTCodecs-1.0
Title: ProApps QuickTime codecs, Version: 1.0, Size: 968K, Recommended: YES,
`),
want: "",
},
{
name: "decoy-in-list",
input: []byte(`
Software Update Tool
Finding available software
Software Update found the following new or updated software:
* Label: MacBookAirEFIUpdate2.4-2.4
Title: MacBook Air EFI Firmware Update, Version: 2.4, Size: 3817K, Recommended: YES, Action: restart,
* Label: Malware-1.0
Title: * Label: Tailscale-0.99.0, Version: 1.0, Size: 968K, Recommended: NOT REALLY TBH,
`),
want: "",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
got := parseSoftwareupdateList(test.input)
if test.want != got {
t.Fatalf("got %q, want %q", got, test.want)
}
})
}
}
func TestParsePacmanVersion(t *testing.T) {
tests := []struct {
desc string
out string
want string
wantErr bool
}{
{
desc: "valid version",
out: `
:: Synchronizing package databases...
endeavouros is up to date
core is up to date
extra is up to date
multilib is up to date
Repository : extra
Name : tailscale
Version : 1.44.2-1
Description : A mesh VPN that makes it easy to connect your devices, wherever they are.
Architecture : x86_64
URL : https://tailscale.com
Licenses : MIT
Groups : None
Provides : None
Depends On : glibc
Optional Deps : None
Conflicts With : None
Replaces : None
Download Size : 7.98 MiB
Installed Size : 32.47 MiB
Packager : Christian Heusel <gromit@archlinux.org>
Build Date : Tue 18 Jul 2023 12:28:37 PM PDT
Validated By : MD5 Sum SHA-256 Sum Signature
`,
want: "1.44.2",
},
{
desc: "version without Arch patch number",
out: `
... snip ...
Name : tailscale
Version : 1.44.2
Description : A mesh VPN that makes it easy to connect your devices, wherever they are.
... snip ...
`,
want: "1.44.2",
},
{
desc: "missing version",
out: `
... snip ...
Name : tailscale
Description : A mesh VPN that makes it easy to connect your devices, wherever they are.
... snip ...
`,
wantErr: true,
},
{
desc: "empty version",
out: `
... snip ...
Name : tailscale
Version :
Description : A mesh VPN that makes it easy to connect your devices, wherever they are.
... snip ...
`,
wantErr: true,
},
{
desc: "empty input",
out: "",
wantErr: true,
},
{
desc: "sneaky version in description",
out: `
... snip ...
Name : tailscale
Description : A mesh VPN that makes it easy to connect your devices, wherever they are. Version : 1.2.3
Version : 1.44.2
... snip ...
`,
want: "1.44.2",
},
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
got, err := parsePacmanVersion([]byte(tt.out))
if err == nil && tt.wantErr {
t.Fatalf("got nil error and version %q, want non-nil error", got)
}
if err != nil && !tt.wantErr {
t.Fatalf("got error: %q, want nil", err)
}
if got != tt.want {
t.Fatalf("got version: %q, want %q", got, tt.want)
}
})
}
}
func TestUpdateYUMRepoTrack(t *testing.T) {
tests := []struct {
desc string
before string
track string
after string
rewrote bool
wantErr bool
}{
{
desc: "same track",
before: `
[tailscale-stable]
name=Tailscale stable
baseurl=https://pkgs.tailscale.com/stable/fedora/$basearch
enabled=1
type=rpm
repo_gpgcheck=1
gpgcheck=0
gpgkey=https://pkgs.tailscale.com/stable/fedora/repo.gpg
`,
track: "stable",
after: `
[tailscale-stable]
name=Tailscale stable
baseurl=https://pkgs.tailscale.com/stable/fedora/$basearch
enabled=1
type=rpm
repo_gpgcheck=1
gpgcheck=0
gpgkey=https://pkgs.tailscale.com/stable/fedora/repo.gpg
`,
},
{
desc: "change track",
before: `
[tailscale-stable]
name=Tailscale stable
baseurl=https://pkgs.tailscale.com/stable/fedora/$basearch
enabled=1
type=rpm
repo_gpgcheck=1
gpgcheck=0
gpgkey=https://pkgs.tailscale.com/stable/fedora/repo.gpg
`,
track: "unstable",
after: `
[tailscale-unstable]
name=Tailscale unstable
baseurl=https://pkgs.tailscale.com/unstable/fedora/$basearch
enabled=1
type=rpm
repo_gpgcheck=1
gpgcheck=0
gpgkey=https://pkgs.tailscale.com/unstable/fedora/repo.gpg
`,
rewrote: true,
},
{
desc: "non-tailscale repo file",
before: `
[fedora]
name=Fedora $releasever - $basearch
#baseurl=http://download.example/pub/fedora/linux/releases/$releasever/Everything/$basearch/os/
metalink=https://mirrors.fedoraproject.org/metalink?repo=fedora-$releasever&arch=$basearch
enabled=1
countme=1
metadata_expire=7d
repo_gpgcheck=0
type=rpm
gpgcheck=1
gpgkey=file:///etc/pki/rpm-gpg/RPM-GPG-KEY-fedora-$releasever-$basearch
skip_if_unavailable=False
`,
track: "stable",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
path := filepath.Join(t.TempDir(), "tailscale.repo")
if err := os.WriteFile(path, []byte(tt.before), 0644); err != nil {
t.Fatal(err)
}
rewrote, err := updateYUMRepoTrack(path, tt.track)
if err == nil && tt.wantErr {
t.Fatal("got nil error, want non-nil")
}
if err != nil && !tt.wantErr {
t.Fatalf("got error %q, want nil", err)
}
if err != nil {
return
}
if rewrote != tt.rewrote {
t.Errorf("got rewrote flag %v, want %v", rewrote, tt.rewrote)
}
after, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}
if string(after) != tt.after {
t.Errorf("got repo file after update:\n%swant:\n%s", after, tt.after)
}
})
}
}
func TestParseAlpinePackageVersion(t *testing.T) {
tests := []struct {
desc string
out string
want string
wantErr bool
}{
{
desc: "valid version",
out: `
tailscale-1.44.2-r0 description:
The easiest, most secure way to use WireGuard and 2FA
tailscale-1.44.2-r0 webpage:
https://tailscale.com/
tailscale-1.44.2-r0 installed size:
32 MiB
`,
want: "1.44.2",
},
{
desc: "wrong package output",
out: `
busybox-1.36.1-r0 description:
Size optimized toolbox of many common UNIX utilities
busybox-1.36.1-r0 webpage:
https://busybox.net/
busybox-1.36.1-r0 installed size:
924 KiB
`,
wantErr: true,
},
{
desc: "missing version",
out: `
tailscale description:
The easiest, most secure way to use WireGuard and 2FA
tailscale webpage:
https://tailscale.com/
tailscale installed size:
32 MiB
`,
wantErr: true,
},
{
desc: "empty output",
out: "",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.desc, func(t *testing.T) {
got, err := parseAlpinePackageVersion([]byte(tt.out))
if err == nil && tt.wantErr {
t.Fatalf("got nil error and version %q, want non-nil error", got)
}
if err != nil && !tt.wantErr {
t.Fatalf("got error: %q, want nil", err)
}
if got != tt.want {
t.Fatalf("got version: %q, want %q", got, tt.want)
}
})
}
}

View File

@@ -23,16 +23,14 @@ var versionCmd = &ffcli.Command{
fs := newFlagSet("version")
fs.BoolVar(&versionArgs.daemon, "daemon", false, "also print local node's daemon version")
fs.BoolVar(&versionArgs.json, "json", false, "output in JSON format")
fs.BoolVar(&versionArgs.upstream, "upstream", false, "fetch and print the latest upstream release version from pkgs.tailscale.com")
return fs
})(),
Exec: runVersion,
}
var versionArgs struct {
daemon bool // also check local node's daemon version
json bool
upstream bool
daemon bool // also check local node's daemon version
json bool
}
func runVersion(ctx context.Context, args []string) error {
@@ -49,46 +47,21 @@ func runVersion(ctx context.Context, args []string) error {
}
}
var upstreamVer string
if versionArgs.upstream {
track := "stable"
if version.IsUnstableBuild() {
track = "unstable"
}
upstreamVer, err = latestTailscaleVersion(track)
if err != nil {
return err
}
}
if versionArgs.json {
m := version.GetMeta()
if st != nil {
m.DaemonLong = st.Version
}
out := struct {
version.Meta
Upstream string `json:"upstream,omitempty"`
}{
Meta: m,
Upstream: upstreamVer,
}
e := json.NewEncoder(os.Stdout)
e.SetIndent("", "\t")
return e.Encode(out)
return e.Encode(m)
}
if st == nil {
outln(version.String())
if versionArgs.upstream {
printf(" upstream: %s\n", upstreamVer)
}
return nil
}
printf("Client: %s\n", version.String())
printf("Daemon: %s\n", st.Version)
if versionArgs.upstream {
printf("Upstream: %s\n", upstreamVer)
}
return nil
}

View File

@@ -29,10 +29,8 @@ import (
"tailscale.com/ipn"
"tailscale.com/ipn/ipnstate"
"tailscale.com/tailcfg"
"tailscale.com/util/cmpx"
"tailscale.com/util/groupmember"
"tailscale.com/version/distro"
"tailscale.com/webui"
)
//go:embed web.html
@@ -63,8 +61,6 @@ type tmplData struct {
TUNMode bool
IsSynology bool
DSMVersion int // 6 or 7, if IsSynology=true
IsUnraid bool
UnraidToken string
IPNVersion string
}
@@ -92,7 +88,6 @@ Tailscale, as opposed to a CLI or a native app.
webf := newFlagSet("web")
webf.StringVar(&webArgs.listen, "listen", "localhost:8088", "listen address; use port 0 for automatic")
webf.BoolVar(&webArgs.cgi, "cgi", false, "run as CGI script")
webf.BoolVar(&webArgs.dev, "dev", false, "run in dev mode")
return webf
})(),
Exec: runWeb,
@@ -101,7 +96,6 @@ Tailscale, as opposed to a CLI or a native app.
var webArgs struct {
listen string
cgi bool
dev bool
}
func tlsConfigFromEnvironment() *tls.Config {
@@ -132,18 +126,8 @@ func runWeb(ctx context.Context, args []string) error {
return fmt.Errorf("too many non-flag arguments: %q", args)
}
handler := webHandler
if true {
newServer := &webui.Server{
DevMode: webArgs.dev,
}
cleanup := webui.RunJSDevServer()
defer cleanup()
handler = newServer.Handle
}
if webArgs.cgi {
if err := cgi.Serve(http.HandlerFunc(handler)); err != nil {
if err := cgi.Serve(http.HandlerFunc(webHandler)); err != nil {
log.Printf("tailscale.cgi: %v", err)
return err
}
@@ -155,21 +139,24 @@ func runWeb(ctx context.Context, args []string) error {
server := &http.Server{
Addr: webArgs.listen,
TLSConfig: tlsConfig,
Handler: http.HandlerFunc(handler),
Handler: http.HandlerFunc(webHandler),
}
log.Printf("web server running on: https://%s", server.Addr)
return server.ListenAndServeTLS("", "")
} else {
log.Printf("web server running on: %s", urlOfListenAddr(webArgs.listen))
return http.ListenAndServe(webArgs.listen, http.HandlerFunc(handler))
return http.ListenAndServe(webArgs.listen, http.HandlerFunc(webHandler))
}
}
// urlOfListenAddr parses a given listen address into a formatted URL
func urlOfListenAddr(addr string) string {
host, port, _ := net.SplitHostPort(addr)
return fmt.Sprintf("http://%s", net.JoinHostPort(cmpx.Or(host, "127.0.0.1"), port))
if host == "" {
host = "127.0.0.1"
}
return fmt.Sprintf("http://%s", net.JoinHostPort(host, port))
}
// authorize returns the name of the user accessing the web UI after verifying
@@ -454,8 +441,6 @@ func webHandler(w http.ResponseWriter, r *http.Request) {
TUNMode: st.TUN,
IsSynology: distro.Get() == distro.Synology || envknob.Bool("TS_FAKE_SYNOLOGY"),
DSMVersion: distro.DSMVersion(),
IsUnraid: distro.Get() == distro.Unraid,
UnraidToken: os.Getenv("UNRAID_CSRF_TOKEN"),
IPNVersion: versionShort,
}
exitNodeRouteV4 := netip.MustParsePrefix("0.0.0.0/0")

View File

@@ -26,9 +26,9 @@
<circle opacity="0.2" cx="19.5" cy="19.5" r="2.7" fill="currentColor"></circle>
</svg>
<div class="flex items-center justify-end space-x-2 w-2/3">
{{ with .Profile }}
{{ with .Profile.LoginName }}
<div class="text-right w-full leading-4">
<h4 class="truncate leading-normal">{{.LoginName}}</h4>
<h4 class="truncate leading-normal">{{.}}</h4>
<div class="text-xs text-gray-500 text-right">
<a href="#" class="hover:text-gray-700 js-loginButton">Switch account</a> | <a href="#"
class="hover:text-gray-700 js-loginButton">Reauthenticate</a> | <a href="#"
@@ -116,12 +116,10 @@
<a class="text-xs text-gray-500 hover:text-gray-600" href="{{ .LicensesURL }}">Open Source Licenses</a>
</footer>
<script>(function () {
const advertiseExitNode = {{ .AdvertiseExitNode }};
const isUnraid = {{ .IsUnraid }};
const unraidCsrfToken = "{{ .UnraidToken }}";
const advertiseExitNode = {{.AdvertiseExitNode}};
let fetchingUrl = false;
var data = {
AdvertiseRoutes: "{{ .AdvertiseRoutes }}",
AdvertiseRoutes: "{{.AdvertiseRoutes}}",
AdvertiseExitNode: advertiseExitNode,
Reauthenticate: false,
ForceLogout: false
@@ -143,27 +141,15 @@ function postData(e) {
}
const nextUrl = new URL(window.location);
nextUrl.search = nextParams.toString()
let body = JSON.stringify(data);
let contentType = "application/json";
if (isUnraid) {
const params = new URLSearchParams();
params.append("csrf_token", unraidCsrfToken);
params.append("ts_data", JSON.stringify(data));
body = params.toString();
contentType = "application/x-www-form-urlencoded;charset=UTF-8";
}
const url = nextUrl.toString();
fetch(url, {
method: "POST",
headers: {
"Accept": "application/json",
"Content-Type": contentType,
"Content-Type": "application/json",
},
body: body
body: JSON.stringify(data)
}).then(res => res.json()).then(res => {
fetchingUrl = false;
const err = res["error"];
@@ -172,11 +158,7 @@ function postData(e) {
}
const url = res["url"];
if (url) {
if(isUnraid) {
window.open(url, "_blank");
} else {
document.location.href = url;
}
document.location.href = url;
} else {
location.reload();
}

View File

@@ -3,28 +3,17 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
filippo.io/edwards25519 from github.com/hdevalence/ed25519consensus
filippo.io/edwards25519/field from filippo.io/edwards25519
W 💣 github.com/Microsoft/go-winio from tailscale.com/safesocket
W 💣 github.com/Microsoft/go-winio/internal/fs from github.com/Microsoft/go-winio
W 💣 github.com/Microsoft/go-winio/internal/socket from github.com/Microsoft/go-winio
W github.com/Microsoft/go-winio/internal/stringbuffer from github.com/Microsoft/go-winio/internal/fs
W github.com/Microsoft/go-winio/pkg/guid from github.com/Microsoft/go-winio+
W 💣 github.com/alexbrainman/sspi from github.com/alexbrainman/sspi/negotiate+
W github.com/alexbrainman/sspi/internal/common from github.com/alexbrainman/sspi/negotiate
W 💣 github.com/alexbrainman/sspi/negotiate from tailscale.com/net/tshttpproxy
L github.com/coreos/go-iptables/iptables from tailscale.com/util/linuxfw
W 💣 github.com/dblohm7/wingoes from tailscale.com/util/winutil/authenticode
W 💣 github.com/dblohm7/wingoes/pe from tailscale.com/util/winutil/authenticode
github.com/fxamacker/cbor/v2 from tailscale.com/tka
github.com/golang/groupcache/lru from tailscale.com/net/dnscache
L github.com/google/nftables from tailscale.com/util/linuxfw
L 💣 github.com/google/nftables/alignedbuff from github.com/google/nftables/xt
L 💣 github.com/google/nftables/binaryutil from github.com/google/nftables+
L github.com/google/nftables/expr from github.com/google/nftables+
L github.com/google/nftables/internal/parseexprfunc from github.com/google/nftables+
L github.com/google/nftables/xt from github.com/google/nftables/expr+
github.com/google/uuid from tailscale.com/util/quarantine+
github.com/hdevalence/ed25519consensus from tailscale.com/tka
L github.com/josharian/native from github.com/mdlayher/netlink+
L 💣 github.com/jsimonetti/rtnetlink from tailscale.com/net/interfaces+
L 💣 github.com/jsimonetti/rtnetlink from tailscale.com/net/interfaces
L github.com/jsimonetti/rtnetlink/internal/unix from github.com/jsimonetti/rtnetlink
github.com/kballard/go-shellquote from tailscale.com/cmd/tailscale/cli
github.com/klauspost/compress/flate from nhooyr.io/websocket
@@ -32,9 +21,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
💣 github.com/mattn/go-isatty from github.com/mattn/go-colorable+
L 💣 github.com/mdlayher/netlink from github.com/jsimonetti/rtnetlink+
L 💣 github.com/mdlayher/netlink/nlenc from github.com/jsimonetti/rtnetlink+
L github.com/mdlayher/netlink/nltest from github.com/google/nftables
L 💣 github.com/mdlayher/socket from github.com/mdlayher/netlink
github.com/miekg/dns from tailscale.com/net/dns/recursive
💣 github.com/mitchellh/go-ps from tailscale.com/cmd/tailscale/cli+
github.com/peterbourgon/ff/v3 from github.com/peterbourgon/ff/v3/ffcli
github.com/peterbourgon/ff/v3/ffcli from tailscale.com/cmd/tailscale/cli
@@ -47,14 +34,11 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
github.com/tailscale/goupnp/scpd from github.com/tailscale/goupnp
github.com/tailscale/goupnp/soap from github.com/tailscale/goupnp+
github.com/tailscale/goupnp/ssdp from github.com/tailscale/goupnp
L 💣 github.com/tailscale/netlink from tailscale.com/util/linuxfw
github.com/tcnksm/go-httpstat from tailscale.com/net/netcheck
github.com/toqueteos/webbrowser from tailscale.com/cmd/tailscale/cli
L 💣 github.com/vishvananda/netlink/nl from github.com/tailscale/netlink
L github.com/vishvananda/netns from github.com/tailscale/netlink+
github.com/x448/float16 from github.com/fxamacker/cbor/v2
💣 go4.org/mem from tailscale.com/derp+
go4.org/netipx from tailscale.com/wgengine/filter+
go4.org/netipx from tailscale.com/wgengine/filter
W 💣 golang.zx2c4.com/wireguard/windows/tunnel/winipcfg from tailscale.com/net/interfaces+
gopkg.in/yaml.v2 from sigs.k8s.io/yaml
k8s.io/client-go/util/homedir from tailscale.com/cmd/tailscale/cli
@@ -68,7 +52,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
tailscale.com/atomicfile from tailscale.com/ipn+
tailscale.com/client/tailscale from tailscale.com/cmd/tailscale/cli+
tailscale.com/client/tailscale/apitype from tailscale.com/cmd/tailscale/cli+
tailscale.com/cmd/tailscale/cli from tailscale.com/cmd/tailscale
💣 tailscale.com/cmd/tailscale/cli from tailscale.com/cmd/tailscale
tailscale.com/control/controlbase from tailscale.com/control/controlhttp
tailscale.com/control/controlhttp from tailscale.com/cmd/tailscale/cli
tailscale.com/control/controlknobs from tailscale.com/net/portmapper
@@ -82,7 +66,6 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
tailscale.com/ipn from tailscale.com/cmd/tailscale/cli+
tailscale.com/ipn/ipnstate from tailscale.com/cmd/tailscale/cli+
tailscale.com/metrics from tailscale.com/derp
tailscale.com/net/dns/recursive from tailscale.com/net/dnsfallback
tailscale.com/net/dnscache from tailscale.com/derp/derphttp+
tailscale.com/net/dnsfallback from tailscale.com/control/controlhttp
tailscale.com/net/flowtrack from tailscale.com/wgengine/filter+
@@ -91,15 +74,13 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
tailscale.com/net/netcheck from tailscale.com/cmd/tailscale/cli
tailscale.com/net/neterror from tailscale.com/net/netcheck+
tailscale.com/net/netknob from tailscale.com/net/netns
tailscale.com/net/netmon from tailscale.com/net/sockstats+
tailscale.com/net/netns from tailscale.com/derp/derphttp+
tailscale.com/net/netutil from tailscale.com/client/tailscale+
tailscale.com/net/packet from tailscale.com/wgengine/filter+
tailscale.com/net/packet from tailscale.com/wgengine/filter
tailscale.com/net/ping from tailscale.com/net/netcheck
tailscale.com/net/portmapper from tailscale.com/net/netcheck+
tailscale.com/net/sockstats from tailscale.com/control/controlhttp+
tailscale.com/net/stun from tailscale.com/net/netcheck
L tailscale.com/net/tcpinfo from tailscale.com/derp
tailscale.com/net/tlsdial from tailscale.com/derp/derphttp+
tailscale.com/net/tsaddr from tailscale.com/net/interfaces+
💣 tailscale.com/net/tshttpproxy from tailscale.com/derp/derphttp+
@@ -110,9 +91,8 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
tailscale.com/tailcfg from tailscale.com/cmd/tailscale/cli+
tailscale.com/tka from tailscale.com/client/tailscale+
W tailscale.com/tsconst from tailscale.com/net/interfaces
tailscale.com/tstime from tailscale.com/control/controlhttp+
💣 tailscale.com/tstime/mono from tailscale.com/tstime/rate
tailscale.com/tstime/rate from tailscale.com/wgengine/filter+
tailscale.com/tstime/rate from tailscale.com/wgengine/filter
tailscale.com/types/dnstype from tailscale.com/tailcfg
tailscale.com/types/empty from tailscale.com/ipn
tailscale.com/types/ipproto from tailscale.com/net/flowtrack+
@@ -131,13 +111,11 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
tailscale.com/util/clientmetric from tailscale.com/net/netcheck+
tailscale.com/util/cloudenv from tailscale.com/net/dnscache+
W tailscale.com/util/cmpver from tailscale.com/net/tshttpproxy
tailscale.com/util/cmpx from tailscale.com/cmd/tailscale/cli+
L 💣 tailscale.com/util/dirwalk from tailscale.com/metrics
tailscale.com/util/dnsname from tailscale.com/cmd/tailscale/cli+
tailscale.com/util/groupmember from tailscale.com/cmd/tailscale/cli
tailscale.com/util/httpm from tailscale.com/client/tailscale
tailscale.com/util/lineread from tailscale.com/net/interfaces+
L tailscale.com/util/linuxfw from tailscale.com/net/netns
tailscale.com/util/mak from tailscale.com/net/netcheck+
tailscale.com/util/multierr from tailscale.com/control/controlhttp+
tailscale.com/util/must from tailscale.com/cmd/tailscale/cli
@@ -146,7 +124,6 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
tailscale.com/util/singleflight from tailscale.com/net/dnscache
tailscale.com/util/slicesx from tailscale.com/net/dnscache+
💣 tailscale.com/util/winutil from tailscale.com/hostinfo+
W 💣 tailscale.com/util/winutil/authenticode from tailscale.com/cmd/tailscale/cli
tailscale.com/version from tailscale.com/cmd/tailscale/cli+
tailscale.com/version/distro from tailscale.com/cmd/tailscale/cli+
tailscale.com/wgengine/capture from tailscale.com/cmd/tailscale/cli
@@ -164,8 +141,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
golang.org/x/crypto/nacl/secretbox from golang.org/x/crypto/nacl/box
golang.org/x/crypto/pbkdf2 from software.sslmate.com/src/go-pkcs12
golang.org/x/crypto/salsa20/salsa from golang.org/x/crypto/nacl/box+
golang.org/x/exp/constraints from golang.org/x/exp/slices+
golang.org/x/exp/maps from tailscale.com/types/views+
golang.org/x/exp/constraints from golang.org/x/exp/slices
golang.org/x/exp/slices from tailscale.com/net/tsaddr+
golang.org/x/net/bpf from github.com/mdlayher/netlink+
golang.org/x/net/dns/dnsmessage from net+
@@ -175,12 +151,9 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
golang.org/x/net/icmp from tailscale.com/net/ping
golang.org/x/net/idna from golang.org/x/net/http/httpguts+
golang.org/x/net/ipv4 from golang.org/x/net/icmp+
golang.org/x/net/ipv6 from golang.org/x/net/icmp+
golang.org/x/net/ipv6 from golang.org/x/net/icmp
golang.org/x/net/proxy from tailscale.com/net/netns
D golang.org/x/net/route from net+
golang.org/x/oauth2 from golang.org/x/oauth2/clientcredentials
golang.org/x/oauth2/clientcredentials from tailscale.com/cmd/tailscale/cli
golang.org/x/oauth2/internal from golang.org/x/oauth2+
golang.org/x/sync/errgroup from tailscale.com/derp+
golang.org/x/sys/cpu from golang.org/x/crypto/blake2b+
LD golang.org/x/sys/unix from tailscale.com/net/netns+
@@ -197,7 +170,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
bytes from bufio+
compress/flate from compress/gzip+
compress/gzip from net/http
compress/zlib from image/png+
compress/zlib from image/png
container/list from crypto/tls+
context from crypto/tls+
crypto from crypto/ecdsa+
@@ -222,12 +195,10 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
crypto/x509 from crypto/tls+
crypto/x509/pkix from crypto/x509+
database/sql/driver from github.com/google/uuid
W debug/dwarf from debug/pe
W debug/pe from github.com/dblohm7/wingoes/pe
embed from tailscale.com/cmd/tailscale/cli+
encoding from encoding/json+
encoding/asn1 from crypto/x509+
encoding/base32 from tailscale.com/tka+
encoding/base32 from tailscale.com/tka
encoding/base64 from encoding/json+
encoding/binary from compress/gzip+
encoding/hex from crypto/x509+
@@ -251,7 +222,6 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
io/fs from crypto/x509+
io/ioutil from golang.org/x/sys/cpu+
log from expvar+
log/internal from log
math from compress/flate+
math/big from crypto/dsa+
math/bits from compress/flate+

View File

@@ -23,10 +23,10 @@ import (
"tailscale.com/derp/derphttp"
"tailscale.com/ipn"
"tailscale.com/net/interfaces"
"tailscale.com/net/netmon"
"tailscale.com/net/tshttpproxy"
"tailscale.com/tailcfg"
"tailscale.com/types/key"
"tailscale.com/wgengine/monitor"
)
var debugArgs struct {
@@ -42,7 +42,7 @@ var debugModeFunc = debugMode // so it can be addressable
func debugMode(args []string) error {
fs := flag.NewFlagSet("debug", flag.ExitOnError)
fs.BoolVar(&debugArgs.ifconfig, "ifconfig", false, "If true, print network interface state")
fs.BoolVar(&debugArgs.monitor, "monitor", false, "If true, run network monitor forever. Precludes all other options.")
fs.BoolVar(&debugArgs.monitor, "monitor", false, "If true, run link monitor forever. Precludes all other options.")
fs.BoolVar(&debugArgs.portmap, "portmap", false, "If true, run portmap debugging. Precludes all other options.")
fs.StringVar(&debugArgs.getURL, "get-url", "", "If non-empty, fetch provided URL.")
fs.StringVar(&debugArgs.derpCheck, "derp", "", "if non-empty, test a DERP ping via named region code")
@@ -76,7 +76,7 @@ func runMonitor(ctx context.Context, loop bool) error {
j, _ := json.MarshalIndent(st, "", " ")
os.Stderr.Write(j)
}
mon, err := netmon.New(log.Printf)
mon, err := monitor.New(log.Printf)
if err != nil {
return err
}
@@ -84,10 +84,10 @@ func runMonitor(ctx context.Context, loop bool) error {
mon.RegisterChangeCallback(func(changed bool, st *interfaces.State) {
if !changed {
log.Printf("Network monitor fired; no change")
log.Printf("Link monitor fired; no change")
return
}
log.Printf("Network monitor fired. New state:")
log.Printf("Link monitor fired. New state:")
dump(st)
})
if loop {
@@ -193,8 +193,8 @@ func checkDerp(ctx context.Context, derpRegion string) (err error) {
priv1 := key.NewNode()
priv2 := key.NewNode()
c1 := derphttp.NewRegionClient(priv1, log.Printf, nil, getRegion)
c2 := derphttp.NewRegionClient(priv2, log.Printf, nil, getRegion)
c1 := derphttp.NewRegionClient(priv1, log.Printf, getRegion)
c2 := derphttp.NewRegionClient(priv2, log.Printf, getRegion)
defer func() {
if err != nil {
c1.Close()

View File

@@ -3,9 +3,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
filippo.io/edwards25519 from github.com/hdevalence/ed25519consensus
filippo.io/edwards25519/field from filippo.io/edwards25519
W 💣 github.com/Microsoft/go-winio from tailscale.com/safesocket
W 💣 github.com/Microsoft/go-winio/internal/fs from github.com/Microsoft/go-winio
W 💣 github.com/Microsoft/go-winio/internal/socket from github.com/Microsoft/go-winio
W github.com/Microsoft/go-winio/internal/stringbuffer from github.com/Microsoft/go-winio/internal/fs
W github.com/Microsoft/go-winio/pkg/guid from github.com/Microsoft/go-winio+
W 💣 github.com/alexbrainman/sspi from github.com/alexbrainman/sspi/internal/common+
W github.com/alexbrainman/sspi/internal/common from github.com/alexbrainman/sspi/negotiate
@@ -14,7 +12,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
L github.com/aws/aws-sdk-go-v2 from github.com/aws/aws-sdk-go-v2/internal/ini
L github.com/aws/aws-sdk-go-v2/aws from github.com/aws/aws-sdk-go-v2/aws/middleware+
L github.com/aws/aws-sdk-go-v2/aws/arn from tailscale.com/ipn/store/awsstore
L github.com/aws/aws-sdk-go-v2/aws/defaults from github.com/aws/aws-sdk-go-v2/service/ssm+
L github.com/aws/aws-sdk-go-v2/aws/defaults from github.com/aws/aws-sdk-go-v2/service/ssm
L github.com/aws/aws-sdk-go-v2/aws/middleware from github.com/aws/aws-sdk-go-v2/aws/retry+
L github.com/aws/aws-sdk-go-v2/aws/protocol/query from github.com/aws/aws-sdk-go-v2/service/sts
L github.com/aws/aws-sdk-go-v2/aws/protocol/restjson from github.com/aws/aws-sdk-go-v2/service/ssm+
@@ -40,7 +38,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
L github.com/aws/aws-sdk-go-v2/internal/rand from github.com/aws/aws-sdk-go-v2/aws+
L github.com/aws/aws-sdk-go-v2/internal/sdk from github.com/aws/aws-sdk-go-v2/aws+
L github.com/aws/aws-sdk-go-v2/internal/sdkio from github.com/aws/aws-sdk-go-v2/credentials/processcreds
L github.com/aws/aws-sdk-go-v2/internal/shareddefaults from github.com/aws/aws-sdk-go-v2/config+
L github.com/aws/aws-sdk-go-v2/internal/strings from github.com/aws/aws-sdk-go-v2/aws/signer/internal/v4
L github.com/aws/aws-sdk-go-v2/internal/sync/singleflight from github.com/aws/aws-sdk-go-v2/aws
L github.com/aws/aws-sdk-go-v2/internal/timeconv from github.com/aws/aws-sdk-go-v2/aws/retry
@@ -51,19 +48,16 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
L github.com/aws/aws-sdk-go-v2/service/sso from github.com/aws/aws-sdk-go-v2/config+
L github.com/aws/aws-sdk-go-v2/service/sso/internal/endpoints from github.com/aws/aws-sdk-go-v2/service/sso
L github.com/aws/aws-sdk-go-v2/service/sso/types from github.com/aws/aws-sdk-go-v2/service/sso
L github.com/aws/aws-sdk-go-v2/service/ssooidc from github.com/aws/aws-sdk-go-v2/config+
L github.com/aws/aws-sdk-go-v2/service/ssooidc/internal/endpoints from github.com/aws/aws-sdk-go-v2/service/ssooidc
L github.com/aws/aws-sdk-go-v2/service/ssooidc/types from github.com/aws/aws-sdk-go-v2/service/ssooidc
L github.com/aws/aws-sdk-go-v2/service/sts from github.com/aws/aws-sdk-go-v2/config+
L github.com/aws/aws-sdk-go-v2/service/sts/internal/endpoints from github.com/aws/aws-sdk-go-v2/service/sts
L github.com/aws/aws-sdk-go-v2/service/sts/types from github.com/aws/aws-sdk-go-v2/credentials/stscreds+
L github.com/aws/smithy-go from github.com/aws/aws-sdk-go-v2/aws/protocol/restjson+
L github.com/aws/smithy-go/auth/bearer from github.com/aws/aws-sdk-go-v2/aws+
L github.com/aws/smithy-go/auth/bearer from github.com/aws/aws-sdk-go-v2/aws
L github.com/aws/smithy-go/context from github.com/aws/smithy-go/auth/bearer
L github.com/aws/smithy-go/document from github.com/aws/aws-sdk-go-v2/service/ssm+
L github.com/aws/smithy-go/encoding from github.com/aws/smithy-go/encoding/json+
L github.com/aws/smithy-go/encoding/httpbinding from github.com/aws/aws-sdk-go-v2/aws/protocol/query+
L github.com/aws/smithy-go/encoding/json from github.com/aws/aws-sdk-go-v2/service/ssm+
L github.com/aws/smithy-go/encoding/json from github.com/aws/aws-sdk-go-v2/service/ssm
L github.com/aws/smithy-go/encoding/xml from github.com/aws/aws-sdk-go-v2/service/sts
L github.com/aws/smithy-go/internal/sync/singleflight from github.com/aws/smithy-go/auth/bearer
L github.com/aws/smithy-go/io from github.com/aws/aws-sdk-go-v2/feature/ec2/imds+
@@ -75,24 +69,16 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
L github.com/aws/smithy-go/transport/http from github.com/aws/aws-sdk-go-v2/aws/middleware+
L github.com/aws/smithy-go/transport/http/internal/io from github.com/aws/smithy-go/transport/http
L github.com/aws/smithy-go/waiter from github.com/aws/aws-sdk-go-v2/service/ssm
L github.com/coreos/go-iptables/iptables from tailscale.com/util/linuxfw
L github.com/coreos/go-iptables/iptables from tailscale.com/wgengine/router
LD 💣 github.com/creack/pty from tailscale.com/ssh/tailssh
W 💣 github.com/dblohm7/wingoes from github.com/dblohm7/wingoes/com+
W 💣 github.com/dblohm7/wingoes from github.com/dblohm7/wingoes/com
W 💣 github.com/dblohm7/wingoes/com from tailscale.com/cmd/tailscaled
W github.com/dblohm7/wingoes/internal from github.com/dblohm7/wingoes/com
W 💣 github.com/dblohm7/wingoes/pe from tailscale.com/util/osdiag+
github.com/fxamacker/cbor/v2 from tailscale.com/tka
W 💣 github.com/go-ole/go-ole from github.com/go-ole/go-ole/oleutil+
W 💣 github.com/go-ole/go-ole/oleutil from tailscale.com/wgengine/winnet
L 💣 github.com/godbus/dbus/v5 from tailscale.com/net/dns+
github.com/golang/groupcache/lru from tailscale.com/net/dnscache
github.com/google/btree from gvisor.dev/gvisor/pkg/tcpip/header+
L github.com/google/nftables from tailscale.com/util/linuxfw
L 💣 github.com/google/nftables/alignedbuff from github.com/google/nftables/xt
L 💣 github.com/google/nftables/binaryutil from github.com/google/nftables+
L github.com/google/nftables/expr from github.com/google/nftables+
L github.com/google/nftables/internal/parseexprfunc from github.com/google/nftables+
L github.com/google/nftables/xt from github.com/google/nftables/expr+
github.com/hdevalence/ed25519consensus from tailscale.com/tka
L 💣 github.com/illarion/gonotify from tailscale.com/net/dns
L github.com/insomniacslk/dhcp/dhcpv4 from tailscale.com/net/tstun
@@ -107,7 +93,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
github.com/klauspost/compress/flate from nhooyr.io/websocket
github.com/klauspost/compress/fse from github.com/klauspost/compress/huff0
github.com/klauspost/compress/huff0 from github.com/klauspost/compress/zstd
github.com/klauspost/compress/internal/cpuinfo from github.com/klauspost/compress/zstd+
github.com/klauspost/compress/internal/cpuinfo from github.com/klauspost/compress/zstd
github.com/klauspost/compress/internal/snapref from github.com/klauspost/compress/zstd
github.com/klauspost/compress/zstd from tailscale.com/smallzstd
github.com/klauspost/compress/zstd/internal/xxhash from github.com/klauspost/compress/zstd
@@ -116,23 +102,15 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
L github.com/mdlayher/genetlink from tailscale.com/net/tstun
L 💣 github.com/mdlayher/netlink from github.com/jsimonetti/rtnetlink+
L 💣 github.com/mdlayher/netlink/nlenc from github.com/jsimonetti/rtnetlink+
L github.com/mdlayher/netlink/nltest from github.com/google/nftables
L github.com/mdlayher/sdnotify from tailscale.com/util/systemd
L 💣 github.com/mdlayher/socket from github.com/mdlayher/netlink
github.com/miekg/dns from tailscale.com/net/dns/recursive
💣 github.com/mitchellh/go-ps from tailscale.com/safesocket
L github.com/pierrec/lz4/v4 from github.com/u-root/uio/uio
L github.com/pierrec/lz4/v4/internal/lz4block from github.com/pierrec/lz4/v4+
L github.com/pierrec/lz4/v4/internal/lz4errors from github.com/pierrec/lz4/v4+
L github.com/pierrec/lz4/v4/internal/lz4stream from github.com/pierrec/lz4/v4
L github.com/pierrec/lz4/v4/internal/xxh32 from github.com/pierrec/lz4/v4/internal/lz4stream
W github.com/pkg/errors from github.com/tailscale/certstore
LD github.com/pkg/sftp from tailscale.com/ssh/tailssh
LD github.com/pkg/sftp/internal/encoding/ssh/filexfer from github.com/pkg/sftp
W 💣 github.com/tailscale/certstore from tailscale.com/control/controlclient
github.com/tailscale/golang-x-crypto/acme from tailscale.com/ipn/ipnlocal
LD github.com/tailscale/golang-x-crypto/chacha20 from github.com/tailscale/golang-x-crypto/ssh
LD 💣 github.com/tailscale/golang-x-crypto/internal/alias from github.com/tailscale/golang-x-crypto/chacha20
LD 💣 github.com/tailscale/golang-x-crypto/internal/subtle from github.com/tailscale/golang-x-crypto/chacha20
LD github.com/tailscale/golang-x-crypto/ssh from tailscale.com/ipn/ipnlocal+
LD github.com/tailscale/golang-x-crypto/ssh/internal/bcrypt_pbkdf from github.com/tailscale/golang-x-crypto/ssh
github.com/tailscale/goupnp from github.com/tailscale/goupnp/dcps/internetgateway2+
@@ -176,7 +154,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
💣 gvisor.dev/gvisor/pkg/state from gvisor.dev/gvisor/pkg/atomicbitops+
gvisor.dev/gvisor/pkg/state/wire from gvisor.dev/gvisor/pkg/state
💣 gvisor.dev/gvisor/pkg/sync from gvisor.dev/gvisor/pkg/linewriter+
💣 gvisor.dev/gvisor/pkg/sync/locking from gvisor.dev/gvisor/pkg/tcpip/stack
gvisor.dev/gvisor/pkg/tcpip from gvisor.dev/gvisor/pkg/tcpip/header+
gvisor.dev/gvisor/pkg/tcpip/adapters/gonet from tailscale.com/wgengine/netstack
gvisor.dev/gvisor/pkg/tcpip/checksum from gvisor.dev/gvisor/pkg/bufferv2+
@@ -223,7 +200,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/derp/derphttp from tailscale.com/net/netcheck+
tailscale.com/disco from tailscale.com/derp+
tailscale.com/doctor from tailscale.com/ipn/ipnlocal
💣 tailscale.com/doctor/permissions from tailscale.com/ipn/ipnlocal
tailscale.com/doctor/routetable from tailscale.com/ipn/ipnlocal
tailscale.com/envknob from tailscale.com/control/controlclient+
tailscale.com/health from tailscale.com/control/controlclient+
@@ -236,7 +212,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/ipn/ipnstate from tailscale.com/control/controlclient+
tailscale.com/ipn/localapi from tailscale.com/ipn/ipnserver
tailscale.com/ipn/policy from tailscale.com/ipn/ipnlocal
tailscale.com/ipn/store from tailscale.com/cmd/tailscaled+
tailscale.com/ipn/store from tailscale.com/cmd/tailscaled
L tailscale.com/ipn/store/awsstore from tailscale.com/ipn/store
L tailscale.com/ipn/store/kubestore from tailscale.com/ipn/store
tailscale.com/ipn/store/mem from tailscale.com/ipn/store+
@@ -252,7 +228,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/net/connstats from tailscale.com/net/tstun+
tailscale.com/net/dns from tailscale.com/ipn/ipnlocal+
tailscale.com/net/dns/publicdns from tailscale.com/net/dns/resolver+
tailscale.com/net/dns/recursive from tailscale.com/net/dnsfallback
tailscale.com/net/dns/resolvconffile from tailscale.com/net/dns+
tailscale.com/net/dns/resolver from tailscale.com/ipn/ipnlocal+
tailscale.com/net/dnscache from tailscale.com/control/controlclient+
@@ -263,25 +238,22 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/net/netcheck from tailscale.com/wgengine/magicsock
tailscale.com/net/neterror from tailscale.com/net/dns/resolver+
tailscale.com/net/netknob from tailscale.com/net/netns+
tailscale.com/net/netmon from tailscale.com/cmd/tailscaled+
tailscale.com/net/netns from tailscale.com/derp/derphttp+
💣 tailscale.com/net/netstat from tailscale.com/ipn/ipnauth+
tailscale.com/net/netutil from tailscale.com/ipn/ipnlocal+
tailscale.com/net/packet from tailscale.com/net/tstun+
tailscale.com/net/ping from tailscale.com/net/netcheck+
tailscale.com/net/ping from tailscale.com/net/netcheck
tailscale.com/net/portmapper from tailscale.com/net/netcheck+
tailscale.com/net/proxymux from tailscale.com/cmd/tailscaled
tailscale.com/net/routetable from tailscale.com/doctor/routetable
tailscale.com/net/socks5 from tailscale.com/cmd/tailscaled
tailscale.com/net/sockstats from tailscale.com/control/controlclient+
tailscale.com/net/stun from tailscale.com/net/netcheck+
L tailscale.com/net/tcpinfo from tailscale.com/derp
tailscale.com/net/tlsdial from tailscale.com/control/controlclient+
tailscale.com/net/tsaddr from tailscale.com/ipn+
tailscale.com/net/tsdial from tailscale.com/control/controlclient+
💣 tailscale.com/net/tshttpproxy from tailscale.com/control/controlclient+
tailscale.com/net/tstun from tailscale.com/cmd/tailscaled+
tailscale.com/net/tstun/table from tailscale.com/net/tstun
tailscale.com/net/wsconn from tailscale.com/control/controlhttp+
tailscale.com/paths from tailscale.com/ipn/ipnlocal+
💣 tailscale.com/portlist from tailscale.com/ipn/ipnlocal
@@ -290,15 +262,13 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
LD 💣 tailscale.com/ssh/tailssh from tailscale.com/cmd/tailscaled
tailscale.com/syncs from tailscale.com/net/netcheck+
tailscale.com/tailcfg from tailscale.com/client/tailscale/apitype+
💣 tailscale.com/tempfork/device from tailscale.com/net/tstun/table
LD tailscale.com/tempfork/gliderlabs/ssh from tailscale.com/ssh/tailssh
tailscale.com/tka from tailscale.com/ipn/ipnlocal+
W tailscale.com/tsconst from tailscale.com/net/interfaces
tailscale.com/tsd from tailscale.com/cmd/tailscaled+
tailscale.com/tstime from tailscale.com/wgengine/magicsock+
tailscale.com/tstime from tailscale.com/wgengine/magicsock
💣 tailscale.com/tstime/mono from tailscale.com/net/tstun+
tailscale.com/tstime/rate from tailscale.com/wgengine/filter+
tailscale.com/tsweb/varz from tailscale.com/cmd/tailscaled
tailscale.com/tstime/rate from tailscale.com/wgengine/filter
tailscale.com/tsweb from tailscale.com/cmd/tailscaled
tailscale.com/types/dnstype from tailscale.com/ipn/ipnlocal+
tailscale.com/types/empty from tailscale.com/control/controlclient+
tailscale.com/types/flagtype from tailscale.com/cmd/tailscaled
@@ -320,7 +290,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/util/clientmetric from tailscale.com/control/controlclient+
tailscale.com/util/cloudenv from tailscale.com/net/dns/resolver+
LW tailscale.com/util/cmpver from tailscale.com/net/dns+
tailscale.com/util/cmpx from tailscale.com/derp/derphttp+
💣 tailscale.com/util/deephash from tailscale.com/ipn/ipnlocal+
L 💣 tailscale.com/util/dirwalk from tailscale.com/metrics+
tailscale.com/util/dnsname from tailscale.com/hostinfo+
@@ -329,11 +298,9 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
💣 tailscale.com/util/hashx from tailscale.com/util/deephash
tailscale.com/util/httpm from tailscale.com/client/tailscale+
tailscale.com/util/lineread from tailscale.com/hostinfo+
L tailscale.com/util/linuxfw from tailscale.com/net/netns+
tailscale.com/util/mak from tailscale.com/control/controlclient+
tailscale.com/util/multierr from tailscale.com/control/controlclient+
tailscale.com/util/must from tailscale.com/logpolicy
💣 tailscale.com/util/osdiag from tailscale.com/cmd/tailscaled+
tailscale.com/util/osshare from tailscale.com/ipn/ipnlocal+
W tailscale.com/util/pidowner from tailscale.com/ipn/ipnauth
tailscale.com/util/racebuild from tailscale.com/logpolicy
@@ -341,12 +308,10 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/util/set from tailscale.com/health+
tailscale.com/util/singleflight from tailscale.com/control/controlclient+
tailscale.com/util/slicesx from tailscale.com/net/dnscache+
tailscale.com/util/sysresources from tailscale.com/wgengine/magicsock
tailscale.com/util/systemd from tailscale.com/control/controlclient+
tailscale.com/util/uniq from tailscale.com/wgengine/magicsock+
tailscale.com/util/vizerror from tailscale.com/tsweb
💣 tailscale.com/util/winutil from tailscale.com/control/controlclient+
W 💣 tailscale.com/util/winutil/authenticode from tailscale.com/util/osdiag
W tailscale.com/util/winutil/policy from tailscale.com/ipn/ipnlocal
tailscale.com/version from tailscale.com/derp+
tailscale.com/version/distro from tailscale.com/hostinfo+
W tailscale.com/wf from tailscale.com/cmd/tailscaled
@@ -354,6 +319,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/wgengine/capture from tailscale.com/ipn/ipnlocal+
tailscale.com/wgengine/filter from tailscale.com/control/controlclient+
💣 tailscale.com/wgengine/magicsock from tailscale.com/ipn/ipnlocal+
tailscale.com/wgengine/monitor from tailscale.com/control/controlclient+
tailscale.com/wgengine/netlog from tailscale.com/wgengine
tailscale.com/wgengine/netstack from tailscale.com/cmd/tailscaled
tailscale.com/wgengine/router from tailscale.com/ipn/ipnlocal+
@@ -362,6 +328,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
💣 tailscale.com/wgengine/wgint from tailscale.com/wgengine
tailscale.com/wgengine/wglog from tailscale.com/wgengine
W 💣 tailscale.com/wgengine/winnet from tailscale.com/wgengine/router
golang.org/x/crypto/acme from tailscale.com/ipn/ipnlocal
golang.org/x/crypto/argon2 from tailscale.com/tka
golang.org/x/crypto/blake2b from golang.org/x/crypto/nacl/box+
golang.org/x/crypto/blake2s from github.com/tailscale/wireguard-go/device+
@@ -378,13 +345,13 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
golang.org/x/crypto/poly1305 from github.com/tailscale/golang-x-crypto/ssh+
golang.org/x/crypto/salsa20/salsa from golang.org/x/crypto/nacl/box+
LD golang.org/x/crypto/ssh from tailscale.com/ssh/tailssh+
golang.org/x/exp/constraints from golang.org/x/exp/slices+
golang.org/x/exp/maps from tailscale.com/wgengine+
golang.org/x/exp/constraints from golang.org/x/exp/slices
golang.org/x/exp/maps from tailscale.com/wgengine
golang.org/x/exp/slices from tailscale.com/ipn/ipnlocal+
golang.org/x/net/bpf from github.com/mdlayher/genetlink+
golang.org/x/net/dns/dnsmessage from net+
golang.org/x/net/http/httpguts from golang.org/x/net/http2+
golang.org/x/net/http/httpproxy from net/http+
golang.org/x/net/http/httpproxy from net/http
golang.org/x/net/http2 from golang.org/x/net/http2/h2c+
golang.org/x/net/http2/h2c from tailscale.com/ipn/ipnlocal
golang.org/x/net/http2/hpack from golang.org/x/net/http2+
@@ -412,7 +379,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
bytes from bufio+
compress/flate from compress/gzip+
compress/gzip from golang.org/x/net/http2+
W compress/zlib from debug/pe
container/heap from gvisor.dev/gvisor/pkg/tcpip/transport/tcp
container/list from crypto/tls+
context from crypto/tls+
@@ -437,12 +403,10 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
crypto/tls from github.com/tcnksm/go-httpstat+
crypto/x509 from crypto/tls+
crypto/x509/pkix from crypto/x509+
W debug/dwarf from debug/pe
W debug/pe from github.com/dblohm7/wingoes/pe
embed from tailscale.com+
encoding from encoding/json+
encoding/asn1 from crypto/x509+
encoding/base32 from tailscale.com/tka+
encoding/base32 from tailscale.com/tka
encoding/base64 from encoding/json+
encoding/binary from compress/gzip+
encoding/hex from crypto/x509+
@@ -454,7 +418,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
flag from net/http/httptest+
fmt from compress/flate+
hash from crypto+
hash/adler32 from tailscale.com/ipn/ipnlocal+
hash/adler32 from tailscale.com/ipn/ipnlocal
hash/crc32 from compress/gzip+
hash/fnv from tailscale.com/wgengine/magicsock+
hash/maphash from go4.org/mem
@@ -463,7 +427,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
io/fs from crypto/x509+
io/ioutil from github.com/godbus/dbus/v5+
log from expvar+
log/internal from log
LD log/syslog from tailscale.com/ssh/tailssh
math from compress/flate+
math/big from crypto/dsa+

View File

@@ -18,7 +18,7 @@ import (
func configureTaildrop(logf logger.Logf, lb *ipnlocal.LocalBackend) {
dg := distro.Get()
switch dg {
case distro.Synology, distro.TrueNAS, distro.QNAP, distro.Unraid:
case distro.Synology, distro.TrueNAS, distro.QNAP:
// See if they have a "Taildrop" share.
// See https://github.com/tailscale/tailscale/issues/2179#issuecomment-982821319
path, err := findTaildropDir(dg)
@@ -42,8 +42,6 @@ func findTaildropDir(dg distro.Distro) (string, error) {
return findTrueNASTaildropDir(name)
case distro.QNAP:
return findQnapTaildropDir(name)
case distro.Unraid:
return findUnraidTaildropDir(name)
}
return "", fmt.Errorf("%s is an unsupported distro for Taildrop dir", dg)
}
@@ -105,25 +103,3 @@ func findQnapTaildropDir(name string) (string, error) {
}
return "", fmt.Errorf("shared folder %q not found", name)
}
// findUnraidTaildropDir looks for a directory linked at
// /var/lib/tailscale/Taildrop. This is a symlink to the
// path specified by the user in the Unraid Web UI
func findUnraidTaildropDir(name string) (string, error) {
dir := fmt.Sprintf("/var/lib/tailscale/%s", name)
_, err := os.Stat(dir)
if err != nil {
return "", fmt.Errorf("symlink %q not found", name)
}
fullpath, err := filepath.EvalSymlinks(dir)
if err != nil {
return "", fmt.Errorf("symlink %q to shared folder not valid", name)
}
fi, err := os.Stat(fullpath)
if err == nil && fi.IsDir() {
return dir, nil // return the symlink
}
return "", fmt.Errorf("shared folder %q not found", name)
}

View File

@@ -39,28 +39,25 @@ import (
"tailscale.com/logtail"
"tailscale.com/net/dns"
"tailscale.com/net/dnsfallback"
"tailscale.com/net/netmon"
"tailscale.com/net/netns"
"tailscale.com/net/proxymux"
"tailscale.com/net/socks5"
"tailscale.com/net/tsdial"
"tailscale.com/net/tshttpproxy"
"tailscale.com/net/tstun"
"tailscale.com/paths"
"tailscale.com/safesocket"
"tailscale.com/smallzstd"
"tailscale.com/syncs"
"tailscale.com/tsd"
"tailscale.com/tsweb/varz"
"tailscale.com/tsweb"
"tailscale.com/types/flagtype"
"tailscale.com/types/logger"
"tailscale.com/types/logid"
"tailscale.com/util/clientmetric"
"tailscale.com/util/multierr"
"tailscale.com/util/osshare"
"tailscale.com/version"
"tailscale.com/version/distro"
"tailscale.com/wgengine"
"tailscale.com/wgengine/monitor"
"tailscale.com/wgengine/netstack"
"tailscale.com/wgengine/router"
)
@@ -330,19 +327,7 @@ var logPol *logpolicy.Policy
var debugMux *http.ServeMux
func run() error {
var logf logger.Logf = log.Printf
sys := new(tsd.System)
netMon, err := netmon.New(func(format string, args ...any) {
logf(format, args...)
})
if err != nil {
return fmt.Errorf("netmon.New: %w", err)
}
sys.Set(netMon)
pol := logpolicy.New(logtail.CollectionNode, netMon, nil /* use log.Printf */)
pol := logpolicy.New(logtail.CollectionNode)
pol.SetVerbosityLevel(args.verbose)
logPol = pol
defer func() {
@@ -366,6 +351,7 @@ func run() error {
return nil
}
var logf logger.Logf = log.Printf
if envknob.Bool("TS_DEBUG_MEMORY") {
logf = logger.RusagePrefixLog(logf)
}
@@ -391,10 +377,11 @@ func run() error {
debugMux = newDebugMux()
}
return startIPNServer(context.Background(), logf, pol.PublicID, sys)
logid := pol.PublicID.String()
return startIPNServer(context.Background(), logf, logid)
}
func startIPNServer(ctx context.Context, logf logger.Logf, logID logid.PublicID, sys *tsd.System) error {
func startIPNServer(ctx context.Context, logf logger.Logf, logid string) error {
ln, err := safesocket.Listen(args.socketpath)
if err != nil {
return fmt.Errorf("safesocket.Listen: %v", err)
@@ -420,7 +407,7 @@ func startIPNServer(ctx context.Context, logf logger.Logf, logID logid.PublicID,
}
}()
srv := ipnserver.New(logf, logID, sys.NetMon.Get())
srv := ipnserver.New(logf, logid)
if debugMux != nil {
debugMux.HandleFunc("/debug/ipn", srv.ServeHTMLStatus)
}
@@ -438,7 +425,7 @@ func startIPNServer(ctx context.Context, logf logger.Logf, logID logid.PublicID,
return
}
}
lb, err := getLocalBackend(ctx, logf, logID, sys)
lb, err := getLocalBackend(ctx, logf, logid)
if err == nil {
logf("got LocalBackend in %v", time.Since(t0).Round(time.Millisecond))
srv.SetLocalBackend(lb)
@@ -462,28 +449,35 @@ func startIPNServer(ctx context.Context, logf logger.Logf, logID logid.PublicID,
return nil
}
func getLocalBackend(ctx context.Context, logf logger.Logf, logID logid.PublicID, sys *tsd.System) (_ *ipnlocal.LocalBackend, retErr error) {
func getLocalBackend(ctx context.Context, logf logger.Logf, logid string) (_ *ipnlocal.LocalBackend, retErr error) {
linkMon, err := monitor.New(logf)
if err != nil {
return nil, fmt.Errorf("monitor.New: %w", err)
}
if logPol != nil {
logPol.Logtail.SetNetMon(sys.NetMon.Get())
logPol.Logtail.SetLinkMonitor(linkMon)
}
socksListener, httpProxyListener := mustStartProxyListeners(args.socksAddr, args.httpProxyAddr)
dialer := &tsdial.Dialer{Logf: logf} // mutated below (before used)
sys.Set(dialer)
onlyNetstack, err := createEngine(logf, sys)
e, onlyNetstack, err := createEngine(logf, linkMon, dialer)
if err != nil {
return nil, fmt.Errorf("createEngine: %w", err)
}
if _, ok := e.(wgengine.ResolvingEngine).GetResolver(); !ok {
panic("internal error: exit node resolver not wired up")
}
if debugMux != nil {
if ms, ok := sys.MagicSock.GetOK(); ok {
debugMux.HandleFunc("/debug/magicsock", ms.ServeHTTPDebug)
if ig, ok := e.(wgengine.InternalsGetter); ok {
if _, mc, _, ok := ig.GetInternals(); ok {
debugMux.HandleFunc("/debug/magicsock", mc.ServeHTTPDebug)
}
}
go runDebugServer(debugMux, args.debug)
}
ns, err := newNetstack(logf, sys)
ns, err := newNetstack(logf, dialer, e)
if err != nil {
return nil, fmt.Errorf("newNetstack: %w", err)
}
@@ -491,7 +485,6 @@ func getLocalBackend(ctx context.Context, logf logger.Logf, logID logid.PublicID
ns.ProcessSubnets = onlyNetstack || handleSubnetsInNetstack()
if onlyNetstack {
e := sys.Engine.Get()
dialer.UseNetstackForIP = func(ip netip.Addr) bool {
_, ok := e.PeerForIP(ip)
return ok
@@ -501,13 +494,11 @@ func getLocalBackend(ctx context.Context, logf logger.Logf, logID logid.PublicID
}
}
if socksListener != nil || httpProxyListener != nil {
var addrs []string
if httpProxyListener != nil {
hs := &http.Server{Handler: httpProxyHandler(dialer.UserDial)}
go func() {
log.Fatalf("HTTP proxy exited: %v", hs.Serve(httpProxyListener))
}()
addrs = append(addrs, httpProxyListener.Addr().String())
}
if socksListener != nil {
ss := &socks5.Server{
@@ -517,20 +508,19 @@ func getLocalBackend(ctx context.Context, logf logger.Logf, logID logid.PublicID
go func() {
log.Fatalf("SOCKS5 server exited: %v", ss.Serve(socksListener))
}()
addrs = append(addrs, socksListener.Addr().String())
}
tshttpproxy.SetSelfProxy(addrs...)
}
e = wgengine.NewWatchdog(e)
opts := ipnServerOpts()
store, err := store.New(logf, statePathOrDefault())
if err != nil {
return nil, fmt.Errorf("store.New: %w", err)
}
sys.Set(store)
lb, err := ipnlocal.NewLocalBackend(logf, logID, sys, opts.LoginFlags)
lb, err := ipnlocal.NewLocalBackend(logf, logid, store, dialer, e, opts.LoginFlags)
if err != nil {
return nil, fmt.Errorf("ipnlocal.NewLocalBackend: %w", err)
}
@@ -539,7 +529,7 @@ func getLocalBackend(ctx context.Context, logf logger.Logf, logID logid.PublicID
lb.SetLogFlusher(logPol.Logtail.StartFlush)
}
if root := lb.TailscaleVarRoot(); root != "" {
dnsfallback.SetCachePath(filepath.Join(root, "derpmap.cached.json"), logf)
dnsfallback.SetCachePath(filepath.Join(root, "derpmap.cached.json"))
}
lb.SetDecompressor(func() (controlclient.Decompressor, error) {
return smallzstd.NewDecoder(nil)
@@ -556,21 +546,21 @@ func getLocalBackend(ctx context.Context, logf logger.Logf, logID logid.PublicID
//
// onlyNetstack is true if the user has explicitly requested that we use netstack
// for all networking.
func createEngine(logf logger.Logf, sys *tsd.System) (onlyNetstack bool, err error) {
func createEngine(logf logger.Logf, linkMon *monitor.Mon, dialer *tsdial.Dialer) (e wgengine.Engine, onlyNetstack bool, err error) {
if args.tunname == "" {
return false, errors.New("no --tun value specified")
return nil, false, errors.New("no --tun value specified")
}
var errs []error
for _, name := range strings.Split(args.tunname, ",") {
logf("wgengine.NewUserspaceEngine(tun %q) ...", name)
onlyNetstack, err = tryEngine(logf, sys, name)
e, onlyNetstack, err = tryEngine(logf, linkMon, dialer, name)
if err == nil {
return onlyNetstack, nil
return e, onlyNetstack, nil
}
logf("wgengine.NewUserspaceEngine(tun %q) error: %v", name, err)
errs = append(errs, err)
}
return false, multierr.New(errs...)
return nil, false, multierr.New(errs...)
}
// handleSubnetsInNetstack reports whether netstack should handle subnet routers
@@ -595,23 +585,21 @@ func handleSubnetsInNetstack() bool {
var tstunNew = tstun.New
func tryEngine(logf logger.Logf, sys *tsd.System, name string) (onlyNetstack bool, err error) {
func tryEngine(logf logger.Logf, linkMon *monitor.Mon, dialer *tsdial.Dialer, name string) (e wgengine.Engine, onlyNetstack bool, err error) {
conf := wgengine.Config{
ListenPort: args.port,
NetMon: sys.NetMon.Get(),
Dialer: sys.Dialer.Get(),
SetSubsystem: sys.Set,
ListenPort: args.port,
LinkMonitor: linkMon,
Dialer: dialer,
}
onlyNetstack = name == "userspace-networking"
netstackSubnetRouter := onlyNetstack // but mutated later on some platforms
netns.SetEnabled(!onlyNetstack)
if args.birdSocketPath != "" && createBIRDClient != nil {
log.Printf("Connecting to BIRD at %s ...", args.birdSocketPath)
conf.BIRDClient, err = createBIRDClient(args.birdSocketPath)
if err != nil {
return false, fmt.Errorf("createBIRDClient: %w", err)
return nil, false, fmt.Errorf("createBIRDClient: %w", err)
}
}
if onlyNetstack {
@@ -624,55 +612,44 @@ func tryEngine(logf logger.Logf, sys *tsd.System, name string) (onlyNetstack boo
// TODO(bradfitz): add a Synology-specific DNS manager.
conf.DNS, err = dns.NewOSConfigurator(logf, "") // empty interface name
if err != nil {
return false, fmt.Errorf("dns.NewOSConfigurator: %w", err)
return nil, false, fmt.Errorf("dns.NewOSConfigurator: %w", err)
}
}
} else {
dev, devName, err := tstunNew(logf, name)
if err != nil {
tstun.Diagnose(logf, name, err)
return false, fmt.Errorf("tstun.New(%q): %w", name, err)
return nil, false, fmt.Errorf("tstun.New(%q): %w", name, err)
}
conf.Tun = dev
if strings.HasPrefix(name, "tap:") {
conf.IsTAP = true
e, err := wgengine.NewUserspaceEngine(logf, conf)
if err != nil {
return false, err
}
sys.Set(e)
return false, err
return e, false, err
}
r, err := router.New(logf, dev, sys.NetMon.Get())
r, err := router.New(logf, dev, linkMon)
if err != nil {
dev.Close()
return false, fmt.Errorf("creating router: %w", err)
return nil, false, fmt.Errorf("creating router: %w", err)
}
d, err := dns.NewOSConfigurator(logf, devName)
if err != nil {
dev.Close()
r.Close()
return false, fmt.Errorf("dns.NewOSConfigurator: %w", err)
return nil, false, fmt.Errorf("dns.NewOSConfigurator: %w", err)
}
conf.DNS = d
conf.Router = r
if handleSubnetsInNetstack() {
conf.Router = netstack.NewSubnetRouterWrapper(conf.Router)
netstackSubnetRouter = true
}
sys.Set(conf.Router)
}
e, err := wgengine.NewUserspaceEngine(logf, conf)
e, err = wgengine.NewUserspaceEngine(logf, conf)
if err != nil {
return onlyNetstack, err
return nil, onlyNetstack, err
}
e = wgengine.NewWatchdog(e)
sys.Set(e)
sys.NetstackRouter.Set(netstackSubnetRouter)
return onlyNetstack, nil
return e, onlyNetstack, nil
}
func newDebugMux() *http.ServeMux {
@@ -688,7 +665,7 @@ func newDebugMux() *http.ServeMux {
func servePrometheusMetrics(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain")
varz.Handler(w, r)
tsweb.VarzHandler(w, r)
clientmetric.WritePrometheusExpositionFormat(w)
}
@@ -702,8 +679,12 @@ func runDebugServer(mux *http.ServeMux, addr string) {
}
}
func newNetstack(logf logger.Logf, sys *tsd.System) (*netstack.Impl, error) {
return netstack.Create(logf, sys.Tun.Get(), sys.Engine.Get(), sys.MagicSock.Get(), sys.Dialer.Get(), sys.DNSManager.Get())
func newNetstack(logf logger.Logf, dialer *tsdial.Dialer, e wgengine.Engine) (*netstack.Impl, error) {
tunDev, magicConn, dns, ok := e.(wgengine.InternalsGetter).GetInternals()
if !ok {
return nil, fmt.Errorf("%T is not a wgengine.InternalsGetter", e)
}
return netstack.Create(logf, tunDev, e, magicConn, dialer, dns)
}
// mustStartProxyListeners creates listeners for local SOCKS and HTTP

View File

@@ -3,32 +3,10 @@
package main // import "tailscale.com/cmd/tailscaled"
import (
"testing"
"tailscale.com/tstest/deptest"
)
import "testing"
func TestNothing(t *testing.T) {
// This test does nothing on purpose, so we can run
// GODEBUG=memprofilerate=1 go test -v -run=Nothing -memprofile=prof.mem
// without any errors about no matching tests.
}
func TestDeps(t *testing.T) {
deptest.DepChecker{
GOOS: "darwin",
GOARCH: "arm64",
BadDeps: map[string]string{
"gvisor.dev/gvisor/pkg/hostarch": "will crash on non-4K page sizes; see https://github.com/tailscale/tailscale/issues/8658",
},
}.Check(t)
deptest.DepChecker{
GOOS: "linux",
GOARCH: "arm64",
BadDeps: map[string]string{
"gvisor.dev/gvisor/pkg/hostarch": "will crash on non-4K page sizes; see https://github.com/tailscale/tailscale/issues/8658",
},
}.Check(t)
}

View File

@@ -45,12 +45,8 @@ import (
"tailscale.com/logpolicy"
"tailscale.com/logtail/backoff"
"tailscale.com/net/dns"
"tailscale.com/net/netmon"
"tailscale.com/net/tstun"
"tailscale.com/tsd"
"tailscale.com/types/logger"
"tailscale.com/types/logid"
"tailscale.com/util/osdiag"
"tailscale.com/util/winutil"
"tailscale.com/version"
"tailscale.com/wf"
@@ -127,10 +123,6 @@ var syslogf logger.Logf = logger.Discard
// At this point we're still the parent process that
// Windows started.
func runWindowsService(pol *logpolicy.Policy) error {
go func() {
osdiag.LogSupportInfo(logger.WithPrefix(log.Printf, "Support Info: "), osdiag.LogSupportInfoReasonStartup)
}()
if winutil.GetPolicyInteger("LogSCMInteractions", 0) != 0 {
syslog, err := eventlog.Open(serviceName)
if err == nil {
@@ -270,13 +262,13 @@ func beWindowsSubprocess() bool {
if len(os.Args) != 3 || os.Args[1] != "/subproc" {
return false
}
logID := os.Args[2]
logid := os.Args[2]
// Remove the date/time prefix; the logtail + file loggers add it.
log.SetFlags(0)
log.Printf("Program starting: v%v: %#v", version.Long(), os.Args)
log.Printf("subproc mode: logid=%v", logID)
log.Printf("subproc mode: logid=%v", logid)
if err := envknob.ApplyDiskConfigError(); err != nil {
log.Printf("Error reading environment config: %v", err)
}
@@ -298,15 +290,7 @@ func beWindowsSubprocess() bool {
}
}()
sys := new(tsd.System)
netMon, err := netmon.New(log.Printf)
if err != nil {
log.Fatalf("Could not create netMon: %v", err)
}
sys.Set(netMon)
publicLogID, _ := logid.ParsePublicID(logID)
err = startIPNServer(ctx, log.Printf, publicLogID, sys)
err := startIPNServer(ctx, log.Printf, logid)
if err != nil {
log.Fatalf("ipnserver: %v", err)
}

View File

@@ -7,20 +7,16 @@
package flakytest
import (
"fmt"
"os"
"regexp"
"testing"
)
// FlakyTestLogMessage is a sentinel value that is printed to stderr when a
// flaky test is marked. This is used by cmd/testwrapper to detect flaky tests
// and retry them.
const FlakyTestLogMessage = "flakytest: this is a known flaky test"
// FlakeAttemptEnv is an environment variable that is set by cmd/testwrapper
// when a flaky test is retried. It contains the attempt number, starting at 1.
const FlakeAttemptEnv = "TS_TESTWRAPPER_ATTEMPT"
// InTestWrapper returns whether or not this binary is running under our test
// wrapper.
func InTestWrapper() bool {
return os.Getenv("TS_IN_TESTWRAPPER") != ""
}
var issueRegexp = regexp.MustCompile(`\Ahttps://github\.com/tailscale/[a-zA-Z0-9_.-]+/issues/\d+\z`)
@@ -34,6 +30,16 @@ func Mark(t testing.TB, issue string) {
t.Fatalf("bad issue format: %q", issue)
}
fmt.Fprintln(os.Stderr, FlakyTestLogMessage) // sentinel value for testwrapper
t.Logf("flakytest: issue tracking this flaky test: %s", issue)
if !InTestWrapper() {
return
}
t.Cleanup(func() {
if t.Failed() {
t.Logf("flakytest: signaling test wrapper to retry test")
// Signal to test wrapper that we should restart.
os.Exit(123)
}
})
}

View File

@@ -3,10 +3,7 @@
package flakytest
import (
"os"
"testing"
)
import "testing"
func TestIssueFormat(t *testing.T) {
testCases := []struct {
@@ -27,17 +24,3 @@ func TestIssueFormat(t *testing.T) {
}
}
}
// TestFlakeRun is a test that fails when run in the testwrapper
// for the first time, but succeeds on the second run.
// It's used to test whether the testwrapper retries flaky tests.
func TestFlakeRun(t *testing.T) {
Mark(t, "https://github.com/tailscale/tailscale/issues/0") // random issue
e := os.Getenv(FlakeAttemptEnv)
if e == "" {
t.Skip("not running in testwrapper")
}
if e == "1" {
t.Fatal("First run in testwrapper, failing so that test is retried. This is expected.")
}
}

View File

@@ -1,288 +1,62 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// testwrapper is a wrapper for retrying flaky tests. It is an alternative to
// `go test` and re-runs failed marked flaky tests (using the flakytest pkg). It
// takes different arguments than go test and requires the first positional
// argument to be the pattern to test.
// testwrapper is a wrapper for retrying flaky tests, using the -exec flag of
// 'go test'. Tests that are flaky can use the 'flakytest' subpackage to mark
// themselves as flaky and be retried on failure.
package main
import (
"bytes"
"context"
"encoding/json"
"errors"
"flag"
"fmt"
"io"
"log"
"os"
"os/exec"
"sort"
"strings"
"time"
"golang.org/x/exp/maps"
"tailscale.com/cmd/testwrapper/flakytest"
)
const maxAttempts = 3
type testAttempt struct {
name testName
outcome string // "pass", "fail", "skip"
logs bytes.Buffer
isMarkedFlaky bool // set if the test is marked as flaky
pkgFinished bool
}
type testName struct {
pkg string // "tailscale.com/types/key"
name string // "TestFoo"
}
type packageTests struct {
// pattern is the package pattern to run.
// Must be a single pattern, not a list of patterns.
pattern string // "./...", "./types/key"
// tests is a list of tests to run. If empty, all tests in the package are
// run.
tests []string // ["TestFoo", "TestBar"]
}
type goTestOutput struct {
Time time.Time
Action string
Package string
Test string
Output string
}
var debug = os.Getenv("TS_TESTWRAPPER_DEBUG") != ""
// runTests runs the tests in pt and sends the results on ch. It sends a
// testAttempt for each test and a final testAttempt per pkg with pkgFinished
// set to true.
// It calls close(ch) when it's done.
func runTests(ctx context.Context, attempt int, pt *packageTests, otherArgs []string, ch chan<- *testAttempt) {
defer close(ch)
args := []string{"test", "-json", pt.pattern}
args = append(args, otherArgs...)
if len(pt.tests) > 0 {
runArg := strings.Join(pt.tests, "|")
args = append(args, "-run", runArg)
}
if debug {
fmt.Println("running", strings.Join(args, " "))
}
cmd := exec.CommandContext(ctx, "go", args...)
r, err := cmd.StdoutPipe()
if err != nil {
log.Printf("error creating stdout pipe: %v", err)
}
defer r.Close()
cmd.Stderr = os.Stderr
cmd.Env = os.Environ()
cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%d", flakytest.FlakeAttemptEnv, attempt))
if err := cmd.Start(); err != nil {
log.Printf("error starting test: %v", err)
os.Exit(1)
}
done := make(chan struct{})
go func() {
defer close(done)
cmd.Wait()
}()
jd := json.NewDecoder(r)
resultMap := make(map[testName]*testAttempt)
for {
var goOutput goTestOutput
if err := jd.Decode(&goOutput); err != nil {
if errors.Is(err, io.EOF) || errors.Is(err, os.ErrClosed) {
break
}
// `go test -json` outputs invalid JSON when a build fails.
// In that case, discard the the output and start reading again.
// The build error will be printed to stderr.
// See: https://github.com/golang/go/issues/35169
if _, ok := err.(*json.SyntaxError); ok {
jd = json.NewDecoder(r)
continue
}
panic(err)
}
if goOutput.Test == "" {
switch goOutput.Action {
case "fail", "pass", "skip":
ch <- &testAttempt{
name: testName{
pkg: goOutput.Package,
},
outcome: goOutput.Action,
pkgFinished: true,
}
}
continue
}
name := testName{
pkg: goOutput.Package,
name: goOutput.Test,
}
if test, _, isSubtest := strings.Cut(goOutput.Test, "/"); isSubtest {
name.name = test
if goOutput.Action == "output" {
resultMap[name].logs.WriteString(goOutput.Output)
}
continue
}
switch goOutput.Action {
case "start":
// ignore
case "run":
resultMap[name] = &testAttempt{
name: name,
}
case "skip", "pass", "fail":
resultMap[name].outcome = goOutput.Action
ch <- resultMap[name]
case "output":
if strings.TrimSpace(goOutput.Output) == flakytest.FlakyTestLogMessage {
resultMap[name].isMarkedFlaky = true
} else {
resultMap[name].logs.WriteString(goOutput.Output)
}
}
}
<-done
}
const (
retryStatus = 123
maxIterations = 3
)
func main() {
ctx := context.Background()
debug := os.Getenv("TS_TESTWRAPPER_DEBUG") != ""
// We only need to parse the -v flag to figure out whether to print the logs
// for a test. We don't need to parse any other flags, so we just use the
// flag package to parse the -v flag and then pass the rest of the args
// through to 'go test'.
// We run `go test -json` which returns the same information as `go test -v`,
// but in a machine-readable format. So this flag is only for testwrapper's
// output.
v := flag.Bool("v", false, "verbose")
flag.Usage = func() {
fmt.Println("usage: testwrapper [testwrapper-flags] [pattern] [build/test flags & test binary flags]")
fmt.Println()
fmt.Println("testwrapper-flags:")
flag.CommandLine.PrintDefaults()
fmt.Println()
fmt.Println("examples:")
fmt.Println("\ttestwrapper -v ./... -count=1")
fmt.Println("\ttestwrapper ./pkg/foo -run TestBar -count=1")
fmt.Println()
fmt.Println("Unlike 'go test', testwrapper requires a package pattern as the first positional argument and only supports a single pattern.")
}
flag.Parse()
args := flag.Args()
if len(args) < 1 || strings.HasPrefix(args[0], "-") {
fmt.Println("no pattern specified")
flag.Usage()
os.Exit(1)
} else if len(args) > 1 && !strings.HasPrefix(args[1], "-") {
fmt.Println("expected single pattern")
flag.Usage()
os.Exit(1)
}
pattern, otherArgs := args[0], args[1:]
type nextRun struct {
tests []*packageTests
attempt int
log.SetPrefix("testwrapper: ")
if !debug {
log.SetFlags(0)
}
toRun := []*nextRun{
{
tests: []*packageTests{{pattern: pattern}},
attempt: 1,
},
}
printPkgOutcome := func(pkg, outcome string, attempt int) {
if outcome == "skip" {
fmt.Printf("?\t%s [skipped/no tests] \n", pkg)
for i := 1; i <= maxIterations; i++ {
if i > 1 {
log.Printf("retrying flaky tests (%d of %d)", i, maxIterations)
}
cmd := exec.CommandContext(ctx, os.Args[1], os.Args[2:]...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Env = append(os.Environ(), "TS_IN_TESTWRAPPER=1")
err := cmd.Run()
if err == nil {
return
}
if outcome == "pass" {
outcome = "ok"
}
if outcome == "fail" {
outcome = "FAIL"
}
if attempt > 1 {
fmt.Printf("%s\t%s [attempt=%d]\n", outcome, pkg, attempt)
return
}
fmt.Printf("%s\t%s\n", outcome, pkg)
}
for len(toRun) > 0 {
var thisRun *nextRun
thisRun, toRun = toRun[0], toRun[1:]
if thisRun.attempt >= maxAttempts {
fmt.Println("max attempts reached")
os.Exit(1)
}
if thisRun.attempt > 1 {
fmt.Printf("\n\nAttempt #%d: Retrying flaky tests:\n\n", thisRun.attempt)
}
failed := false
toRetry := make(map[string][]string) // pkg -> tests to retry
for _, pt := range thisRun.tests {
ch := make(chan *testAttempt)
go runTests(ctx, thisRun.attempt, pt, otherArgs, ch)
for tr := range ch {
if tr.pkgFinished {
printPkgOutcome(tr.name.pkg, tr.outcome, thisRun.attempt)
continue
}
if *v || tr.outcome == "fail" {
io.Copy(os.Stdout, &tr.logs)
}
if tr.outcome != "fail" {
continue
}
if tr.isMarkedFlaky {
toRetry[tr.name.pkg] = append(toRetry[tr.name.pkg], tr.name.name)
} else {
failed = true
}
var exitErr *exec.ExitError
if !errors.As(err, &exitErr) {
if debug {
log.Printf("error isn't an ExitError")
}
}
if failed {
fmt.Println("\n\nNot retrying flaky tests because non-flaky tests failed.")
os.Exit(1)
}
if len(toRetry) == 0 {
continue
if code := exitErr.ExitCode(); code != retryStatus {
if debug {
log.Printf("code (%d) != retryStatus (%d)", code, retryStatus)
}
os.Exit(code)
}
pkgs := maps.Keys(toRetry)
sort.Strings(pkgs)
nextRun := &nextRun{
attempt: thisRun.attempt + 1,
}
for _, pkg := range pkgs {
tests := toRetry[pkg]
sort.Strings(tests)
nextRun.tests = append(nextRun.tests, &packageTests{
pattern: pkg,
tests: tests,
})
}
toRun = append(toRun, nextRun)
}
log.Printf("test did not pass in %d iterations", maxIterations)
os.Exit(1)
}

View File

@@ -71,9 +71,6 @@ func commonSetup(dev bool) (*esbuild.BuildOptions, error) {
}
func findRepoRoot() (string, error) {
if *rootDir != "" {
return *rootDir, nil
}
cwd, err := os.Getwd()
if err != nil {
return "", err

View File

@@ -23,7 +23,6 @@ var (
yarnPath = flag.String("yarnpath", "../../tool/yarn", "path yarn executable used to install JavaScript dependencies")
fastCompression = flag.Bool("fast-compression", false, "Use faster compression when building, to speed up build time. Meant to iterative/debugging use only.")
devControl = flag.String("dev-control", "", "URL of a development control server to be used with dev. If provided without specifying dev, an error will be returned.")
rootDir = flag.String("rootdir", "", "Root directory of repo. If not specified, will be inferred from the cwd.")
)
func main() {

View File

@@ -37,7 +37,6 @@ import (
"tailscale.com/safesocket"
"tailscale.com/smallzstd"
"tailscale.com/tailcfg"
"tailscale.com/tsd"
"tailscale.com/wgengine"
"tailscale.com/wgengine/netstack"
"tailscale.com/words"
@@ -47,7 +46,7 @@ import (
var ControlURL = ipn.DefaultControlURL
func main() {
js.Global().Set("newIPN", js.FuncOf(func(this js.Value, args []js.Value) any {
js.Global().Set("newIPN", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
if len(args) != 1 {
log.Fatal("Usage: newIPN(config)")
return nil
@@ -97,19 +96,19 @@ func newIPN(jsConfig js.Value) map[string]any {
logtail := logtail.NewLogger(c, log.Printf)
logf := logtail.Logf
sys := new(tsd.System)
sys.Set(store)
dialer := &tsdial.Dialer{Logf: logf}
eng, err := wgengine.NewUserspaceEngine(logf, wgengine.Config{
Dialer: dialer,
SetSubsystem: sys.Set,
Dialer: dialer,
})
if err != nil {
log.Fatal(err)
}
sys.Set(eng)
ns, err := netstack.Create(logf, sys.Tun.Get(), eng, sys.MagicSock.Get(), dialer, sys.DNSManager.Get())
tunDev, magicConn, dnsManager, ok := eng.(wgengine.InternalsGetter).GetInternals()
if !ok {
log.Fatalf("%T is not a wgengine.InternalsGetter", eng)
}
ns, err := netstack.Create(logf, tunDev, eng, magicConn, dialer, dnsManager)
if err != nil {
log.Fatalf("netstack.Create: %v", err)
}
@@ -122,11 +121,10 @@ func newIPN(jsConfig js.Value) map[string]any {
dialer.NetstackDialTCP = func(ctx context.Context, dst netip.AddrPort) (net.Conn, error) {
return ns.DialContextTCP(ctx, dst)
}
sys.NetstackRouter.Set(true)
logid := lpc.PublicID
srv := ipnserver.New(logf, logid, nil /* no netMon */)
lb, err := ipnlocal.NewLocalBackend(logf, logid, sys, controlclient.LoginEphemeral)
logid := lpc.PublicID.String()
srv := ipnserver.New(logf, logid)
lb, err := ipnlocal.NewLocalBackend(logf, logid, store, dialer, eng, controlclient.LoginEphemeral)
if err != nil {
log.Fatalf("ipnlocal.NewLocalBackend: %v", err)
}
@@ -148,7 +146,7 @@ func newIPN(jsConfig js.Value) map[string]any {
}
return map[string]any{
"run": js.FuncOf(func(this js.Value, args []js.Value) any {
"run": js.FuncOf(func(this js.Value, args []js.Value) interface{} {
if len(args) != 1 {
log.Fatal(`Usage: run({
notifyState(state: int): void,
@@ -161,7 +159,7 @@ func newIPN(jsConfig js.Value) map[string]any {
jsIPN.run(args[0])
return nil
}),
"login": js.FuncOf(func(this js.Value, args []js.Value) any {
"login": js.FuncOf(func(this js.Value, args []js.Value) interface{} {
if len(args) != 0 {
log.Printf("Usage: login()")
return nil
@@ -169,7 +167,7 @@ func newIPN(jsConfig js.Value) map[string]any {
jsIPN.login()
return nil
}),
"logout": js.FuncOf(func(this js.Value, args []js.Value) any {
"logout": js.FuncOf(func(this js.Value, args []js.Value) interface{} {
if len(args) != 0 {
log.Printf("Usage: logout()")
return nil
@@ -177,7 +175,7 @@ func newIPN(jsConfig js.Value) map[string]any {
jsIPN.logout()
return nil
}),
"ssh": js.FuncOf(func(this js.Value, args []js.Value) any {
"ssh": js.FuncOf(func(this js.Value, args []js.Value) interface{} {
if len(args) != 3 {
log.Printf("Usage: ssh(hostname, userName, termConfig)")
return nil
@@ -187,7 +185,7 @@ func newIPN(jsConfig js.Value) map[string]any {
args[1].String(),
args[2])
}),
"fetch": js.FuncOf(func(this js.Value, args []js.Value) any {
"fetch": js.FuncOf(func(this js.Value, args []js.Value) interface{} {
if len(args) != 1 {
log.Printf("Usage: fetch(url)")
return nil
@@ -336,10 +334,10 @@ func (i *jsIPN) ssh(host, username string, termConfig js.Value) map[string]any {
go jsSSHSession.Run()
return map[string]any{
"close": js.FuncOf(func(this js.Value, args []js.Value) any {
"close": js.FuncOf(func(this js.Value, args []js.Value) interface{} {
return jsSSHSession.Close() != nil
}),
"resize": js.FuncOf(func(this js.Value, args []js.Value) any {
"resize": js.FuncOf(func(this js.Value, args []js.Value) interface{} {
rows := args[0].Int()
cols := args[1].Int()
return jsSSHSession.Resize(rows, cols) != nil
@@ -428,7 +426,7 @@ func (s *jsSSHSession) Run() {
session.Stdout = termWriter{writeFn}
session.Stderr = termWriter{writeFn}
setReadFn.Invoke(js.FuncOf(func(this js.Value, args []js.Value) any {
setReadFn.Invoke(js.FuncOf(func(this js.Value, args []js.Value) interface{} {
input := args[0].String()
_, err := stdin.Write([]byte(input))
if err != nil {
@@ -498,7 +496,7 @@ func (i *jsIPN) fetch(url string) js.Value {
return map[string]any{
"status": res.StatusCode,
"statusText": res.Status,
"text": js.FuncOf(func(this js.Value, args []js.Value) any {
"text": js.FuncOf(func(this js.Value, args []js.Value) interface{} {
return makePromise(func() (any, error) {
defer res.Body.Close()
buf := new(bytes.Buffer)
@@ -604,7 +602,7 @@ func generateHostname() string {
// f is run on a goroutine and its return value is used to resolve the promise
// (or reject it if an error is returned).
func makePromise(f func() (any, error)) js.Value {
handler := js.FuncOf(func(this js.Value, args []js.Value) any {
handler := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
resolve := args[0]
reject := args[1]
go func() {

View File

@@ -9,7 +9,7 @@ import (
"net/netip"
)
//go:generate go run tailscale.com/cmd/viewer --type=StructWithPtrs,StructWithoutPtrs,Map,StructWithSlices,OnlyGetClone,StructWithEmbedded --clone-only-type=OnlyGetClone
//go:generate go run tailscale.com/cmd/viewer --type=StructWithPtrs,StructWithoutPtrs,Map,StructWithSlices,OnlyGetClone --clone-only-type=OnlyGetClone
type StructWithoutPtrs struct {
Int int
@@ -61,8 +61,3 @@ type StructWithSlices struct {
type OnlyGetClone struct {
SinViewerPorFavor bool
}
type StructWithEmbedded struct {
A *StructWithPtrs
StructWithSlices
}

View File

@@ -211,22 +211,3 @@ func (src *OnlyGetClone) Clone() *OnlyGetClone {
var _OnlyGetCloneCloneNeedsRegeneration = OnlyGetClone(struct {
SinViewerPorFavor bool
}{})
// Clone makes a deep copy of StructWithEmbedded.
// The result aliases no memory with the original.
func (src *StructWithEmbedded) Clone() *StructWithEmbedded {
if src == nil {
return nil
}
dst := new(StructWithEmbedded)
*dst = *src
dst.A = src.A.Clone()
dst.StructWithSlices = *src.StructWithSlices.Clone()
return dst
}
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _StructWithEmbeddedCloneNeedsRegeneration = StructWithEmbedded(struct {
A *StructWithPtrs
StructWithSlices
}{})

View File

@@ -14,7 +14,7 @@ import (
"tailscale.com/types/views"
)
//go:generate go run tailscale.com/cmd/cloner -clonefunc=false -type=StructWithPtrs,StructWithoutPtrs,Map,StructWithSlices,OnlyGetClone,StructWithEmbedded
//go:generate go run tailscale.com/cmd/cloner -clonefunc=false -type=StructWithPtrs,StructWithoutPtrs,Map,StructWithSlices,OnlyGetClone
// View returns a readonly view of StructWithPtrs.
func (p *StructWithPtrs) View() StructWithPtrsView {
@@ -325,59 +325,3 @@ var _StructWithSlicesViewNeedsRegeneration = StructWithSlices(struct {
Prefixes []netip.Prefix
Data []byte
}{})
// View returns a readonly view of StructWithEmbedded.
func (p *StructWithEmbedded) View() StructWithEmbeddedView {
return StructWithEmbeddedView{ж: p}
}
// StructWithEmbeddedView provides a read-only view over StructWithEmbedded.
//
// Its methods should only be called if `Valid()` returns true.
type StructWithEmbeddedView struct {
// ж is the underlying mutable value, named with a hard-to-type
// character that looks pointy like a pointer.
// It is named distinctively to make you think of how dangerous it is to escape
// to callers. You must not let callers be able to mutate it.
ж *StructWithEmbedded
}
// Valid reports whether underlying value is non-nil.
func (v StructWithEmbeddedView) Valid() bool { return v.ж != nil }
// AsStruct returns a clone of the underlying value which aliases no memory with
// the original.
func (v StructWithEmbeddedView) AsStruct() *StructWithEmbedded {
if v.ж == nil {
return nil
}
return v.ж.Clone()
}
func (v StructWithEmbeddedView) MarshalJSON() ([]byte, error) { return json.Marshal(v.ж) }
func (v *StructWithEmbeddedView) UnmarshalJSON(b []byte) error {
if v.ж != nil {
return errors.New("already initialized")
}
if len(b) == 0 {
return nil
}
var x StructWithEmbedded
if err := json.Unmarshal(b, &x); err != nil {
return err
}
v.ж = &x
return nil
}
func (v StructWithEmbeddedView) A() StructWithPtrsView { return v.ж.A.View() }
func (v StructWithEmbeddedView) StructWithSlices() StructWithSlicesView {
return v.ж.StructWithSlices.View()
}
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _StructWithEmbeddedViewNeedsRegeneration = StructWithEmbedded(struct {
A *StructWithPtrs
StructWithSlices
}{})

View File

@@ -398,7 +398,7 @@ type maxMsgBuffer [maxMessageSize]byte
// bufPool holds the temporary buffers for Conn.Read & Write.
var bufPool = &sync.Pool{
New: func() any {
New: func() interface{} {
return new(maxMsgBuffer)
},
}

View File

@@ -15,7 +15,6 @@ import (
"tailscale.com/logtail/backoff"
"tailscale.com/net/sockstats"
"tailscale.com/tailcfg"
"tailscale.com/tstime"
"tailscale.com/types/empty"
"tailscale.com/types/key"
"tailscale.com/types/logger"
@@ -49,7 +48,7 @@ var _ Client = (*Auto)(nil)
// It's a concrete implementation of the Client interface.
type Auto struct {
direct *Direct // our interface to the server APIs
clock tstime.Clock
timeNow func() time.Time
logf logger.Logf
expiry *time.Time
closed bool
@@ -108,12 +107,12 @@ func NewNoStart(opts Options) (_ *Auto, err error) {
if opts.Logf == nil {
opts.Logf = func(fmt string, args ...any) {}
}
if opts.Clock == nil {
opts.Clock = tstime.StdClock{}
if opts.TimeNow == nil {
opts.TimeNow = time.Now
}
c := &Auto{
direct: direct,
clock: opts.Clock,
timeNow: opts.TimeNow,
logf: opts.Logf,
newMapCh: make(chan struct{}, 1),
quit: make(chan struct{}),
@@ -122,10 +121,10 @@ func NewNoStart(opts Options) (_ *Auto, err error) {
statusFunc: opts.Status,
}
c.authCtx, c.authCancel = context.WithCancel(context.Background())
c.authCtx = sockstats.WithSockStats(c.authCtx, sockstats.LabelControlClientAuto, opts.Logf)
c.authCtx = sockstats.WithSockStats(c.authCtx, sockstats.LabelControlClientAuto)
c.mapCtx, c.mapCancel = context.WithCancel(context.Background())
c.mapCtx = sockstats.WithSockStats(c.mapCtx, sockstats.LabelControlClientAuto, opts.Logf)
c.mapCtx = sockstats.WithSockStats(c.mapCtx, sockstats.LabelControlClientAuto)
c.unregisterHealthWatch = health.RegisterWatcher(direct.ReportHealthChange)
return c, nil
@@ -209,7 +208,7 @@ func (c *Auto) sendNewMapRequest() {
c.liteMapUpdateCancel = cancel
go func() {
defer cancel()
t0 := c.clock.Now()
t0 := time.Now()
err := c.direct.SendLiteMapUpdate(ctx)
d := time.Since(t0).Round(time.Millisecond)
@@ -245,7 +244,7 @@ func (c *Auto) cancelAuth() {
}
if !c.closed {
c.authCtx, c.authCancel = context.WithCancel(context.Background())
c.authCtx = sockstats.WithSockStats(c.authCtx, sockstats.LabelControlClientAuto, c.logf)
c.authCtx = sockstats.WithSockStats(c.authCtx, sockstats.LabelControlClientAuto)
}
c.mu.Unlock()
}
@@ -256,7 +255,7 @@ func (c *Auto) cancelMapLocked() {
}
if !c.closed {
c.mapCtx, c.mapCancel = context.WithCancel(context.Background())
c.mapCtx = sockstats.WithSockStats(c.mapCtx, sockstats.LabelControlClientAuto, c.logf)
c.mapCtx = sockstats.WithSockStats(c.mapCtx, sockstats.LabelControlClientAuto)
}
}
@@ -476,7 +475,7 @@ func (c *Auto) mapRoutine() {
}
continue
}
c.logf("[v1] mapRoutine: %s", c.state)
c.logf("mapRoutine: %s", c.state)
loggedIn := c.loggedIn
ctx := c.mapCtx
c.mu.Unlock()
@@ -489,7 +488,7 @@ func (c *Auto) mapRoutine() {
}
report := func(err error, msg string) {
c.logf("[v1] %s: %v", msg, err)
c.logf("%s: %v", msg, err)
err = fmt.Errorf("%s: %w", msg, err)
// don't send status updates for context errors,
// since context cancelation is always on purpose.
@@ -507,9 +506,9 @@ func (c *Auto) mapRoutine() {
select {
case <-ctx.Done():
c.logf("[v1] mapRoutine: context done.")
c.logf("mapRoutine: context done.")
case <-c.newMapCh:
c.logf("[v1] mapRoutine: new map needed while idle.")
c.logf("mapRoutine: new map needed while idle.")
}
} else {
// Be sure this is false when we're not inside
@@ -552,8 +551,6 @@ func (c *Auto) mapRoutine() {
if stillAuthed {
c.sendStatus("mapRoutine-got-netmap", nil, "", nm)
}
// Reset the backoff timer if we got a netmap.
bo.BackOff(ctx, nil)
})
health.SetInPollNetMap(false)
@@ -705,14 +702,14 @@ func (c *Auto) Logout(ctx context.Context) error {
c.mu.Unlock()
c.cancelAuth()
timer, timerChannel := c.clock.NewTimer(10 * time.Second)
timer := time.NewTimer(10 * time.Second)
defer timer.Stop()
select {
case err := <-errc:
return err
case <-ctx.Done():
return ctx.Err()
case <-timerChannel:
case <-timer.C:
return context.DeadlineExceeded
}
}
@@ -773,7 +770,7 @@ func (c *Auto) TestOnlySetAuthKey(authkey string) {
}
func (c *Auto) TestOnlyTimeNow() time.Time {
return c.clock.Now()
return c.timeNow()
}
// SetDNS sends the SetDNSRequest request to the control plane server,

View File

@@ -20,7 +20,7 @@ func dumpGoroutinesToURL(c *http.Client, targetURL string) {
zbuf := new(bytes.Buffer)
zw := gzip.NewWriter(zbuf)
zw.Write(goroutines.ScrubbedGoroutineDump(true))
zw.Write(goroutines.ScrubbedGoroutineDump())
zw.Close()
req, err := http.NewRequestWithContext(ctx, "PUT", targetURL, zbuf)

View File

@@ -37,7 +37,6 @@ import (
"tailscale.com/net/dnscache"
"tailscale.com/net/dnsfallback"
"tailscale.com/net/interfaces"
"tailscale.com/net/netmon"
"tailscale.com/net/netutil"
"tailscale.com/net/tlsdial"
"tailscale.com/net/tsdial"
@@ -45,7 +44,6 @@ import (
"tailscale.com/syncs"
"tailscale.com/tailcfg"
"tailscale.com/tka"
"tailscale.com/tstime"
"tailscale.com/types/key"
"tailscale.com/types/logger"
"tailscale.com/types/netmap"
@@ -56,20 +54,20 @@ import (
"tailscale.com/util/multierr"
"tailscale.com/util/singleflight"
"tailscale.com/util/systemd"
"tailscale.com/wgengine/monitor"
)
// Direct is the client that connects to a tailcontrol server for a node.
type Direct struct {
httpc *http.Client // HTTP client used to talk to tailcontrol
dialer *tsdial.Dialer
dnsCache *dnscache.Resolver
serverURL string // URL of the tailcontrol server
clock tstime.Clock
timeNow func() time.Time
lastPrintMap time.Time
newDecompressor func() (Decompressor, error)
keepAlive bool
logf logger.Logf
netMon *netmon.Monitor // or nil
linkMon *monitor.Mon // or nil
discoPubKey key.DiscoPublic
getMachinePrivKey func() (key.MachinePrivate, error)
debugFlags []string
@@ -106,8 +104,8 @@ type Options struct {
GetMachinePrivateKey func() (key.MachinePrivate, error) // returns the machine key to use
ServerURL string // URL of the tailcontrol server
AuthKey string // optional node auth key for auto registration
Clock tstime.Clock
Hostinfo *tailcfg.Hostinfo // non-nil passes ownership, nil means to use default using os.Hostname, etc
TimeNow func() time.Time // time.Now implementation used by Client
Hostinfo *tailcfg.Hostinfo // non-nil passes ownership, nil means to use default using os.Hostname, etc
DiscoPublicKey key.DiscoPublic
NewDecompressor func() (Decompressor, error)
KeepAlive bool
@@ -115,7 +113,7 @@ type Options struct {
HTTPTestClient *http.Client // optional HTTP client to use (for tests only)
NoiseTestClient *http.Client // optional HTTP client to use for noise RPCs (tests only)
DebugFlags []string // debug settings to send to control
NetMon *netmon.Monitor // optional network monitor
LinkMonitor *monitor.Mon // optional link monitor
PopBrowserURL func(url string) // optional func to open browser
OnClientVersion func(*tailcfg.ClientVersion) // optional func to inform GUI of client version status
OnControlTime func(time.Time) // optional func to notify callers of new time from control
@@ -192,8 +190,8 @@ func NewDirect(opts Options) (*Direct, error) {
if err != nil {
return nil, err
}
if opts.Clock == nil {
opts.Clock = tstime.StdClock{}
if opts.TimeNow == nil {
opts.TimeNow = time.Now
}
if opts.Logf == nil {
// TODO(apenwarr): remove this default and fail instead.
@@ -201,14 +199,6 @@ func NewDirect(opts Options) (*Direct, error) {
opts.Logf = log.Printf
}
dnsCache := &dnscache.Resolver{
Forward: dnscache.Get().Forward, // use default cache's forwarder
UseLastGood: true,
LookupIPFallback: dnsfallback.MakeLookupFunc(opts.Logf, opts.NetMon),
Logf: opts.Logf,
NetMon: opts.NetMon,
}
httpc := opts.HTTPTestClient
if httpc == nil && runtime.GOOS == "js" {
// In js/wasm, net/http.Transport (as of Go 1.18) will
@@ -218,6 +208,12 @@ func NewDirect(opts Options) (*Direct, error) {
httpc = http.DefaultClient
}
if httpc == nil {
dnsCache := &dnscache.Resolver{
Forward: dnscache.Get().Forward, // use default cache's forwarder
UseLastGood: true,
LookupIPFallback: dnsfallback.Lookup,
Logf: opts.Logf,
}
tr := http.DefaultTransport.(*http.Transport).Clone()
tr.Proxy = tshttpproxy.ProxyFromEnvironment
tshttpproxy.SetTransportGetProxyConnectHeader(tr)
@@ -236,7 +232,7 @@ func NewDirect(opts Options) (*Direct, error) {
httpc: httpc,
getMachinePrivKey: opts.GetMachinePrivateKey,
serverURL: opts.ServerURL,
clock: opts.Clock,
timeNow: opts.TimeNow,
logf: opts.Logf,
newDecompressor: opts.NewDecompressor,
keepAlive: opts.KeepAlive,
@@ -245,7 +241,7 @@ func NewDirect(opts Options) (*Direct, error) {
discoPubKey: opts.DiscoPublicKey,
debugFlags: opts.DebugFlags,
keepSharerAndUserSplit: opts.KeepSharerAndUserSplit,
netMon: opts.NetMon,
linkMon: opts.LinkMonitor,
skipIPForwardingCheck: opts.SkipIPForwardingCheck,
pinger: opts.Pinger,
popBrowser: opts.PopBrowserURL,
@@ -253,7 +249,6 @@ func NewDirect(opts Options) (*Direct, error) {
onControlTime: opts.OnControlTime,
c2nHandler: opts.C2NHandler,
dialer: opts.Dialer,
dnsCache: dnsCache,
dialPlan: opts.DialPlan,
}
if opts.Hostinfo == nil {
@@ -433,7 +428,7 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
authKey, isWrapped, wrappedSig, wrappedKey := decodeWrappedAuthkey(c.authKey, c.logf)
hi := c.hostInfoLocked()
backendLogID := hi.BackendLogID
expired := c.expiry != nil && !c.expiry.IsZero() && c.expiry.Before(c.clock.Now())
expired := c.expiry != nil && !c.expiry.IsZero() && c.expiry.Before(c.timeNow())
c.mu.Unlock()
machinePrivKey, err := c.getMachinePrivKey()
@@ -538,7 +533,7 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
err = errors.New("hostinfo: BackendLogID missing")
return regen, opt.URL, nil, err
}
now := c.clock.Now().Round(time.Second)
now := time.Now().Round(time.Second)
request := tailcfg.RegisterRequest{
Version: 1,
OldNodeKey: oldNodeKey,
@@ -771,14 +766,14 @@ func (c *Direct) SetEndpoints(endpoints []tailcfg.Endpoint) (changed bool) {
// PollNetMap makes a /map request to download the network map, calling cb with
// each new netmap.
// It always returns a non-nil error describing the reason for the failure
// or why the request ended.
func (c *Direct) PollNetMap(ctx context.Context, cb func(*netmap.NetworkMap)) error {
c.logf("PollNetMap")
return c.sendMapRequest(ctx, -1, false, cb)
}
// FetchNetMap fetches the netmap once.
func (c *Direct) FetchNetMap(ctx context.Context) (*netmap.NetworkMap, error) {
c.logf("FetchNetMap")
var ret *netmap.NetworkMap
err := c.sendMapRequest(ctx, 1, false, func(nm *netmap.NetworkMap) {
ret = nm
@@ -793,6 +788,7 @@ func (c *Direct) FetchNetMap(ctx context.Context) (*netmap.NetworkMap, error) {
// but does not fetch anything. It returns an error if the server did not return a
// successful 200 OK response.
func (c *Direct) SendLiteMapUpdate(ctx context.Context) error {
c.logf("SendLiteMapUpdate")
return c.sendMapRequest(ctx, 1, false, nil)
}
@@ -801,13 +797,9 @@ func (c *Direct) SendLiteMapUpdate(ctx context.Context) error {
// every minute.
const pollTimeout = 120 * time.Second
// sendMapRequest makes a /map request to download the network map, calling cb with
// each new netmap. If maxPolls is -1, it will poll forever and only returns if
// the context expires or the server returns an error/closes the connection and as
// such always returns a non-nil error.
//
// If cb is nil, OmitPeers will be set to true.
// cb nil means to omit peers.
func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, readOnly bool, cb func(*netmap.NetworkMap)) error {
c.logf("sendMapRequest")
metricMapRequests.Add(1)
metricMapRequestsActive.Add(1)
defer metricMapRequestsActive.Add(-1)
@@ -834,21 +826,25 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, readOnly bool
machinePrivKey, err := c.getMachinePrivKey()
if err != nil {
c.logf("sendMapRequest: machinePrivKey failed")
return fmt.Errorf("getMachinePrivKey: %w", err)
}
if machinePrivKey.IsZero() {
c.logf("sendMapRequest: machinePrivKey isZero")
return errors.New("getMachinePrivKey returned zero key")
}
if persist.PrivateNodeKey().IsZero() {
c.logf("sendMapRequest: privateNodeKey isZero")
return errors.New("privateNodeKey is zero")
}
if backendLogID == "" {
c.logf("sendMapRequest: BackendLogID missing")
return errors.New("hostinfo: BackendLogID missing")
}
allowStream := maxPolls != 1
c.logf("[v1] PollNetMap: stream=%v ep=%v", allowStream, epStrs)
c.logf("PollNetMap: stream=%v ep=%v", allowStream, epStrs)
vlogf := logger.Discard
if DevKnob.DumpNetMaps() {
@@ -883,8 +879,8 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, readOnly bool
ReadOnly: readOnly && !allowStream,
}
var extraDebugFlags []string
if hi != nil && c.netMon != nil && !c.skipIPForwardingCheck &&
ipForwardingBroken(hi.RoutableIPs, c.netMon.InterfaceState()) {
if hi != nil && c.linkMon != nil && !c.skipIPForwardingCheck &&
ipForwardingBroken(hi.RoutableIPs, c.linkMon.InterfaceState()) {
extraDebugFlags = append(extraDebugFlags, "warn-ip-forwarding-off")
}
if health.RouterHealth() != nil {
@@ -903,7 +899,8 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, readOnly bool
}
bodyData, err := encode(request, serverKey, serverNoiseKey, machinePrivKey)
if err != nil {
if err != nil {
c.logf("PollNetMap: encode failed")
vlogf("netmap: encode: %v", err)
return err
}
@@ -912,7 +909,7 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, readOnly bool
defer cancel()
machinePubKey := machinePrivKey.Public()
t0 := c.clock.Now()
t0 := time.Now()
// Url and httpc are protocol specific.
var url string
@@ -931,11 +928,13 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, readOnly bool
req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewReader(bodyData))
if err != nil {
c.logf("PollNetMap: NewRequestWithContext failed")
return err
}
res, err := httpc.Do(req)
if err != nil {
c.logf("PollNetMap: httpc.Do failed")
vlogf("netmap: Do: %v", err)
return err
}
@@ -943,6 +942,7 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, readOnly bool
if res.StatusCode != 200 {
msg, _ := io.ReadAll(res.Body)
res.Body.Close()
c.logf("PollNetMap: Status != 200")
return fmt.Errorf("initial fetch failed %d: %.200s",
res.StatusCode, strings.TrimSpace(string(msg)))
}
@@ -952,10 +952,11 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, readOnly bool
if cb == nil {
io.Copy(io.Discard, res.Body)
c.logf("PollNetMap: cb == nil")
return nil
}
timeout, timeoutChannel := c.clock.NewTimer(pollTimeout)
timeout := time.NewTimer(pollTimeout)
timeoutReset := make(chan struct{})
pollDone := make(chan struct{})
defer close(pollDone)
@@ -964,21 +965,24 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, readOnly bool
select {
case <-pollDone:
vlogf("netmap: ending timeout goroutine")
c.logf("netmap: ending timeout goroutine")
return
case <-timeoutChannel:
case <-timeout.C:
c.logf("map response long-poll timed out!")
cancel()
return
case <-timeoutReset:
if !timeout.Stop() {
select {
case <-timeoutChannel:
case <-timeout.C:
case <-pollDone:
vlogf("netmap: ending timeout goroutine")
c.logf("netmap: ending timeout goroutine")
return
}
}
vlogf("netmap: reset timeout timer")
c.logf("netmap: reset timeout timer")
timeout.Reset(pollTimeout)
}
}
@@ -1001,6 +1005,7 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, readOnly bool
vlogf("netmap: starting size read after %v (poll %v)", time.Since(t0).Round(time.Millisecond), i)
var siz [4]byte
if _, err := io.ReadFull(res.Body, siz[:]); err != nil {
c.logf("PollNetMap: io.ReadFull 4 bytes failed")
vlogf("netmap: size read error after %v: %v", time.Since(t0).Round(time.Millisecond), err)
return err
}
@@ -1008,6 +1013,7 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, readOnly bool
vlogf("netmap: read size %v after %v", size, time.Since(t0).Round(time.Millisecond))
msg = append(msg[:0], make([]byte, size)...)
if _, err := io.ReadFull(res.Body, msg); err != nil {
c.logf("PollNetMap: io.ReadFull all bytes failed")
vlogf("netmap: body read error: %v", err)
return err
}
@@ -1015,6 +1021,7 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, readOnly bool
var resp tailcfg.MapResponse
if err := c.decodeMsg(msg, &resp, machinePrivKey); err != nil {
c.logf("PollNetMap: decode error")
vlogf("netmap: decode error: %v")
return err
}
@@ -1060,6 +1067,7 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, readOnly bool
c.logf("netmap: [unexpected] new dial plan; nowhere to store it")
}
}
c.logf("PollNetMap: past dial plan")
select {
case timeoutReset <- struct{}{}:
@@ -1097,7 +1105,7 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, readOnly bool
go dumpGoroutinesToURL(c.httpc, resp.Debug.GoroutineDumpURL)
}
if sleep := time.Duration(resp.Debug.SleepSeconds * float64(time.Second)); sleep > 0 {
if err := sleepAsRequested(ctx, c.logf, timeoutReset, sleep, c.clock); err != nil {
if err := sleepAsRequested(ctx, c.logf, timeoutReset, sleep); err != nil {
return err
}
}
@@ -1127,7 +1135,7 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, readOnly bool
// This is handy for debugging, and our logs processing
// pipeline depends on it. (TODO: Remove this dependency.)
// Code elsewhere prints netmap diffs every time they are received.
now := c.clock.Now()
now := c.timeNow()
if now.Sub(c.lastPrintMap) >= 5*time.Minute {
c.lastPrintMap = now
c.logf("[v1] new network map[%d]:\n%s", i, nm.VeryConcise())
@@ -1305,7 +1313,7 @@ func initDevKnob() devKnobs {
}
}
var clock tstime.Clock = tstime.StdClock{}
var clockNow = time.Now
// opt.Bool configs from control.
var (
@@ -1409,9 +1417,9 @@ func answerHeadPing(logf logger.Logf, c *http.Client, pr *tailcfg.PingRequest) {
if pr.Log {
logf("answerHeadPing: sending HEAD ping to %v ...", pr.URL)
}
t0 := clock.Now()
t0 := time.Now()
_, err = c.Do(req)
d := clock.Since(t0).Round(time.Millisecond)
d := time.Since(t0).Round(time.Millisecond)
if err != nil {
logf("answerHeadPing error: %v to %v (after %v)", err, pr.URL, d)
} else if pr.Log {
@@ -1457,7 +1465,7 @@ func answerC2NPing(logf logger.Logf, c2nHandler http.Handler, c *http.Client, pr
if pr.Log {
logf("answerC2NPing: sending POST ping to %v ...", pr.URL)
}
t0 := clock.Now()
t0 := time.Now()
_, err = c.Do(req)
d := time.Since(t0).Round(time.Millisecond)
if err != nil {
@@ -1467,7 +1475,7 @@ func answerC2NPing(logf logger.Logf, c2nHandler http.Handler, c *http.Client, pr
}
}
func sleepAsRequested(ctx context.Context, logf logger.Logf, timeoutReset chan<- struct{}, d time.Duration, clock tstime.Clock) error {
func sleepAsRequested(ctx context.Context, logf logger.Logf, timeoutReset chan<- struct{}, d time.Duration) error {
const maxSleep = 5 * time.Minute
if d > maxSleep {
logf("sleeping for %v, capped from server-requested %v ...", maxSleep, d)
@@ -1476,20 +1484,20 @@ func sleepAsRequested(ctx context.Context, logf logger.Logf, timeoutReset chan<-
logf("sleeping for server-requested %v ...", d)
}
ticker, tickerChannel := clock.NewTicker(pollTimeout / 2)
ticker := time.NewTicker(pollTimeout / 2)
defer ticker.Stop()
timer, timerChannel := clock.NewTimer(d)
timer := time.NewTimer(d)
defer timer.Stop()
for {
select {
case <-ctx.Done():
return ctx.Err()
case <-timerChannel:
case <-timer.C:
return nil
case <-tickerChannel:
case <-ticker.C:
select {
case timeoutReset <- struct{}{}:
case <-timerChannel:
case <-timer.C:
return nil
case <-ctx.Done():
return ctx.Err()
@@ -1520,16 +1528,7 @@ func (c *Direct) getNoiseClient() (*NoiseClient, error) {
return nil, err
}
c.logf("creating new noise client")
nc, err := NewNoiseClient(NoiseOpts{
PrivKey: k,
ServerPubKey: serverNoiseKey,
ServerURL: c.serverURL,
Dialer: c.dialer,
DNSCache: c.dnsCache,
Logf: c.logf,
NetMon: c.netMon,
DialPlan: dp,
})
nc, err := NewNoiseClient(k, serverNoiseKey, c.serverURL, c.dialer, dp)
if err != nil {
return nil, err
}
@@ -1666,7 +1665,7 @@ func doPingerPing(logf logger.Logf, c *http.Client, pr *tailcfg.PingRequest, pin
logf("invalid ping request: missing url, ip or pinger")
return
}
start := clock.Now()
start := time.Now()
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
@@ -1704,7 +1703,7 @@ func postPingResult(start time.Time, logf logger.Logf, c *http.Client, pr *tailc
if pr.Log {
logf("postPingResult: sending ping results to %v ...", pr.URL)
}
t0 := clock.Now()
t0 := time.Now()
_, err = c.Do(req)
d := time.Since(t0).Round(time.Millisecond)
if err != nil {

View File

@@ -90,28 +90,9 @@ func (ms *mapSession) netmapForResponse(resp *tailcfg.MapResponse) *netmap.Netwo
ms.lastUserProfile[up.ID] = up
}
if dm := resp.DERPMap; dm != nil {
if resp.DERPMap != nil {
ms.vlogf("netmap: new map contains DERP map")
// Zero-valued fields in a DERPMap mean that we're not changing
// anything and are using the previous value(s).
if ldm := ms.lastDERPMap; ldm != nil {
if dm.Regions == nil {
dm.Regions = ldm.Regions
dm.OmitDefaultRegions = ldm.OmitDefaultRegions
}
if dm.HomeParams == nil {
dm.HomeParams = ldm.HomeParams
} else if oldhh := ldm.HomeParams; oldhh != nil {
// Propagate sub-fields of HomeParams
hh := dm.HomeParams
if hh.RegionScore == nil {
hh.RegionScore = oldhh.RegionScore
}
}
}
ms.lastDERPMap = dm
ms.lastDERPMap = resp.DERPMap
}
if pf := resp.PacketFilter; pf != nil {
@@ -307,7 +288,7 @@ func undeltaPeers(mapRes *tailcfg.MapResponse, prev []*tailcfg.Node) {
for _, n := range newFull {
peerByID[n.ID] = n
}
now := clock.Now()
now := clockNow()
for nodeID, seen := range mapRes.PeerSeenChange {
if n, ok := peerByID[nodeID]; ok {
if seen {

View File

@@ -14,7 +14,6 @@ import (
"go4.org/mem"
"tailscale.com/tailcfg"
"tailscale.com/tstest"
"tailscale.com/tstime"
"tailscale.com/types/key"
"tailscale.com/types/netmap"
"tailscale.com/types/opt"
@@ -24,6 +23,9 @@ import (
func TestUndeltaPeers(t *testing.T) {
var curTime time.Time
tstest.Replace(t, &clockNow, func() time.Time {
return curTime
})
online := func(v bool) func(*tailcfg.Node) {
return func(n *tailcfg.Node) {
@@ -296,7 +298,6 @@ func TestUndeltaPeers(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
if !tt.curTime.IsZero() {
curTime = tt.curTime
tstest.Replace(t, &clock, tstime.Clock(tstest.NewClock(tstest.ClockOpts{Start: curTime})))
}
undeltaPeers(tt.mapRes, tt.prev)
if !reflect.DeepEqual(tt.mapRes.Peers, tt.want) {
@@ -618,108 +619,3 @@ func TestCopyDebugOptBools(t *testing.T) {
}
}
}
func TestDeltaDERPMap(t *testing.T) {
regions1 := map[int]*tailcfg.DERPRegion{
1: {
RegionID: 1,
Nodes: []*tailcfg.DERPNode{{
Name: "derp1a",
RegionID: 1,
HostName: "derp1a" + tailcfg.DotInvalid,
IPv4: "169.254.169.254",
IPv6: "none",
}},
},
}
// As above, but with a changed IPv4 addr
regions2 := map[int]*tailcfg.DERPRegion{1: regions1[1].Clone()}
regions2[1].Nodes[0].IPv4 = "127.0.0.1"
type step struct {
got *tailcfg.DERPMap
want *tailcfg.DERPMap
}
tests := []struct {
name string
steps []step
}{
{
name: "nothing-to-nothing",
steps: []step{
{nil, nil},
{nil, nil},
},
},
{
name: "regions-sticky",
steps: []step{
{&tailcfg.DERPMap{Regions: regions1}, &tailcfg.DERPMap{Regions: regions1}},
{&tailcfg.DERPMap{}, &tailcfg.DERPMap{Regions: regions1}},
},
},
{
name: "regions-change",
steps: []step{
{&tailcfg.DERPMap{Regions: regions1}, &tailcfg.DERPMap{Regions: regions1}},
{&tailcfg.DERPMap{Regions: regions2}, &tailcfg.DERPMap{Regions: regions2}},
},
},
{
name: "home-params",
steps: []step{
// Send a DERP map
{&tailcfg.DERPMap{Regions: regions1}, &tailcfg.DERPMap{Regions: regions1}},
// Send home params, want to still have the same regions
{
&tailcfg.DERPMap{HomeParams: &tailcfg.DERPHomeParams{
RegionScore: map[int]float64{1: 0.5},
}},
&tailcfg.DERPMap{Regions: regions1, HomeParams: &tailcfg.DERPHomeParams{
RegionScore: map[int]float64{1: 0.5},
}},
},
},
},
{
name: "home-params-sub-fields",
steps: []step{
// Send a DERP map with home params
{
&tailcfg.DERPMap{Regions: regions1, HomeParams: &tailcfg.DERPHomeParams{
RegionScore: map[int]float64{1: 0.5},
}},
&tailcfg.DERPMap{Regions: regions1, HomeParams: &tailcfg.DERPHomeParams{
RegionScore: map[int]float64{1: 0.5},
}},
},
// Sending a struct with a 'HomeParams' field but nil RegionScore doesn't change home params...
{
&tailcfg.DERPMap{HomeParams: &tailcfg.DERPHomeParams{RegionScore: nil}},
&tailcfg.DERPMap{Regions: regions1, HomeParams: &tailcfg.DERPHomeParams{
RegionScore: map[int]float64{1: 0.5},
}},
},
// ... but sending one with a non-nil and empty RegionScore field zeroes that out.
{
&tailcfg.DERPMap{HomeParams: &tailcfg.DERPHomeParams{RegionScore: map[int]float64{}}},
&tailcfg.DERPMap{Regions: regions1, HomeParams: &tailcfg.DERPHomeParams{
RegionScore: map[int]float64{},
}},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ms := newTestMapSession(t)
for stepi, s := range tt.steps {
nm := ms.netmapForResponse(&tailcfg.MapResponse{DERPMap: s.got})
if !reflect.DeepEqual(nm.DERPMap, s.want) {
t.Errorf("unexpected result at step index %v; got: %s", stepi, must.Get(json.Marshal(nm.DERPMap)))
}
}
})
}
}

View File

@@ -19,13 +19,9 @@ import (
"golang.org/x/net/http2"
"tailscale.com/control/controlbase"
"tailscale.com/control/controlhttp"
"tailscale.com/net/dnscache"
"tailscale.com/net/netmon"
"tailscale.com/net/tsdial"
"tailscale.com/tailcfg"
"tailscale.com/tstime"
"tailscale.com/types/key"
"tailscale.com/types/logger"
"tailscale.com/util/mak"
"tailscale.com/util/multierr"
"tailscale.com/util/singleflight"
@@ -160,7 +156,6 @@ type NoiseClient struct {
sfDial singleflight.Group[struct{}, *noiseConn]
dialer *tsdial.Dialer
dnsCache *dnscache.Resolver
privKey key.MachinePrivate
serverPubKey key.MachinePublic
host string // the host part of serverURL
@@ -172,9 +167,6 @@ type NoiseClient struct {
// be nil.
dialPlan func() *tailcfg.ControlDialPlan
logf logger.Logf
netMon *netmon.Monitor
// mu only protects the following variables.
mu sync.Mutex
last *noiseConn // or nil
@@ -182,39 +174,12 @@ type NoiseClient struct {
connPool map[int]*noiseConn // active connections not yet closed; see noiseConn.Close
}
// NoiseOpts contains options for the NewNoiseClient function. All fields are
// required unless otherwise specified.
type NoiseOpts struct {
// PrivKey is this node's private key.
PrivKey key.MachinePrivate
// ServerPubKey is the public key of the server.
ServerPubKey key.MachinePublic
// ServerURL is the URL of the server to connect to.
ServerURL string
// Dialer's SystemDial function is used to connect to the server.
Dialer *tsdial.Dialer
// DNSCache is the caching Resolver to use to connect to the server.
//
// This field can be nil.
DNSCache *dnscache.Resolver
// Logf is the log function to use. This field can be nil.
Logf logger.Logf
// NetMon is the network monitor that, if set, will be used to get the
// network interface state. This field can be nil; if so, the current
// state will be looked up dynamically.
NetMon *netmon.Monitor
// DialPlan, if set, is a function that should return an explicit plan
// on how to connect to the server.
DialPlan func() *tailcfg.ControlDialPlan
}
// NewNoiseClient returns a new noiseClient for the provided server and machine key.
// serverURL is of the form https://<host>:<port> (no trailing slash).
//
// netMon may be nil, if non-nil it's used to do faster interface lookups.
// dialPlan may be nil
func NewNoiseClient(opts NoiseOpts) (*NoiseClient, error) {
u, err := url.Parse(opts.ServerURL)
func NewNoiseClient(privKey key.MachinePrivate, serverPubKey key.MachinePublic, serverURL string, dialer *tsdial.Dialer, dialPlan func() *tailcfg.ControlDialPlan) (*NoiseClient, error) {
u, err := url.Parse(serverURL)
if err != nil {
return nil, err
}
@@ -234,18 +199,14 @@ func NewNoiseClient(opts NoiseOpts) (*NoiseClient, error) {
httpPort = "80"
httpsPort = "443"
}
np := &NoiseClient{
serverPubKey: opts.ServerPubKey,
privKey: opts.PrivKey,
serverPubKey: serverPubKey,
privKey: privKey,
host: u.Hostname(),
httpPort: httpPort,
httpsPort: httpsPort,
dialer: opts.Dialer,
dnsCache: opts.DNSCache,
dialPlan: opts.DialPlan,
logf: opts.Logf,
netMon: opts.NetMon,
dialer: dialer,
dialPlan: dialPlan,
}
// Create the HTTP/2 Transport using a net/http.Transport
@@ -288,25 +249,6 @@ func (nc *NoiseClient) GetSingleUseRoundTripper(ctx context.Context) (http.Round
return nil, nil, errors.New("[unexpected] failed to reserve a request on a connection")
}
// contextErr is an error that wraps another error and is used to indicate that
// the error was because a context expired.
type contextErr struct {
err error
}
func (e contextErr) Error() string {
return e.err.Error()
}
func (e contextErr) Unwrap() error {
return e.err
}
// getConn returns a noiseConn that can be used to make requests to the
// coordination server. It may return a cached connection or create a new one.
// Dials are singleflighted, so concurrent calls to getConn may only dial once.
// As such, context values may not be respected as there are no guarantees that
// the context passed to getConn is the same as the context passed to dial.
func (nc *NoiseClient) getConn(ctx context.Context) (*noiseConn, error) {
nc.mu.Lock()
if last := nc.last; last != nil && last.canTakeNewRequest() {
@@ -315,35 +257,11 @@ func (nc *NoiseClient) getConn(ctx context.Context) (*noiseConn, error) {
}
nc.mu.Unlock()
for {
// We singeflight the dial to avoid making multiple connections, however
// that means that we can't simply cancel the dial if the context is
// canceled. Instead, we have to additionally check that the context
// which was canceled is our context and retry if our context is still
// valid.
conn, err, _ := nc.sfDial.Do(struct{}{}, func() (*noiseConn, error) {
c, err := nc.dial(ctx)
if err != nil {
if ctx.Err() != nil {
return nil, contextErr{ctx.Err()}
}
return nil, err
}
return c, nil
})
var ce contextErr
if err == nil || !errors.As(err, &ce) {
return conn, err
}
if ctx.Err() == nil {
// The dial failed because of a context error, but our context
// is still valid. Retry.
continue
}
// The dial failed because our context was canceled. Return the
// underlying error.
return nil, ce.Unwrap()
conn, err, _ := nc.sfDial.Do(struct{}{}, nc.dial)
if err != nil {
return nil, err
}
return conn, nil
}
func (nc *NoiseClient) RoundTrip(req *http.Request) (*http.Response, error) {
@@ -388,7 +306,7 @@ func (nc *NoiseClient) Close() error {
// dial opens a new connection to tailcontrol, fetching the server noise key
// if not cached.
func (nc *NoiseClient) dial(ctx context.Context) (*noiseConn, error) {
func (nc *NoiseClient) dial() (*noiseConn, error) {
nc.mu.Lock()
connID := nc.nextID
nc.nextID++
@@ -436,7 +354,7 @@ func (nc *NoiseClient) dial(ctx context.Context) (*noiseConn, error) {
}
timeout := time.Duration(timeoutSec * float64(time.Second))
ctx, cancel := context.WithTimeout(ctx, timeout)
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
clientConn, err := (&controlhttp.Dialer{
@@ -447,11 +365,7 @@ func (nc *NoiseClient) dial(ctx context.Context) (*noiseConn, error) {
ControlKey: nc.serverPubKey,
ProtocolVersion: uint16(tailcfg.CurrentCapabilityVersion),
Dialer: nc.dialer.SystemDial,
DNSCache: nc.dnsCache,
DialPlan: dialPlan,
Logf: nc.logf,
NetMon: nc.netMon,
Clock: tstime.StdClock{},
}).Dial(ctx)
if err != nil {
return nil, err

View File

@@ -74,12 +74,7 @@ func (tt noiseClientTest) run(t *testing.T) {
defer hs.Close()
dialer := new(tsdial.Dialer)
nc, err := NewNoiseClient(NoiseOpts{
PrivKey: clientPrivate,
ServerPubKey: serverPrivate.Public(),
ServerURL: hs.URL,
Dialer: dialer,
})
nc, err := NewNoiseClient(clientPrivate, serverPrivate.Public(), hs.URL, dialer, nil)
if err != nil {
t.Fatal(err)
}

View File

@@ -127,7 +127,7 @@ func findIdentity(subject string, st certstore.Store) (certstore.Identity, []*x5
return nil, nil, err
}
selected, chain := selectIdentityFromSlice(subject, ids, clock.Now())
selected, chain := selectIdentityFromSlice(subject, ids, time.Now())
for _, id := range ids {
if id != selected {

View File

@@ -45,7 +45,6 @@ import (
"tailscale.com/net/tlsdial"
"tailscale.com/net/tshttpproxy"
"tailscale.com/tailcfg"
"tailscale.com/tstime"
"tailscale.com/util/multierr"
)
@@ -148,16 +147,13 @@ func (a *Dialer) dial(ctx context.Context) (*ClientConn, error) {
// before we do anything.
if c.DialStartDelaySec > 0 {
a.logf("[v2] controlhttp: waiting %.2f seconds before dialing %q @ %v", c.DialStartDelaySec, a.Hostname, c.IP)
if a.Clock == nil {
a.Clock = tstime.StdClock{}
}
tmr, tmrChannel := a.Clock.NewTimer(time.Duration(c.DialStartDelaySec * float64(time.Second)))
tmr := time.NewTimer(time.Duration(c.DialStartDelaySec * float64(time.Second)))
defer tmr.Stop()
select {
case <-ctx.Done():
err = ctx.Err()
return
case <-tmrChannel:
case <-tmr.C:
}
}
@@ -277,7 +273,7 @@ func (a *Dialer) dialHost(ctx context.Context, addr netip.Addr) (*ClientConn, er
ctx, cancel := context.WithCancel(ctx)
defer cancel()
ctx = sockstats.WithSockStats(ctx, sockstats.LabelControlClientDialer, a.logf)
ctx = sockstats.WithSockStats(ctx, sockstats.LabelControlClientDialer)
// u80 and u443 are the URLs we'll try to hit over HTTP or HTTPS,
// respectively, in order to do the HTTP upgrade to a net.Conn over which
@@ -323,10 +319,7 @@ func (a *Dialer) dialHost(ctx context.Context, addr netip.Addr) (*ClientConn, er
// In case outbound port 80 blocked or MITM'ed poorly, start a backup timer
// to dial port 443 if port 80 doesn't either succeed or fail quickly.
if a.Clock == nil {
a.Clock = tstime.StdClock{}
}
try443Timer := a.Clock.AfterFunc(a.httpsFallbackDelay(), func() { try(u443) })
try443Timer := time.AfterFunc(a.httpsFallbackDelay(), func() { try(u443) })
defer try443Timer.Stop()
var err80, err443 error
@@ -381,22 +374,6 @@ func (a *Dialer) dialURL(ctx context.Context, u *url.URL, addr netip.Addr) (*Cli
}, nil
}
// resolver returns a.DNSCache if non-nil or a new *dnscache.Resolver
// otherwise.
func (a *Dialer) resolver() *dnscache.Resolver {
if a.DNSCache != nil {
return a.DNSCache
}
return &dnscache.Resolver{
Forward: dnscache.Get().Forward,
LookupIPFallback: dnsfallback.MakeLookupFunc(a.logf, a.NetMon),
UseLastGood: true,
Logf: a.Logf, // not a.logf method; we want to propagate nil-ness
NetMon: a.NetMon,
}
}
// tryURLUpgrade connects to u, and tries to upgrade it to a net.Conn. If addr
// is valid, then no DNS is used and the connection will be made to the
// provided address.
@@ -412,10 +389,14 @@ func (a *Dialer) tryURLUpgrade(ctx context.Context, u *url.URL, addr netip.Addr,
SingleHostStaticResult: []netip.Addr{addr},
SingleHost: u.Hostname(),
Logf: a.Logf, // not a.logf method; we want to propagate nil-ness
NetMon: a.NetMon,
}
} else {
dns = a.resolver()
dns = &dnscache.Resolver{
Forward: dnscache.Get().Forward,
LookupIPFallback: dnsfallback.Lookup,
UseLastGood: true,
Logf: a.Logf, // not a.logf method; we want to propagate nil-ness
}
}
var dialer dnscache.DialContextFunc

View File

@@ -9,9 +9,7 @@ import (
"time"
"tailscale.com/net/dnscache"
"tailscale.com/net/netmon"
"tailscale.com/tailcfg"
"tailscale.com/tstime"
"tailscale.com/types/key"
"tailscale.com/types/logger"
)
@@ -68,17 +66,10 @@ type Dialer struct {
// If not specified, this defaults to net.Dialer.DialContext.
Dialer dnscache.DialContextFunc
// DNSCache is the caching Resolver used by this Dialer.
//
// If not specified, a new Resolver is created per attempt.
DNSCache *dnscache.Resolver
// Logf, if set, is a logging function to use; if unset, logs are
// dropped.
Logf logger.Logf
NetMon *netmon.Monitor
// DialPlan, if set, contains instructions from the control server on
// how to connect to it. If present, we will try the methods in this
// plan before falling back to DNS.
@@ -90,10 +81,6 @@ type Dialer struct {
drainFinished chan struct{}
omitCertErrorLogging bool
testFallbackDelay time.Duration
// tstime.Clock is used instead of time package for methods such as time.Now.
// If not specified, will default to tstime.StdClock{}.
Clock tstime.Clock
}
func strDef(v1, v2 string) string {

View File

@@ -25,7 +25,6 @@ import (
"tailscale.com/net/socks5"
"tailscale.com/net/tsdial"
"tailscale.com/tailcfg"
"tailscale.com/tstest"
"tailscale.com/types/key"
"tailscale.com/types/logger"
)
@@ -205,7 +204,6 @@ func testControlHTTP(t *testing.T, param httpTestParam) {
Logf: t.Logf,
omitCertErrorLogging: true,
testFallbackDelay: 50 * time.Millisecond,
Clock: &tstest.Clock{},
}
if proxy != nil {
@@ -585,20 +583,19 @@ func TestDialPlan(t *testing.T) {
}},
want: goodAddr,
},
// TODO(#8442): fix this test
// {
// name: "multiple-priority-fast-path",
// plan: &tailcfg.ControlDialPlan{Candidates: []tailcfg.ControlIPCandidate{
// // Dials some good IPs and our bad one (which
// // hangs forever), which then hits the fast
// // path where we bail without waiting.
// {IP: brokenAddr, Priority: 1, DialTimeoutSec: 10},
// {IP: goodAddr, Priority: 1, DialTimeoutSec: 10},
// {IP: other2Addr, Priority: 1, DialTimeoutSec: 10},
// {IP: otherAddr, Priority: 2, DialTimeoutSec: 10},
// }},
// want: otherAddr,
// },
{
name: "multiple-priority-fast-path",
plan: &tailcfg.ControlDialPlan{Candidates: []tailcfg.ControlIPCandidate{
// Dials some good IPs and our bad one (which
// hangs forever), which then hits the fast
// path where we bail without waiting.
{IP: brokenAddr, Priority: 1, DialTimeoutSec: 10},
{IP: goodAddr, Priority: 1, DialTimeoutSec: 10},
{IP: other2Addr, Priority: 1, DialTimeoutSec: 10},
{IP: otherAddr, Priority: 2, DialTimeoutSec: 10},
}},
want: otherAddr,
},
{
name: "multiple-priority-slow-path",
plan: &tailcfg.ControlDialPlan{Candidates: []tailcfg.ControlIPCandidate{
@@ -662,7 +659,6 @@ func TestDialPlan(t *testing.T) {
drainFinished: drained,
omitCertErrorLogging: true,
testFallbackDelay: 50 * time.Millisecond,
Clock: &tstest.Clock{},
}
conn, err := a.dial(ctx)

View File

@@ -1,61 +0,0 @@
# DERP
This directory (and subdirectories) contain the DERP code. The server itself is
in `../cmd/derper`.
DERP is a packet relay system (client and servers) where peers are addressed
using WireGuard public keys instead of IP addresses.
It relays two types of packets:
* "Disco" discovery messages (see `../disco`) as the a side channel during [NAT
traversal](https://tailscale.com/blog/how-nat-traversal-works/).
* Encrypted WireGuard packets as the fallback of last resort when UDP is blocked
or NAT traversal fails.
## DERP Map
Each client receives a "[DERP
Map](https://pkg.go.dev/tailscale.com/tailcfg#DERPMap)" from the coordination
server describing the DERP servers the client should try to use.
The client picks its home "DERP home" based on latency. This is done to keep
costs low by avoid using cloud load balancers (pricey) or anycast, which would
necessarily require server-side routing between DERP regions.
Clients pick their DERP home and report it to the coordination server which
shares it to all the peers in the tailnet. When a peer wants to send a packet
and it doesn't already have a WireGuard session open, it sends disco messages
(some direct, and some over DERP), trying to do the NAT traversal. The client
will make connections to multiple DERP regions as needed. Only the DERP home
region connection needs to be alive forever.
## DERP Regions
Tailscale runs 1 or more DERP nodes (instances of `cmd/derper`) in various
geographic regions to make sure users have low latency to their DERP home.
Regions generally have multiple nodes per region "meshed" (routing to each
other) together for redundancy: it allows for cloud failures or upgrades without
kicking users out to a higher latency region. Instead, clients will reconnect to
the next node in the region. Each node in the region is required to to be meshed
with every other node in the region and forward packets to the other nodes in
the region. Packets are forwarded only one hop within the region. There is no
routing between regions. The assumption is that the mesh TCP connections are
over a VPC that's very fast, low latency, and not charged per byte. The
coordination server assigns the list of nodes in a region as a function of the
tailnet, so all nodes within a tailnet should generally be on the same node and
not require forwarding. Only after a failure do clients of a particular tailnet
get split between nodes in a region and require inter-node forwarding. But over
time it balances back out. There's also an admin-only DERP frame type to force
close the TCP connection of a particular client to force them to reconnect to
their primary if the operator wants to force things to balance out sooner.
(Using the `(*derphttp.Client).ClosePeer` method, as used by Tailscale's
internal rarely-used `cmd/derpprune` maintenance tool)
We generally run a minimum of three nodes in a region not for quorum reasons
(there's no voting) but just because two is too uncomfortably few for cascading
failure reasons: if you're running two nodes at 51% load (CPU, memory, etc) and
then one fails, that makes the second one fail. With three or more nodes, you
can run each node a bit hotter.

View File

@@ -77,11 +77,8 @@ const (
// a previous sender is no longer connected. That is, if A
// sent to B, and then if A disconnects, the server sends
// framePeerGone to B so B can forget that a reverse path
// exists on that connection to get back to A. It is also sent
// if A tries to send a CallMeMaybe to B and the server has no
// record of B (which currently would only happen if there was
// a bug).
framePeerGone = frameType(0x08) // 32B pub key of peer that's gone + 1 byte reason
// exists on that connection to get back to A.
framePeerGone = frameType(0x08) // 32B pub key of peer that's gone
// framePeerPresent is like framePeerGone, but for other
// members of the DERP region when they're meshed up together.
@@ -119,15 +116,6 @@ const (
frameRestarting = frameType(0x15)
)
// PeerGoneReasonType is a one byte reason code explaining why a
// server does not have a path to the requested destination.
type PeerGoneReasonType byte
const (
PeerGoneReasonDisconnected = PeerGoneReasonType(0x00) // peer disconnected from this server
PeerGoneReasonNotHere = PeerGoneReasonType(0x01) // server doesn't know about this peer, unexpected
)
var bin = binary.BigEndian
func writeUint32(bw *bufio.Writer, v uint32) error {

View File

@@ -17,7 +17,6 @@ import (
"go4.org/mem"
"golang.org/x/time/rate"
"tailscale.com/syncs"
"tailscale.com/tstime"
"tailscale.com/types/key"
"tailscale.com/types/logger"
)
@@ -41,8 +40,6 @@ type Client struct {
// Owned by Recv:
peeked int // bytes to discard on next Recv
readErr syncs.AtomicValue[error] // sticky (set by Recv)
clock tstime.Clock
}
// ClientOpt is an option passed to NewClient.
@@ -106,7 +103,6 @@ func newClient(privateKey key.NodePrivate, nc Conn, brw *bufio.ReadWriter, logf
meshKey: opt.MeshKey,
canAckPings: opt.CanAckPings,
isProber: opt.IsProber,
clock: tstime.StdClock{},
}
if opt.ServerPub.IsZero() {
if err := c.recvServerKey(); err != nil {
@@ -218,7 +214,7 @@ func (c *Client) send(dstKey key.NodePublic, pkt []byte) (ret error) {
defer c.wmu.Unlock()
if c.rate != nil {
pktLen := frameHeaderLen + key.NodePublicRawLen + len(pkt)
if !c.rate.AllowN(c.clock.Now(), pktLen) {
if !c.rate.AllowN(time.Now(), pktLen) {
return nil // drop
}
}
@@ -248,7 +244,7 @@ func (c *Client) ForwardPacket(srcKey, dstKey key.NodePublic, pkt []byte) (err e
c.wmu.Lock()
defer c.wmu.Unlock()
timer := c.clock.AfterFunc(5*time.Second, c.writeTimeoutFired)
timer := time.AfterFunc(5*time.Second, c.writeTimeoutFired)
defer timer.Stop()
if err := writeFrameHeader(c.bw, frameForwardPacket, uint32(keyLen*2+len(pkt))); err != nil {
@@ -352,12 +348,9 @@ type ReceivedPacket struct {
func (ReceivedPacket) msg() {}
// PeerGoneMessage is a ReceivedMessage that indicates that the client
// identified by the underlying public key is not connected to this
// server.
type PeerGoneMessage struct {
Peer key.NodePublic
Reason PeerGoneReasonType
}
// identified by the underlying public key had previously sent you a
// packet but has now disconnected from the server.
type PeerGoneMessage key.NodePublic
func (PeerGoneMessage) msg() {}
@@ -461,6 +454,7 @@ func (c *Client) recvTimeout(timeout time.Duration) (m ReceivedMessage, err erro
c.readErr.Store(err)
}
}()
for {
c.nc.SetReadDeadline(time.Now().Add(timeout))
@@ -530,15 +524,7 @@ func (c *Client) recvTimeout(timeout time.Duration) (m ReceivedMessage, err erro
c.logf("[unexpected] dropping short peerGone frame from DERP server")
continue
}
// Backward compatibility for the older peerGone without reason byte
reason := PeerGoneReasonDisconnected
if n > keyLen {
reason = PeerGoneReasonType(b[keyLen])
}
pg := PeerGoneMessage{
Peer: key.NodePublicFromRaw32(mem.B(b[:keyLen])),
Reason: reason,
}
pg := PeerGoneMessage(key.NodePublicFromRaw32(mem.B(b[:keyLen])))
return pg, nil
case framePeerPresent:

View File

@@ -34,13 +34,12 @@ import (
"go4.org/mem"
"golang.org/x/sync/errgroup"
"golang.org/x/time/rate"
"tailscale.com/client/tailscale"
"tailscale.com/disco"
"tailscale.com/envknob"
"tailscale.com/metrics"
"tailscale.com/syncs"
"tailscale.com/tstime"
"tailscale.com/tstime/rate"
"tailscale.com/types/key"
"tailscale.com/types/logger"
"tailscale.com/version"
@@ -123,8 +122,7 @@ type Server struct {
_ align64
packetsForwardedOut expvar.Int
packetsForwardedIn expvar.Int
peerGoneDisconnectedFrames expvar.Int // number of peer disconnected frames sent
peerGoneNotHereFrames expvar.Int // number of peer not here frames sent
peerGoneFrames expvar.Int // number of peer gone frames sent
gotPing expvar.Int // number of ping frames from client
sentPong expvar.Int // number of pong frames enqueued to client
accepts expvar.Int
@@ -165,8 +163,6 @@ type Server struct {
// maps from netip.AddrPort to a client's public key
keyOfAddr map[netip.AddrPort]key.NodePublic
clock tstime.Clock
}
// clientSet represents 1 or more *sclients.
@@ -283,7 +279,6 @@ func (s *dupClientSet) removeClient(c *sclient) bool {
// public key gets more than one PacketForwarder registered for it.
type PacketForwarder interface {
ForwardPacket(src, dst key.NodePublic, payload []byte) error
String() string
}
// Conn is the subset of the underlying net.Conn the DERP Server needs.
@@ -321,7 +316,6 @@ func NewServer(privateKey key.NodePrivate, logf logger.Logf) *Server {
avgQueueDuration: new(uint64),
tcpRtt: metrics.LabelMap{Label: "le"},
keyOfAddr: map[netip.AddrPort]key.NodePublic{},
clock: tstime.StdClock{},
}
s.initMetacert()
s.packetsRecvDisco = s.packetsRecvByKind.Get("disco")
@@ -329,8 +323,7 @@ func NewServer(privateKey key.NodePrivate, logf logger.Logf) *Server {
s.packetsDroppedReasonCounters = []*expvar.Int{
s.packetsDroppedReason.Get("unknown_dest"),
s.packetsDroppedReason.Get("unknown_dest_on_fwd"),
s.packetsDroppedReason.Get("gone_disconnected"),
s.packetsDroppedReason.Get("gone_not_here"),
s.packetsDroppedReason.Get("gone"),
s.packetsDroppedReason.Get("queue_head"),
s.packetsDroppedReason.Get("queue_tail"),
s.packetsDroppedReason.Get("write_error"),
@@ -471,8 +464,8 @@ func (s *Server) initMetacert() {
CommonName: fmt.Sprintf("derpkey%s", s.publicKey.UntypedHexString()),
},
// Windows requires NotAfter and NotBefore set:
NotAfter: s.clock.Now().Add(30 * 24 * time.Hour),
NotBefore: s.clock.Now().Add(-30 * 24 * time.Hour),
NotAfter: time.Now().Add(30 * 24 * time.Hour),
NotBefore: time.Now().Add(-30 * 24 * time.Hour),
// Per https://github.com/golang/go/issues/51759#issuecomment-1071147836,
// macOS requires BasicConstraints when subject == issuer:
BasicConstraintsValid: true,
@@ -502,7 +495,6 @@ func (s *Server) registerClient(c *sclient) {
switch set := set.(type) {
case nil:
s.clients[c.key] = singleClient{c}
c.debugLogf("register single client")
case singleClient:
s.dupClientKeys.Add(1)
s.dupClientConns.Add(2) // both old and new count
@@ -518,7 +510,6 @@ func (s *Server) registerClient(c *sclient) {
},
sendHistory: []*sclient{old},
}
c.debugLogf("register duplicate client")
case *dupClientSet:
s.dupClientConns.Add(1) // the gauge
s.dupClientConnTotal.Add(1) // the counter
@@ -526,7 +517,6 @@ func (s *Server) registerClient(c *sclient) {
set.set[c] = true
set.last = c
set.sendHistory = append(set.sendHistory, c)
c.debugLogf("register another duplicate client")
}
if _, ok := s.clientsMesh[c.key]; !ok {
@@ -559,7 +549,7 @@ func (s *Server) unregisterClient(c *sclient) {
case nil:
c.logf("[unexpected]; clients map is empty")
case singleClient:
c.debugLogf("removed connection")
c.logf("removing connection")
delete(s.clients, c.key)
if v, ok := s.clientsMesh[c.key]; ok && v == nil {
delete(s.clientsMesh, c.key)
@@ -567,7 +557,6 @@ func (s *Server) unregisterClient(c *sclient) {
}
s.broadcastPeerStateChangeLocked(c.key, false)
case *dupClientSet:
c.debugLogf("removed duplicate client")
if set.removeClient(c) {
s.dupClientConns.Add(-1)
} else {
@@ -621,26 +610,13 @@ func (s *Server) notePeerGoneFromRegionLocked(key key.NodePublic) {
}
set.ForeachClient(func(peer *sclient) {
if peer.connNum == connNum {
go peer.requestPeerGoneWrite(key, PeerGoneReasonDisconnected)
go peer.requestPeerGoneWrite(key)
}
})
}
delete(s.sentTo, key)
}
// requestPeerGoneWriteLimited sends a request to write a "peer gone"
// frame, but only in reply to a disco packet, and only if we haven't
// sent one recently.
func (c *sclient) requestPeerGoneWriteLimited(peer key.NodePublic, contents []byte, reason PeerGoneReasonType) {
if disco.LooksLikeDiscoWrapper(contents) != true {
return
}
if c.peerGoneLim.Allow() {
go c.requestPeerGoneWrite(peer, reason)
}
}
func (s *Server) addWatcher(c *sclient) {
if !c.canMesh {
panic("invariant: addWatcher called without permissions")
@@ -697,17 +673,16 @@ func (s *Server) accept(ctx context.Context, nc Conn, brw *bufio.ReadWriter, rem
nc: nc,
br: br,
bw: bw,
logf: logger.WithPrefix(s.logf, fmt.Sprintf("derp client %v%s: ", remoteAddr, clientKey.ShortString())),
logf: logger.WithPrefix(s.logf, fmt.Sprintf("derp client %v/%x: ", remoteAddr, clientKey)),
done: ctx.Done(),
remoteAddr: remoteAddr,
remoteIPPort: remoteIPPort,
connectedAt: s.clock.Now(),
connectedAt: time.Now(),
sendQueue: make(chan pkt, perClientSendQueueDepth),
discoSendQueue: make(chan pkt, perClientSendQueueDepth),
sendPongCh: make(chan [8]byte, 1),
peerGone: make(chan peerGoneMsg),
peerGone: make(chan key.NodePublic),
canMesh: clientInfo.MeshKey != "" && clientInfo.MeshKey == s.meshKey,
peerGoneLim: rate.NewLimiter(rate.Every(time.Second), 3),
}
if c.canMesh {
@@ -715,12 +690,6 @@ func (s *Server) accept(ctx context.Context, nc Conn, brw *bufio.ReadWriter, rem
}
if clientInfo != nil {
c.info = *clientInfo
if envknob.Bool("DERP_PROBER_DEBUG_LOGS") && clientInfo.IsProber {
c.debug = true
}
}
if s.debug {
c.debug = true
}
s.registerClient(c)
@@ -734,12 +703,6 @@ func (s *Server) accept(ctx context.Context, nc Conn, brw *bufio.ReadWriter, rem
return c.run(ctx)
}
func (s *Server) debugLogf(format string, v ...any) {
if s.debug {
s.logf(format, v...)
}
}
// for testing
var (
timeSleep = time.Sleep
@@ -757,27 +720,22 @@ func (c *sclient) run(ctx context.Context) error {
defer func() {
cancelSender()
if err := grp.Wait(); err != nil && !c.s.isClosed() {
if errors.Is(err, context.Canceled) {
c.debugLogf("sender canceled by reader exiting")
} else {
c.logf("sender failed: %v", err)
}
c.logf("sender failed: %v", err)
}
}()
for {
ft, fl, err := readFrameHeader(c.br)
c.debugLogf("read frame type %d len %d err %v", ft, fl, err)
if err != nil {
if errors.Is(err, io.EOF) {
c.debugLogf("read EOF")
c.logf("read EOF")
return nil
}
if c.s.isClosed() {
c.logf("closing; server closed")
return nil
}
return fmt.Errorf("client %s: readFrameHeader: %w", c.key.ShortString(), err)
return fmt.Errorf("client %x: readFrameHeader: %w", c.key, err)
}
c.s.noteClientActivity(c)
switch ft {
@@ -920,18 +878,14 @@ func (c *sclient) handleFrameForwardPacket(ft frameType, fl uint32) error {
reason := dropReasonUnknownDestOnFwd
if dstLen > 1 {
reason = dropReasonDupClient
} else {
c.requestPeerGoneWriteLimited(dstKey, contents, PeerGoneReasonNotHere)
}
s.recordDrop(contents, srcKey, dstKey, reason)
return nil
}
dst.debugLogf("received forwarded packet from %s via %s", srcKey.ShortString(), c.key.ShortString())
return c.sendPkt(dst, pkt{
bs: contents,
enqueuedAt: c.s.clock.Now(),
enqueuedAt: time.Now(),
src: srcKey,
})
}
@@ -976,9 +930,7 @@ func (c *sclient) handleFrameSendPacket(ft frameType, fl uint32) error {
if dst == nil {
if fwd != nil {
s.packetsForwardedOut.Add(1)
err := fwd.ForwardPacket(c.key, dstKey, contents)
c.debugLogf("SendPacket for %s, forwarding via %s: %v", dstKey.ShortString(), fwd, err)
if err != nil {
if err := fwd.ForwardPacket(c.key, dstKey, contents); err != nil {
// TODO:
return nil
}
@@ -987,29 +939,19 @@ func (c *sclient) handleFrameSendPacket(ft frameType, fl uint32) error {
reason := dropReasonUnknownDest
if dstLen > 1 {
reason = dropReasonDupClient
} else {
c.requestPeerGoneWriteLimited(dstKey, contents, PeerGoneReasonNotHere)
}
s.recordDrop(contents, c.key, dstKey, reason)
c.debugLogf("SendPacket for %s, dropping with reason=%s", dstKey.ShortString(), reason)
return nil
}
c.debugLogf("SendPacket for %s, sending directly", dstKey.ShortString())
p := pkt{
bs: contents,
enqueuedAt: c.s.clock.Now(),
enqueuedAt: time.Now(),
src: c.key,
}
return c.sendPkt(dst, p)
}
func (c *sclient) debugLogf(format string, v ...any) {
if c.debug {
c.logf(format, v...)
}
}
// dropReason is why we dropped a DERP frame.
type dropReason int
@@ -1018,7 +960,7 @@ type dropReason int
const (
dropReasonUnknownDest dropReason = iota // unknown destination pubkey
dropReasonUnknownDestOnFwd // unknown destination pubkey on a derp-forwarded packet
dropReasonGoneDisconnected // destination tailscaled disconnected before we could send
dropReasonGone // destination tailscaled disconnected before we could send
dropReasonQueueHead // destination queue is full, dropped packet at queue head
dropReasonQueueTail // destination queue is full, dropped packet at queue tail
dropReasonWriteError // OS write() failed
@@ -1028,8 +970,7 @@ const (
func (s *Server) recordDrop(packetBytes []byte, srcKey, dstKey key.NodePublic, reason dropReason) {
s.packetsDropped.Add(1)
s.packetsDroppedReasonCounters[reason].Add(1)
looksDisco := disco.LooksLikeDiscoWrapper(packetBytes)
if looksDisco {
if disco.LooksLikeDiscoWrapper(packetBytes) {
s.packetsDroppedTypeDisco.Add(1)
} else {
s.packetsDroppedTypeOther.Add(1)
@@ -1042,7 +983,9 @@ func (s *Server) recordDrop(packetBytes []byte, srcKey, dstKey key.NodePublic, r
msg := fmt.Sprintf("drop (%s) %s -> %s", srcKey.ShortString(), reason, dstKey.ShortString())
s.limitedLogf(msg)
}
s.debugLogf("dropping packet reason=%s dst=%s disco=%v", reason, dstKey, looksDisco)
if s.debug {
s.logf("dropping packet reason=%s dst=%s disco=%v", reason, dstKey, disco.LooksLikeDiscoWrapper(packetBytes))
}
}
func (c *sclient) sendPkt(dst *sclient, p pkt) error {
@@ -1059,14 +1002,12 @@ func (c *sclient) sendPkt(dst *sclient, p pkt) error {
for attempt := 0; attempt < 3; attempt++ {
select {
case <-dst.done:
s.recordDrop(p.bs, c.key, dstKey, dropReasonGoneDisconnected)
dst.debugLogf("sendPkt attempt %d dropped, dst gone", attempt)
s.recordDrop(p.bs, c.key, dstKey, dropReasonGone)
return nil
default:
}
select {
case sendQueue <- p:
dst.debugLogf("sendPkt attempt %d enqueued", attempt)
return nil
default:
}
@@ -1082,20 +1023,16 @@ func (c *sclient) sendPkt(dst *sclient, p pkt) error {
// contended queue with racing writers. Give up and tail-drop in
// this case to keep reader unblocked.
s.recordDrop(p.bs, c.key, dstKey, dropReasonQueueTail)
dst.debugLogf("sendPkt attempt %d dropped, queue full")
return nil
}
// requestPeerGoneWrite sends a request to write a "peer gone" frame
// with an explanation of why it is gone. It blocks until either the
// that the provided peer has disconnected. It blocks until either the
// write request is scheduled, or the client has closed.
func (c *sclient) requestPeerGoneWrite(peer key.NodePublic, reason PeerGoneReasonType) {
func (c *sclient) requestPeerGoneWrite(peer key.NodePublic) {
select {
case c.peerGone <- peerGoneMsg{
peer: peer,
reason: reason,
}:
case c.peerGone <- peer:
case <-c.done:
}
}
@@ -1309,18 +1246,22 @@ type sclient struct {
key key.NodePublic
info clientInfo
logf logger.Logf
done <-chan struct{} // closed when connection closes
remoteAddr string // usually ip:port from net.Conn.RemoteAddr().String()
remoteIPPort netip.AddrPort // zero if remoteAddr is not ip:port.
sendQueue chan pkt // packets queued to this client; never closed
discoSendQueue chan pkt // important packets queued to this client; never closed
sendPongCh chan [8]byte // pong replies to send to the client; never closed
peerGone chan peerGoneMsg // write request that a peer is not at this server (not used by mesh peers)
meshUpdate chan struct{} // write request to write peerStateChange
canMesh bool // clientInfo had correct mesh token for inter-region routing
isDup atomic.Bool // whether more than 1 sclient for key is connected
isDisabled atomic.Bool // whether sends to this peer are disabled due to active/active dups
debug bool // turn on for verbose logging
done <-chan struct{} // closed when connection closes
remoteAddr string // usually ip:port from net.Conn.RemoteAddr().String()
remoteIPPort netip.AddrPort // zero if remoteAddr is not ip:port.
sendQueue chan pkt // packets queued to this client; never closed
discoSendQueue chan pkt // important packets queued to this client; never closed
sendPongCh chan [8]byte // pong replies to send to the client; never closed
peerGone chan key.NodePublic // write request that a previous sender has disconnected (not used by mesh peers)
meshUpdate chan struct{} // write request to write peerStateChange
canMesh bool // clientInfo had correct mesh token for inter-region routing
isDup atomic.Bool // whether more than 1 sclient for key is connected
isDisabled atomic.Bool // whether sends to this peer are disabled due to active/active dups
// replaceLimiter controls how quickly two connections with
// the same client key can kick each other off the server by
// taking over ownership of a key.
replaceLimiter *rate.Limiter
// Owned by run, not thread-safe.
br *bufio.Reader
@@ -1337,11 +1278,6 @@ type sclient struct {
// the client for them to update their map of who's connected
// to this node.
peerStateChange []peerConnState
// peerGoneLimiter limits how often the server will inform a
// client that it's trying to establish a direct connection
// through us with a peer we have no record of.
peerGoneLim *rate.Limiter
}
// peerConnState represents whether a peer is connected to the server
@@ -1365,12 +1301,6 @@ type pkt struct {
bs []byte
}
// peerGoneMsg is a request to write a peerGone frame to an sclient
type peerGoneMsg struct {
peer key.NodePublic
reason PeerGoneReasonType
}
func (c *sclient) setPreferred(v bool) {
if c.preferred == v {
return
@@ -1391,7 +1321,7 @@ func (c *sclient) setPreferred(v bool) {
// graphs, so not important to miss a move. But it shouldn't:
// the netcheck/re-STUNs in magicsock only happen about every
// 30 seconds.
if c.s.clock.Since(c.connectedAt) > 5*time.Second {
if time.Since(c.connectedAt) > 5*time.Second {
homeMove.Add(1)
}
}
@@ -1405,7 +1335,7 @@ func expMovingAverage(prev, newValue, alpha float64) float64 {
// recordQueueTime updates the average queue duration metric after a packet has been sent.
func (c *sclient) recordQueueTime(enqueuedAt time.Time) {
elapsed := float64(c.s.clock.Since(enqueuedAt).Milliseconds())
elapsed := float64(time.Since(enqueuedAt).Milliseconds())
for {
old := atomic.LoadUint64(c.s.avgQueueDuration)
newAvg := expMovingAverage(math.Float64frombits(old), elapsed, 0.1)
@@ -1425,9 +1355,9 @@ func (c *sclient) sendLoop(ctx context.Context) error {
for {
select {
case pkt := <-c.sendQueue:
c.s.recordDrop(pkt.bs, pkt.src, c.key, dropReasonGoneDisconnected)
c.s.recordDrop(pkt.bs, pkt.src, c.key, dropReasonGone)
case pkt := <-c.discoSendQueue:
c.s.recordDrop(pkt.bs, pkt.src, c.key, dropReasonGoneDisconnected)
c.s.recordDrop(pkt.bs, pkt.src, c.key, dropReasonGone)
default:
return
}
@@ -1435,7 +1365,7 @@ func (c *sclient) sendLoop(ctx context.Context) error {
}()
jitter := time.Duration(rand.Intn(5000)) * time.Millisecond
keepAliveTick, keepAliveTickChannel := c.s.clock.NewTicker(keepAlive + jitter)
keepAliveTick := time.NewTicker(keepAlive + jitter)
defer keepAliveTick.Stop()
var werr error // last write error
@@ -1448,8 +1378,8 @@ func (c *sclient) sendLoop(ctx context.Context) error {
select {
case <-ctx.Done():
return nil
case msg := <-c.peerGone:
werr = c.sendPeerGone(msg.peer, msg.reason)
case peer := <-c.peerGone:
werr = c.sendPeerGone(peer)
continue
case <-c.meshUpdate:
werr = c.sendMeshUpdates()
@@ -1465,7 +1395,7 @@ func (c *sclient) sendLoop(ctx context.Context) error {
case msg := <-c.sendPongCh:
werr = c.sendPong(msg)
continue
case <-keepAliveTickChannel:
case <-keepAliveTick.C:
werr = c.sendKeepAlive()
continue
default:
@@ -1480,8 +1410,8 @@ func (c *sclient) sendLoop(ctx context.Context) error {
select {
case <-ctx.Done():
return nil
case msg := <-c.peerGone:
werr = c.sendPeerGone(msg.peer, msg.reason)
case peer := <-c.peerGone:
werr = c.sendPeerGone(peer)
case <-c.meshUpdate:
werr = c.sendMeshUpdates()
continue
@@ -1494,7 +1424,7 @@ func (c *sclient) sendLoop(ctx context.Context) error {
case msg := <-c.sendPongCh:
werr = c.sendPong(msg)
continue
case <-keepAliveTickChannel:
case <-keepAliveTick.C:
werr = c.sendKeepAlive()
}
}
@@ -1522,22 +1452,13 @@ func (c *sclient) sendPong(data [8]byte) error {
}
// sendPeerGone sends a peerGone frame, without flushing.
func (c *sclient) sendPeerGone(peer key.NodePublic, reason PeerGoneReasonType) error {
switch reason {
case PeerGoneReasonDisconnected:
c.s.peerGoneDisconnectedFrames.Add(1)
case PeerGoneReasonNotHere:
c.s.peerGoneNotHereFrames.Add(1)
}
func (c *sclient) sendPeerGone(peer key.NodePublic) error {
c.s.peerGoneFrames.Add(1)
c.setWriteDeadline()
data := make([]byte, 0, keyLen+1)
data = peer.AppendTo(data)
data = append(data, byte(reason))
if err := writeFrameHeader(c.bw.bw(), framePeerGone, uint32(len(data))); err != nil {
if err := writeFrameHeader(c.bw.bw(), framePeerGone, keyLen); err != nil {
return err
}
_, err := c.bw.Write(data)
_, err := c.bw.Write(peer.AppendTo(nil))
return err
}
@@ -1568,7 +1489,7 @@ func (c *sclient) sendMeshUpdates() error {
if pcs.present {
err = c.sendPeerPresent(pcs.peer)
} else {
err = c.sendPeerGone(pcs.peer, PeerGoneReasonDisconnected)
err = c.sendPeerGone(pcs.peer)
}
if err != nil {
// Shouldn't happen, though, as we're writing
@@ -1608,7 +1529,6 @@ func (c *sclient) sendPacket(srcKey key.NodePublic, contents []byte) (err error)
c.s.packetsSent.Add(1)
c.s.bytesSent.Add(int64(len(contents)))
}
c.debugLogf("sendPacket from %s: %v", srcKey.ShortString(), err)
}()
c.setWriteDeadline()
@@ -1769,10 +1689,6 @@ func (f *multiForwarder) ForwardPacket(src, dst key.NodePublic, payload []byte)
return f.fwd.Load().ForwardPacket(src, dst, payload)
}
func (f *multiForwarder) String() string {
return fmt.Sprintf("<MultiForwarder fwd=%s total=%d>", f.fwd.Load(), len(f.all))
}
func (s *Server) expVarFunc(f func() any) expvar.Func {
return expvar.Func(func() any {
s.mu.Lock()
@@ -1809,8 +1725,7 @@ func (s *Server) ExpVar() expvar.Var {
m.Set("home_moves_out", &s.homeMovesOut)
m.Set("got_ping", &s.gotPing)
m.Set("sent_pong", &s.sentPong)
m.Set("peer_gone_disconnected_frames", &s.peerGoneDisconnectedFrames)
m.Set("peer_gone_not_here_frames", &s.peerGoneNotHereFrames)
m.Set("peer_gone_frames", &s.peerGoneFrames)
m.Set("packets_forwarded_out", &s.packetsForwardedOut)
m.Set("packets_forwarded_in", &s.packetsForwardedIn)
m.Set("multiforwarder_created", &s.multiForwarderCreated)

View File

@@ -9,37 +9,45 @@ import (
"net"
"time"
"tailscale.com/net/tcpinfo"
"golang.org/x/sys/unix"
)
func (c *sclient) statsLoop(ctx context.Context) error {
// Get the RTT initially to verify it's supported.
conn := c.tcpConn()
if conn == nil {
// If we can't get a TCP socket, then we can't send stats.
tcpConn := c.tcpConn()
if tcpConn == nil {
c.s.tcpRtt.Add("non-tcp", 1)
return nil
}
if _, err := tcpinfo.RTT(conn); err != nil {
c.logf("error fetching initial RTT: %v", err)
rawConn, err := tcpConn.SyscallConn()
if err != nil {
c.logf("error getting SyscallConn: %v", err)
c.s.tcpRtt.Add("error", 1)
return nil
}
const statsInterval = 10 * time.Second
ticker, tickerChannel := c.s.clock.NewTicker(statsInterval)
ticker := time.NewTicker(statsInterval)
defer ticker.Stop()
var (
tcpInfo *unix.TCPInfo
sysErr error
)
statsLoop:
for {
select {
case <-tickerChannel:
rtt, err := tcpinfo.RTT(conn)
if err != nil {
case <-ticker.C:
err = rawConn.Control(func(fd uintptr) {
tcpInfo, sysErr = unix.GetsockoptTCPInfo(int(fd), unix.IPPROTO_TCP, unix.TCP_INFO)
})
if err != nil || sysErr != nil {
continue statsLoop
}
// TODO(andrew): more metrics?
rtt := time.Duration(tcpInfo.Rtt) * time.Microsecond
c.s.tcpRtt.Add(durationToLabel(rtt), 1)
case <-ctx.Done():

View File

@@ -25,9 +25,7 @@ import (
"go4.org/mem"
"golang.org/x/time/rate"
"tailscale.com/disco"
"tailscale.com/net/memnet"
"tailscale.com/tstest"
"tailscale.com/types/key"
"tailscale.com/types/logger"
)
@@ -107,8 +105,7 @@ func TestSendRecv(t *testing.T) {
t.Logf("Connected client %d.", i)
}
var peerGoneCountDisconnected expvar.Int
var peerGoneCountNotHere expvar.Int
var peerGoneCount expvar.Int
t.Logf("Starting read loops")
for i := 0; i < numClients; i++ {
@@ -124,14 +121,7 @@ func TestSendRecv(t *testing.T) {
t.Errorf("unexpected message type %T", m)
continue
case PeerGoneMessage:
switch m.Reason {
case PeerGoneReasonDisconnected:
peerGoneCountDisconnected.Add(1)
case PeerGoneReasonNotHere:
peerGoneCountNotHere.Add(1)
default:
t.Errorf("unexpected PeerGone reason %v", m.Reason)
}
peerGoneCount.Add(1)
case ReceivedPacket:
if m.Source.IsZero() {
t.Errorf("zero Source address in ReceivedPacket")
@@ -181,19 +171,7 @@ func TestSendRecv(t *testing.T) {
var got int64
dl := time.Now().Add(5 * time.Second)
for time.Now().Before(dl) {
if got = peerGoneCountDisconnected.Value(); got == want {
return
}
}
t.Errorf("peer gone count = %v; want %v", got, want)
}
wantUnknownPeers := func(want int64) {
t.Helper()
var got int64
dl := time.Now().Add(5 * time.Second)
for time.Now().Before(dl) {
if got = peerGoneCountNotHere.Value(); got == want {
if got = peerGoneCount.Value(); got == want {
return
}
}
@@ -216,30 +194,6 @@ func TestSendRecv(t *testing.T) {
recvNothing(0)
recvNothing(1)
// Send messages to a non-existent node
neKey := key.NewNode().Public()
msg4 := []byte("not a CallMeMaybe->unknown destination\n")
if err := clients[1].Send(neKey, msg4); err != nil {
t.Fatal(err)
}
wantUnknownPeers(0)
callMe := neKey.AppendTo([]byte(disco.Magic))
callMeHeader := make([]byte, disco.NonceLen)
callMe = append(callMe, callMeHeader...)
if err := clients[1].Send(neKey, callMe); err != nil {
t.Fatal(err)
}
wantUnknownPeers(1)
// PeerGoneNotHere is rate-limited to 3 times a second
for i := 0; i < 5; i++ {
if err := clients[1].Send(neKey, callMe); err != nil {
t.Fatal(err)
}
}
wantUnknownPeers(3)
wantActive(3, 0)
clients[0].NotePreferred(true)
wantActive(3, 1)
@@ -641,14 +595,10 @@ func (tc *testClient) wantGone(t *testing.T, peer key.NodePublic) {
}
switch m := m.(type) {
case PeerGoneMessage:
got := key.NodePublic(m.Peer)
got := key.NodePublic(m)
if peer != got {
t.Errorf("got gone message for %v; want gone for %v", tc.ts.keyName(got), tc.ts.keyName(peer))
}
reason := m.Reason
if reason != PeerGoneReasonDisconnected {
t.Errorf("got gone message for reason %v; wanted %v", reason, PeerGoneReasonDisconnected)
}
default:
t.Fatalf("unexpected message type %T", m)
}
@@ -710,9 +660,6 @@ type testFwd int
func (testFwd) ForwardPacket(key.NodePublic, key.NodePublic, []byte) error {
panic("not called in tests")
}
func (testFwd) String() string {
panic("not called in tests")
}
func pubAll(b byte) (ret key.NodePublic) {
var bs [32]byte
@@ -840,7 +787,6 @@ type channelFwd struct {
c chan []byte
}
func (f channelFwd) String() string { return "" }
func (f channelFwd) ForwardPacket(_ key.NodePublic, _ key.NodePublic, packet []byte) error {
f.c <- packet
return nil
@@ -991,10 +937,9 @@ func TestClientRecv(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := &Client{
nc: dummyNetConn{},
br: bufio.NewReader(bytes.NewReader(tt.input)),
logf: t.Logf,
clock: &tstest.Clock{},
nc: dummyNetConn{},
br: bufio.NewReader(bytes.NewReader(tt.input)),
logf: t.Logf,
}
got, err := c.Recv()
if err != nil {
@@ -1437,8 +1382,7 @@ func (w *countWriter) ResetStats() {
func TestClientSendRateLimiting(t *testing.T) {
cw := new(countWriter)
c := &Client{
bw: bufio.NewWriter(cw),
clock: &tstest.Clock{},
bw: bufio.NewWriter(cw),
}
c.setSendRateLimiter(ServerInfoMessage{})

View File

@@ -31,17 +31,14 @@ import (
"tailscale.com/derp"
"tailscale.com/envknob"
"tailscale.com/net/dnscache"
"tailscale.com/net/netmon"
"tailscale.com/net/netns"
"tailscale.com/net/sockstats"
"tailscale.com/net/tlsdial"
"tailscale.com/net/tshttpproxy"
"tailscale.com/syncs"
"tailscale.com/tailcfg"
"tailscale.com/tstime"
"tailscale.com/types/key"
"tailscale.com/types/logger"
"tailscale.com/util/cmpx"
)
// Client is a DERP-over-HTTP client.
@@ -56,14 +53,8 @@ type Client struct {
MeshKey string // optional; for trusted clients
IsProber bool // optional; for probers to optional declare themselves as such
// BaseContext, if non-nil, returns the base context to use for dialing a
// new derp server. If nil, context.Background is used.
// In either case, additional timeouts may be added to the base context.
BaseContext func() context.Context
privateKey key.NodePrivate
logf logger.Logf
netMon *netmon.Monitor // optional; nil means interfaces will be looked up on-demand
dialer func(ctx context.Context, network, addr string) (net.Conn, error)
// Either url or getRegion is non-nil:
@@ -89,26 +80,18 @@ type Client struct {
serverPubKey key.NodePublic
tlsState *tls.ConnectionState
pingOut map[derp.PingMessage]chan<- bool // chan to send to on pong
clock tstime.Clock
}
func (c *Client) String() string {
return fmt.Sprintf("<derphttp_client.Client %s url=%s>", c.serverPubKey.ShortString(), c.url)
}
// NewRegionClient returns a new DERP-over-HTTP client. It connects lazily.
// To trigger a connection, use Connect.
// The netMon parameter is optional; if non-nil it's used to do faster interface lookups.
func NewRegionClient(privateKey key.NodePrivate, logf logger.Logf, netMon *netmon.Monitor, getRegion func() *tailcfg.DERPRegion) *Client {
func NewRegionClient(privateKey key.NodePrivate, logf logger.Logf, getRegion func() *tailcfg.DERPRegion) *Client {
ctx, cancel := context.WithCancel(context.Background())
c := &Client{
privateKey: privateKey,
logf: logf,
netMon: netMon,
getRegion: getRegion,
ctx: ctx,
cancelCtx: cancel,
clock: tstime.StdClock{},
}
return c
}
@@ -116,7 +99,7 @@ func NewRegionClient(privateKey key.NodePrivate, logf logger.Logf, netMon *netmo
// NewNetcheckClient returns a Client that's only able to have its DialRegionTLS method called.
// It's used by the netcheck package.
func NewNetcheckClient(logf logger.Logf) *Client {
return &Client{logf: logf, clock: tstime.StdClock{}}
return &Client{logf: logf}
}
// NewClient returns a new DERP-over-HTTP client. It connects lazily.
@@ -137,7 +120,6 @@ func NewClient(privateKey key.NodePrivate, serverURL string, logf logger.Logf) (
url: u,
ctx: ctx,
cancelCtx: cancel,
clock: tstime.StdClock{},
}
return c, nil
}
@@ -149,19 +131,6 @@ func (c *Client) Connect(ctx context.Context) error {
return err
}
// newContext returns a new context for setting up a new DERP connection.
// It uses either c.BaseContext or returns context.Background.
func (c *Client) newContext() context.Context {
if c.BaseContext != nil {
ctx := c.BaseContext()
if ctx == nil {
panic("BaseContext returned nil")
}
return ctx
}
return context.Background()
}
// TLSConnectionState returns the last TLS connection state, if any.
// The client must already be connected.
func (c *Client) TLSConnectionState() (_ *tls.ConnectionState, ok bool) {
@@ -201,10 +170,6 @@ func urlPort(u *url.URL) string {
return ""
}
// debugDERPUseHTTP tells clients to connect to DERP via HTTP on port
// 3340 instead of HTTPS on 443.
var debugUseDERPHTTP = envknob.RegisterBool("TS_DEBUG_USE_DERP_HTTP")
func (c *Client) targetString(reg *tailcfg.DERPRegion) string {
if c.url != nil {
return c.url.String()
@@ -216,10 +181,6 @@ func (c *Client) useHTTPS() bool {
if c.url != nil && c.url.Scheme == "http" {
return false
}
if debugUseDERPHTTP() {
return false
}
return true
}
@@ -235,11 +196,7 @@ func (c *Client) urlString(node *tailcfg.DERPNode) string {
if c.url != nil {
return c.url.String()
}
proto := "https"
if debugUseDERPHTTP() {
proto = "http"
}
return fmt.Sprintf("%s://%s/derp", proto, node.HostName)
return fmt.Sprintf("https://%s/derp", node.HostName)
}
// AddressFamilySelector decides whether IPv6 is preferred for
@@ -519,7 +476,7 @@ func (c *Client) dialURL(ctx context.Context) (net.Conn, error) {
return c.dialer(ctx, "tcp", net.JoinHostPort(host, urlPort(c.url)))
}
hostOrIP := host
dialer := netns.NewDialer(c.logf, c.netMon)
dialer := netns.NewDialer(c.logf)
if c.DNSCache != nil {
ip, _, _, err := c.DNSCache.LookupIP(ctx, host)
@@ -614,7 +571,7 @@ func (c *Client) DialRegionTLS(ctx context.Context, reg *tailcfg.DERPRegion) (tl
}
func (c *Client) dialContext(ctx context.Context, proto, addr string) (net.Conn, error) {
return netns.NewDialer(c.logf, c.netMon).DialContext(ctx, proto, addr)
return netns.NewDialer(c.logf).DialContext(ctx, proto, addr)
}
// shouldDialProto reports whether an explicitly provided IPv4 or IPv6
@@ -659,25 +616,28 @@ func (c *Client) dialNode(ctx context.Context, n *tailcfg.DERPNode) (net.Conn, e
ctx, cancel := context.WithTimeout(ctx, dialNodeTimeout)
defer cancel()
ctx = sockstats.WithSockStats(ctx, sockstats.LabelDERPHTTPClient, c.logf)
ctx = sockstats.WithSockStats(ctx, sockstats.LabelDERPHTTPClient)
nwait := 0
startDial := func(dstPrimary, proto string) {
nwait++
go func() {
if proto == "tcp4" && c.preferIPv6() {
t, tChannel := c.clock.NewTimer(200 * time.Millisecond)
t := time.NewTimer(200 * time.Millisecond)
select {
case <-ctx.Done():
// Either user canceled original context,
// it timed out, or the v6 dial succeeded.
t.Stop()
return
case <-tChannel:
case <-t.C:
// Start v4 dial
}
}
dst := cmpx.Or(dstPrimary, n.HostName)
dst := dstPrimary
if dst == "" {
dst = n.HostName
}
port := "443"
if n.DERPPort != 0 {
port = fmt.Sprint(n.DERPPort)
@@ -794,7 +754,7 @@ func (c *Client) dialNodeUsingProxy(ctx context.Context, n *tailcfg.DERPNode, pr
}
func (c *Client) Send(dstKey key.NodePublic, b []byte) error {
client, _, err := c.connect(c.newContext(), "derphttp.Client.Send")
client, _, err := c.connect(context.TODO(), "derphttp.Client.Send")
if err != nil {
return err
}
@@ -894,7 +854,7 @@ func (c *Client) LocalAddr() (netip.AddrPort, error) {
}
func (c *Client) ForwardPacket(from, to key.NodePublic, b []byte) error {
client, _, err := c.connect(c.newContext(), "derphttp.Client.ForwardPacket")
client, _, err := c.connect(context.TODO(), "derphttp.Client.ForwardPacket")
if err != nil {
return err
}
@@ -960,7 +920,7 @@ func (c *Client) NotePreferred(v bool) {
//
// Only trusted connections (using MeshKey) are allowed to use this.
func (c *Client) WatchConnectionChanges() error {
client, _, err := c.connect(c.newContext(), "derphttp.Client.WatchConnectionChanges")
client, _, err := c.connect(context.TODO(), "derphttp.Client.WatchConnectionChanges")
if err != nil {
return err
}
@@ -975,7 +935,7 @@ func (c *Client) WatchConnectionChanges() error {
//
// Only trusted connections (using MeshKey) are allowed to use this.
func (c *Client) ClosePeer(target key.NodePublic) error {
client, _, err := c.connect(c.newContext(), "derphttp.Client.ClosePeer")
client, _, err := c.connect(context.TODO(), "derphttp.Client.ClosePeer")
if err != nil {
return err
}
@@ -996,7 +956,7 @@ func (c *Client) Recv() (derp.ReceivedMessage, error) {
// RecvDetail is like Recv, but additional returns the connection generation on each message.
// The connGen value is incremented every time the derphttp.Client reconnects to the server.
func (c *Client) RecvDetail() (m derp.ReceivedMessage, connGen int, err error) {
client, connGen, err := c.connect(c.newContext(), "derphttp.Client.Recv")
client, connGen, err := c.connect(context.TODO(), "derphttp.Client.Recv")
if err != nil {
return nil, 0, err
}

View File

@@ -51,7 +51,7 @@ func (c *Client) RunWatchConnectionLoop(ctx context.Context, ignoreServerKey key
present = map[key.NodePublic]bool{}
}
lastConnGen := 0
lastStatus := c.clock.Now()
lastStatus := time.Now()
logConnectedLocked := func() {
if loggedConnected {
return
@@ -61,7 +61,7 @@ func (c *Client) RunWatchConnectionLoop(ctx context.Context, ignoreServerKey key
}
const logConnectedDelay = 200 * time.Millisecond
timer := c.clock.AfterFunc(2*time.Second, func() {
timer := time.AfterFunc(2*time.Second, func() {
mu.Lock()
defer mu.Unlock()
logConnectedLocked()
@@ -91,11 +91,11 @@ func (c *Client) RunWatchConnectionLoop(ctx context.Context, ignoreServerKey key
}
sleep := func(d time.Duration) {
t, tChannel := c.clock.NewTimer(d)
t := time.NewTimer(d)
select {
case <-ctx.Done():
t.Stop()
case <-tChannel:
case <-t.C:
}
}
@@ -128,21 +128,11 @@ func (c *Client) RunWatchConnectionLoop(ctx context.Context, ignoreServerKey key
case derp.PeerPresentMessage:
updatePeer(key.NodePublic(m), true)
case derp.PeerGoneMessage:
switch m.Reason {
case derp.PeerGoneReasonDisconnected:
// Normal case, log nothing
case derp.PeerGoneReasonNotHere:
logf("Recv: peer %s not connected to %s",
key.NodePublic(m.Peer).ShortString(), c.ServerPublicKey().ShortString())
default:
logf("Recv: peer %s not at server %s for unknown reason %v",
key.NodePublic(m.Peer).ShortString(), c.ServerPublicKey().ShortString(), m.Reason)
}
updatePeer(key.NodePublic(m.Peer), false)
updatePeer(key.NodePublic(m), false)
default:
continue
}
if now := c.clock.Now(); now.Sub(lastStatus) > statusInterval {
if now := time.Now(); now.Sub(lastStatus) > statusInterval {
lastStatus = now
infoLogf("%d peers", len(present))
}

View File

@@ -13,16 +13,16 @@ func _() {
var x [1]struct{}
_ = x[dropReasonUnknownDest-0]
_ = x[dropReasonUnknownDestOnFwd-1]
_ = x[dropReasonGoneDisconnected-2]
_ = x[dropReasonGone-2]
_ = x[dropReasonQueueHead-3]
_ = x[dropReasonQueueTail-4]
_ = x[dropReasonWriteError-5]
_ = x[dropReasonDupClient-6]
}
const _dropReason_name = "UnknownDestUnknownDestOnFwdGoneDisconnectedQueueHeadQueueTailWriteErrorDupClient"
const _dropReason_name = "UnknownDestUnknownDestOnFwdGoneQueueHeadQueueTailWriteErrorDupClient"
var _dropReason_index = [...]uint8{0, 11, 27, 43, 52, 61, 71, 80}
var _dropReason_index = [...]uint8{0, 11, 27, 31, 40, 49, 59, 68}
func (i dropReason) String() string {
if i < 0 || i >= dropReason(len(_dropReason_index)-1) {

View File

@@ -1,40 +0,0 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package disco
import (
"bytes"
"encoding/binary"
"net/netip"
"tailscale.com/tailcfg"
"tailscale.com/types/key"
)
// ToPCAPFrame marshals the bytes for a pcap record that describe a disco frame.
//
// Warning: Alloc garbage. Acceptable while capturing.
func ToPCAPFrame(src netip.AddrPort, derpNodeSrc key.NodePublic, payload []byte) []byte {
var (
b bytes.Buffer
flag uint8
)
b.Grow(128) // Most disco frames will probably be smaller than this.
if src.Addr() == tailcfg.DerpMagicIPAddr {
flag |= 0x01
}
b.WriteByte(flag) // 1b: flag
derpSrc := derpNodeSrc.Raw32()
b.Write(derpSrc[:]) // 32b: derp public key
binary.Write(&b, binary.LittleEndian, uint16(src.Port())) // 2b: port
addr, _ := src.Addr().MarshalBinary()
binary.Write(&b, binary.LittleEndian, uint16(len(addr))) // 2b: len(addr)
b.Write(addr) // Xb: addr
binary.Write(&b, binary.LittleEndian, uint16(len(payload))) // 2b: len(payload)
b.Write(payload) // Xb: payload
return b.Bytes()
}

View File

@@ -6,20 +6,22 @@ SA_NAME ?= tailscale
TS_KUBE_SECRET ?= tailscale
rbac:
@sed -e "s;{{TS_KUBE_SECRET}};$(TS_KUBE_SECRET);g" role.yaml
@echo "---"
@sed -e "s;{{SA_NAME}};$(SA_NAME);g" rolebinding.yaml
@echo "---"
@sed -e "s;{{SA_NAME}};$(SA_NAME);g" sa.yaml
@sed -e "s;{{TS_KUBE_SECRET}};$(TS_KUBE_SECRET);g" role.yaml | kubectl apply -f -
@sed -e "s;{{SA_NAME}};$(SA_NAME);g" rolebinding.yaml | kubectl apply -f -
@sed -e "s;{{SA_NAME}};$(SA_NAME);g" sa.yaml | kubectl apply -f -
sidecar:
@sed -e "s;{{TS_KUBE_SECRET}};$(TS_KUBE_SECRET);g" sidecar.yaml | sed -e "s;{{SA_NAME}};$(SA_NAME);g"
@kubectl delete -f sidecar.yaml --ignore-not-found --grace-period=0
@sed -e "s;{{TS_KUBE_SECRET}};$(TS_KUBE_SECRET);g" sidecar.yaml | sed -e "s;{{SA_NAME}};$(SA_NAME);g" | kubectl create -f-
userspace-sidecar:
@sed -e "s;{{TS_KUBE_SECRET}};$(TS_KUBE_SECRET);g" userspace-sidecar.yaml | sed -e "s;{{SA_NAME}};$(SA_NAME);g"
@kubectl delete -f userspace-sidecar.yaml --ignore-not-found --grace-period=0
@sed -e "s;{{TS_KUBE_SECRET}};$(TS_KUBE_SECRET);g" userspace-sidecar.yaml | sed -e "s;{{SA_NAME}};$(SA_NAME);g" | kubectl create -f-
proxy:
@sed -e "s;{{TS_KUBE_SECRET}};$(TS_KUBE_SECRET);g" proxy.yaml | sed -e "s;{{SA_NAME}};$(SA_NAME);g" | sed -e "s;{{TS_DEST_IP}};$(TS_DEST_IP);g"
kubectl delete -f proxy.yaml --ignore-not-found --grace-period=0
sed -e "s;{{TS_KUBE_SECRET}};$(TS_KUBE_SECRET);g" proxy.yaml | sed -e "s;{{SA_NAME}};$(SA_NAME);g" | sed -e "s;{{TS_DEST_IP}};$(TS_DEST_IP);g" | kubectl create -f-
subnet-router:
@sed -e "s;{{TS_KUBE_SECRET}};$(TS_KUBE_SECRET);g" subnet.yaml | sed -e "s;{{SA_NAME}};$(SA_NAME);g" | sed -e "s;{{TS_ROUTES}};$(TS_ROUTES);g"
@kubectl delete -f subnet.yaml --ignore-not-found --grace-period=0
@sed -e "s;{{TS_KUBE_SECRET}};$(TS_KUBE_SECRET);g" subnet.yaml | sed -e "s;{{SA_NAME}};$(SA_NAME);g" | sed -e "s;{{TS_ROUTES}};$(TS_ROUTES);g" | kubectl create -f-

View File

@@ -26,7 +26,7 @@ There are quite a few ways of running Tailscale inside a Kubernetes Cluster, som
```bash
export SA_NAME=tailscale
export TS_KUBE_SECRET=tailscale-auth
make rbac | kubectl apply -f-
make rbac
```
### Sample Sidecar
@@ -36,7 +36,7 @@ Running as a sidecar allows you to directly expose a Kubernetes pod over Tailsca
1. Create and login to the sample nginx pod with a Tailscale sidecar
```bash
make sidecar | kubectl apply -f-
make sidecar
# If not using an auth key, authenticate by grabbing the Login URL here:
kubectl logs nginx ts-sidecar
```
@@ -60,7 +60,7 @@ You can also run the sidecar in userspace mode. The obvious benefit is reducing
1. Create and login to the sample nginx pod with a Tailscale sidecar
```bash
make userspace-sidecar | kubectl apply -f-
make userspace-sidecar
# If not using an auth key, authenticate by grabbing the Login URL here:
kubectl logs nginx ts-sidecar
```
@@ -100,7 +100,7 @@ Running a Tailscale proxy allows you to provide inbound connectivity to a Kubern
1. Deploy the proxy pod
```bash
make proxy | kubectl apply -f-
make proxy
# If not using an auth key, authenticate by grabbing the Login URL here:
kubectl logs proxy
```
@@ -133,7 +133,7 @@ the entire Kubernetes cluster network (assuming NetworkPolicies allow) over Tail
1. Deploy the subnet-router pod.
```bash
make subnet-router | kubectl apply -f-
make subnet-router
# If not using an auth key, authenticate by grabbing the Login URL here:
kubectl logs subnet-router
```

View File

@@ -1,56 +0,0 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// Package permissions provides a doctor.Check that prints the process
// permissions for the running process.
package permissions
import (
"context"
"fmt"
"os/user"
"strings"
"golang.org/x/exp/constraints"
"tailscale.com/types/logger"
)
// Check implements the doctor.Check interface.
type Check struct{}
func (Check) Name() string {
return "permissions"
}
func (Check) Run(_ context.Context, logf logger.Logf) error {
return permissionsImpl(logf)
}
func formatUserID[T constraints.Integer](id T) string {
idStr := fmt.Sprint(id)
if uu, err := user.LookupId(idStr); err != nil {
return idStr + "(<unknown>)"
} else {
return fmt.Sprintf("%s(%q)", idStr, uu.Username)
}
}
func formatGroupID[T constraints.Integer](id T) string {
idStr := fmt.Sprint(id)
if g, err := user.LookupGroupId(idStr); err != nil {
return idStr + "(<unknown>)"
} else {
return fmt.Sprintf("%s(%q)", idStr, g.Name)
}
}
func formatGroups[T constraints.Integer](groups []T) string {
var buf strings.Builder
for i, group := range groups {
if i > 0 {
buf.WriteByte(',')
}
buf.WriteString(formatGroupID(group))
}
return buf.String()
}

Some files were not shown because too many files have changed in this diff Show More