Update dependencies

This commit is contained in:
bluepython508
2025-04-09 01:00:12 +01:00
parent f0641ffd6e
commit 5a9cfc022c
882 changed files with 68930 additions and 24201 deletions

7
vendor/tailscale.com/.golangci.yml generated vendored
View File

@@ -26,16 +26,11 @@ issues:
# 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:
govet:
# Matches what we use in corp as of 2023-12-07
enable:
@@ -78,8 +73,6 @@ linters-settings:
# analyzer doesn't support type declarations
#- github.com/tailscale/tailscale/types/logger.Logf
misspell:
revive:
enable-all-rules: false
ignore-generated-header: true

2
vendor/tailscale.com/ALPINE.txt generated vendored
View File

@@ -1 +1 @@
3.18
3.19

6
vendor/tailscale.com/Dockerfile generated vendored
View File

@@ -27,7 +27,7 @@
# $ docker exec tailscaled tailscale status
FROM golang:1.23-alpine AS build-env
FROM golang:1.24-alpine AS build-env
WORKDIR /go/src/tailscale
@@ -62,8 +62,10 @@ RUN GOARCH=$TARGETARCH go install -ldflags="\
-X tailscale.com/version.gitCommitStamp=$VERSION_GIT_HASH" \
-v ./cmd/tailscale ./cmd/tailscaled ./cmd/containerboot
FROM alpine:3.18
FROM alpine:3.19
RUN apk add --no-cache ca-certificates iptables iproute2 ip6tables
RUN rm /sbin/iptables && ln -s /sbin/iptables-legacy /sbin/iptables
RUN rm /sbin/ip6tables && ln -s /sbin/ip6tables-legacy /sbin/ip6tables
COPY --from=build-env /go/bin/* /usr/local/bin/
# For compat with the previous run.sh, although ideally you should be

11
vendor/tailscale.com/Dockerfile.base generated vendored
View File

@@ -1,5 +1,12 @@
# Copyright (c) Tailscale Inc & AUTHORS
# SPDX-License-Identifier: BSD-3-Clause
FROM alpine:3.18
RUN apk add --no-cache ca-certificates iptables iproute2 ip6tables iputils
FROM alpine:3.19
RUN apk add --no-cache ca-certificates iptables iptables-legacy iproute2 ip6tables iputils
# Alpine 3.19 replaces legacy iptables with nftables based implementation. We
# can't be certain that all hosts that run Tailscale containers currently
# suppport nftables, so link back to legacy for backwards compatibility reasons.
# TODO(irbekrm): add some way how to determine if we still run on nodes that
# don't support nftables, so that we can eventually remove these symlinks.
RUN rm /sbin/iptables && ln -s /sbin/iptables-legacy /sbin/iptables
RUN rm /sbin/ip6tables && ln -s /sbin/ip6tables-legacy /sbin/ip6tables

7
vendor/tailscale.com/Makefile generated vendored
View File

@@ -17,7 +17,7 @@ lint: ## Run golangci-lint
updatedeps: ## Update depaware deps
# depaware (via x/tools/go/packages) shells back to "go", so make sure the "go"
# it finds in its $$PATH is the right one.
PATH="$$(./tool/go env GOROOT)/bin:$$PATH" ./tool/go run github.com/tailscale/depaware --update \
PATH="$$(./tool/go env GOROOT)/bin:$$PATH" ./tool/go run github.com/tailscale/depaware --update --internal \
tailscale.com/cmd/tailscaled \
tailscale.com/cmd/tailscale \
tailscale.com/cmd/derper \
@@ -27,7 +27,7 @@ updatedeps: ## Update depaware deps
depaware: ## Run depaware checks
# depaware (via x/tools/go/packages) shells back to "go", so make sure the "go"
# it finds in its $$PATH is the right one.
PATH="$$(./tool/go env GOROOT)/bin:$$PATH" ./tool/go run github.com/tailscale/depaware --check \
PATH="$$(./tool/go env GOROOT)/bin:$$PATH" ./tool/go run github.com/tailscale/depaware --check --internal \
tailscale.com/cmd/tailscaled \
tailscale.com/cmd/tailscale \
tailscale.com/cmd/derper \
@@ -100,7 +100,7 @@ publishdevoperator: ## Build and publish k8s-operator image to location specifie
@test "${REPO}" != "ghcr.io/tailscale/tailscale" || (echo "REPO=... must not be ghcr.io/tailscale/tailscale" && exit 1)
@test "${REPO}" != "tailscale/k8s-operator" || (echo "REPO=... must not be tailscale/k8s-operator" && exit 1)
@test "${REPO}" != "ghcr.io/tailscale/k8s-operator" || (echo "REPO=... must not be ghcr.io/tailscale/k8s-operator" && exit 1)
TAGS="${TAGS}" REPOS=${REPO} PLATFORM=${PLATFORM} PUSH=true TARGET=operator ./build_docker.sh
TAGS="${TAGS}" REPOS=${REPO} PLATFORM=${PLATFORM} PUSH=true TARGET=k8s-operator ./build_docker.sh
publishdevnameserver: ## Build and publish k8s-nameserver image to location specified by ${REPO}
@test -n "${REPO}" || (echo "REPO=... required; e.g. REPO=ghcr.io/${USER}/tailscale" && exit 1)
@@ -116,7 +116,6 @@ sshintegrationtest: ## Run the SSH integration tests in various Docker container
GOOS=linux GOARCH=amd64 ./tool/go build -o ssh/tailssh/testcontainers/tailscaled ./cmd/tailscaled && \
echo "Testing on ubuntu:focal" && docker build --build-arg="BASE=ubuntu:focal" -t ssh-ubuntu-focal ssh/tailssh/testcontainers && \
echo "Testing on ubuntu:jammy" && docker build --build-arg="BASE=ubuntu:jammy" -t ssh-ubuntu-jammy ssh/tailssh/testcontainers && \
echo "Testing on ubuntu:mantic" && docker build --build-arg="BASE=ubuntu:mantic" -t ssh-ubuntu-mantic ssh/tailssh/testcontainers && \
echo "Testing on ubuntu:noble" && docker build --build-arg="BASE=ubuntu:noble" -t ssh-ubuntu-noble ssh/tailssh/testcontainers && \
echo "Testing on alpine:latest" && docker build --build-arg="BASE=alpine:latest" -t ssh-alpine-latest ssh/tailssh/testcontainers

2
vendor/tailscale.com/README.md generated vendored
View File

@@ -72,7 +72,7 @@ Origin](https://en.wikipedia.org/wiki/Developer_Certificate_of_Origin)
`Signed-off-by` lines in commits.
See `git log` for our commit message style. It's basically the same as
[Go's style](https://github.com/golang/go/wiki/CommitMessage).
[Go's style](https://go.dev/wiki/CommitMessage).
## About Us

2
vendor/tailscale.com/VERSION.txt generated vendored
View File

@@ -1 +1 @@
1.76.3
1.82.0

View File

@@ -18,7 +18,6 @@ import (
"sync"
"time"
xmaps "golang.org/x/exp/maps"
"golang.org/x/net/dns/dnsmessage"
"tailscale.com/types/logger"
"tailscale.com/types/views"
@@ -290,12 +289,14 @@ func (e *AppConnector) updateDomains(domains []string) {
toRemove = append(toRemove, netip.PrefixFrom(a, a.BitLen()))
}
}
if err := e.routeAdvertiser.UnadvertiseRoute(toRemove...); err != nil {
e.logf("failed to unadvertise routes on domain removal: %v: %v: %v", xmaps.Keys(oldDomains), toRemove, err)
}
e.queue.Add(func() {
if err := e.routeAdvertiser.UnadvertiseRoute(toRemove...); err != nil {
e.logf("failed to unadvertise routes on domain removal: %v: %v: %v", slicesx.MapKeys(oldDomains), toRemove, err)
}
})
}
e.logf("handling domains: %v and wildcards: %v", xmaps.Keys(e.domains), e.wildcards)
e.logf("handling domains: %v and wildcards: %v", slicesx.MapKeys(e.domains), e.wildcards)
}
// updateRoutes merges the supplied routes into the currently configured routes. The routes supplied
@@ -311,11 +312,6 @@ func (e *AppConnector) updateRoutes(routes []netip.Prefix) {
return
}
if err := e.routeAdvertiser.AdvertiseRoute(routes...); err != nil {
e.logf("failed to advertise routes: %v: %v", routes, err)
return
}
var toRemove []netip.Prefix
// If we're storing routes and know e.controlRoutes is a good
@@ -339,9 +335,14 @@ nextRoute:
}
}
if err := e.routeAdvertiser.UnadvertiseRoute(toRemove...); err != nil {
e.logf("failed to unadvertise routes: %v: %v", toRemove, err)
}
e.queue.Add(func() {
if err := e.routeAdvertiser.AdvertiseRoute(routes...); err != nil {
e.logf("failed to advertise routes: %v: %v", routes, err)
}
if err := e.routeAdvertiser.UnadvertiseRoute(toRemove...); err != nil {
e.logf("failed to unadvertise routes: %v: %v", toRemove, err)
}
})
e.controlRoutes = routes
if err := e.storeRoutesLocked(); err != nil {
@@ -354,7 +355,7 @@ func (e *AppConnector) Domains() views.Slice[string] {
e.mu.Lock()
defer e.mu.Unlock()
return views.SliceOf(xmaps.Keys(e.domains))
return views.SliceOf(slicesx.MapKeys(e.domains))
}
// DomainRoutes returns a map of domains to resolved IP
@@ -375,13 +376,13 @@ func (e *AppConnector) DomainRoutes() map[string][]netip.Addr {
// response is being returned over the PeerAPI. The response is parsed and
// matched against the configured domains, if matched the routeAdvertiser is
// advised to advertise the discovered route.
func (e *AppConnector) ObserveDNSResponse(res []byte) {
func (e *AppConnector) ObserveDNSResponse(res []byte) error {
var p dnsmessage.Parser
if _, err := p.Start(res); err != nil {
return
return err
}
if err := p.SkipAllQuestions(); err != nil {
return
return err
}
// cnameChain tracks a chain of CNAMEs for a given query in order to reverse
@@ -400,12 +401,12 @@ func (e *AppConnector) ObserveDNSResponse(res []byte) {
break
}
if err != nil {
return
return err
}
if h.Class != dnsmessage.ClassINET {
if err := p.SkipAnswer(); err != nil {
return
return err
}
continue
}
@@ -414,7 +415,7 @@ func (e *AppConnector) ObserveDNSResponse(res []byte) {
case dnsmessage.TypeCNAME, dnsmessage.TypeA, dnsmessage.TypeAAAA:
default:
if err := p.SkipAnswer(); err != nil {
return
return err
}
continue
@@ -428,7 +429,7 @@ func (e *AppConnector) ObserveDNSResponse(res []byte) {
if h.Type == dnsmessage.TypeCNAME {
res, err := p.CNAMEResource()
if err != nil {
return
return err
}
cname := strings.TrimSuffix(strings.ToLower(res.CNAME.String()), ".")
if len(cname) == 0 {
@@ -442,20 +443,20 @@ func (e *AppConnector) ObserveDNSResponse(res []byte) {
case dnsmessage.TypeA:
r, err := p.AResource()
if err != nil {
return
return err
}
addr := netip.AddrFrom4(r.A)
mak.Set(&addressRecords, domain, append(addressRecords[domain], addr))
case dnsmessage.TypeAAAA:
r, err := p.AAAAResource()
if err != nil {
return
return err
}
addr := netip.AddrFrom16(r.AAAA)
mak.Set(&addressRecords, domain, append(addressRecords[domain], addr))
default:
if err := p.SkipAnswer(); err != nil {
return
return err
}
continue
}
@@ -486,6 +487,7 @@ func (e *AppConnector) ObserveDNSResponse(res []byte) {
e.scheduleAdvertisement(domain, toAdvertise...)
}
}
return nil
}
// starting from the given domain that resolved to an address, find it, or any

View File

@@ -15,8 +15,9 @@ import (
)
// 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.
// The perm argument is ignored on Windows, but if the target filename already
// exists then the target file's attributes and ACLs are preserved. If the target
// filename already exists but is not a regular file, WriteFile returns an error.
func WriteFile(filename string, data []byte, perm os.FileMode) (err error) {
fi, err := os.Stat(filename)
if err == nil && !fi.Mode().IsRegular() {
@@ -47,5 +48,5 @@ func WriteFile(filename string, data []byte, perm os.FileMode) (err error) {
if err := f.Close(); err != nil {
return err
}
return os.Rename(tmpName, filename)
return rename(tmpName, filename)
}

View File

@@ -0,0 +1,14 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build !windows
package atomicfile
import (
"os"
)
func rename(srcFile, destFile string) error {
return os.Rename(srcFile, destFile)
}

33
vendor/tailscale.com/atomicfile/atomicfile_windows.go generated vendored Normal file
View File

@@ -0,0 +1,33 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package atomicfile
import (
"os"
"golang.org/x/sys/windows"
)
func rename(srcFile, destFile string) error {
// Use replaceFile when possible to preserve the original file's attributes and ACLs.
if err := replaceFile(destFile, srcFile); err == nil || err != windows.ERROR_FILE_NOT_FOUND {
return err
}
// destFile doesn't exist. Just do a normal rename.
return os.Rename(srcFile, destFile)
}
func replaceFile(destFile, srcFile string) error {
destFile16, err := windows.UTF16PtrFromString(destFile)
if err != nil {
return err
}
srcFile16, err := windows.UTF16PtrFromString(srcFile)
if err != nil {
return err
}
return replaceFileW(destFile16, srcFile16, nil, 0, nil, nil)
}

8
vendor/tailscale.com/atomicfile/mksyscall.go generated vendored Normal file
View File

@@ -0,0 +1,8 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package atomicfile
//go:generate go run golang.org/x/sys/windows/mkwinsyscall -output zsyscall_windows.go mksyscall.go
//sys replaceFileW(replaced *uint16, replacement *uint16, backup *uint16, flags uint32, exclude unsafe.Pointer, reserved unsafe.Pointer) (err error) [int32(failretval)==0] = kernel32.ReplaceFileW

52
vendor/tailscale.com/atomicfile/zsyscall_windows.go generated vendored Normal file
View File

@@ -0,0 +1,52 @@
// Code generated by 'go generate'; DO NOT EDIT.
package atomicfile
import (
"syscall"
"unsafe"
"golang.org/x/sys/windows"
)
var _ unsafe.Pointer
// Do the interface allocations only once for common
// Errno values.
const (
errnoERROR_IO_PENDING = 997
)
var (
errERROR_IO_PENDING error = syscall.Errno(errnoERROR_IO_PENDING)
errERROR_EINVAL error = syscall.EINVAL
)
// errnoErr returns common boxed Errno values, to prevent
// allocations at runtime.
func errnoErr(e syscall.Errno) error {
switch e {
case 0:
return errERROR_EINVAL
case errnoERROR_IO_PENDING:
return errERROR_IO_PENDING
}
// TODO: add more here, after collecting data on the common
// error values see on Windows. (perhaps when running
// all.bat?)
return e
}
var (
modkernel32 = windows.NewLazySystemDLL("kernel32.dll")
procReplaceFileW = modkernel32.NewProc("ReplaceFileW")
)
func replaceFileW(replaced *uint16, replacement *uint16, backup *uint16, flags uint32, exclude unsafe.Pointer, reserved unsafe.Pointer) (err error) {
r1, _, e1 := syscall.Syscall6(procReplaceFileW.Addr(), 6, uintptr(unsafe.Pointer(replaced)), uintptr(unsafe.Pointer(replacement)), uintptr(unsafe.Pointer(backup)), uintptr(flags), uintptr(exclude), uintptr(reserved))
if int32(r1) == 0 {
err = errnoErr(e1)
}
return
}

2
vendor/tailscale.com/build_dist.sh generated vendored
View File

@@ -37,7 +37,7 @@ while [ "$#" -gt 1 ]; do
--extra-small)
shift
ldflags="$ldflags -w -s"
tags="${tags:+$tags,}ts_omit_aws,ts_omit_bird,ts_omit_tap,ts_omit_kube,ts_omit_completion"
tags="${tags:+$tags,}ts_omit_aws,ts_omit_bird,ts_omit_tap,ts_omit_kube,ts_omit_completion,ts_omit_ssh,ts_omit_wakeonlan,ts_omit_capture"
;;
--box)
shift

17
vendor/tailscale.com/build_docker.sh generated vendored
View File

@@ -16,13 +16,21 @@ eval "$(./build_dist.sh shellvars)"
DEFAULT_TARGET="client"
DEFAULT_TAGS="v${VERSION_SHORT},v${VERSION_MINOR}"
DEFAULT_BASE="tailscale/alpine-base:3.18"
DEFAULT_BASE="tailscale/alpine-base:3.19"
# Set a few pre-defined OCI annotations. The source annotation is used by tools such as Renovate that scan the linked
# Github repo to find release notes for any new image tags. Note that for official Tailscale images the default
# annotations defined here will be overriden by release scripts that call this script.
# https://github.com/opencontainers/image-spec/blob/main/annotations.md#pre-defined-annotation-keys
DEFAULT_ANNOTATIONS="org.opencontainers.image.source=https://github.com/tailscale/tailscale/blob/main/build_docker.sh,org.opencontainers.image.vendor=Tailscale"
PUSH="${PUSH:-false}"
TARGET="${TARGET:-${DEFAULT_TARGET}}"
TAGS="${TAGS:-${DEFAULT_TAGS}}"
BASE="${BASE:-${DEFAULT_BASE}}"
PLATFORM="${PLATFORM:-}" # default to all platforms
# OCI annotations that will be added to the image.
# https://github.com/opencontainers/image-spec/blob/main/annotations.md
ANNOTATIONS="${ANNOTATIONS:-${DEFAULT_ANNOTATIONS}}"
case "$TARGET" in
client)
@@ -43,9 +51,10 @@ case "$TARGET" in
--repos="${REPOS}" \
--push="${PUSH}" \
--target="${PLATFORM}" \
--annotations="${ANNOTATIONS}" \
/usr/local/bin/containerboot
;;
operator)
k8s-operator)
DEFAULT_REPOS="tailscale/k8s-operator"
REPOS="${REPOS:-${DEFAULT_REPOS}}"
go run github.com/tailscale/mkctr \
@@ -56,9 +65,11 @@ case "$TARGET" in
-X tailscale.com/version.gitCommitStamp=${VERSION_GIT_HASH}" \
--base="${BASE}" \
--tags="${TAGS}" \
--gotags="ts_kube,ts_package_container" \
--repos="${REPOS}" \
--push="${PUSH}" \
--target="${PLATFORM}" \
--annotations="${ANNOTATIONS}" \
/usr/local/bin/operator
;;
k8s-nameserver)
@@ -72,9 +83,11 @@ case "$TARGET" in
-X tailscale.com/version.gitCommitStamp=${VERSION_GIT_HASH}" \
--base="${BASE}" \
--tags="${TAGS}" \
--gotags="ts_kube,ts_package_container" \
--repos="${REPOS}" \
--push="${PUSH}" \
--target="${PLATFORM}" \
--annotations="${ANNOTATIONS}" \
/usr/local/bin/k8s-nameserver
;;
*)

View File

@@ -1,15 +1,17 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build go1.19
//go:build go1.22
package tailscale
// Package local contains a Go client for the Tailscale LocalAPI.
package local
import (
"bytes"
"cmp"
"context"
"crypto/tls"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
@@ -40,13 +42,14 @@ import (
"tailscale.com/types/dnstype"
"tailscale.com/types/key"
"tailscale.com/types/tkatype"
"tailscale.com/util/syspolicy/setting"
)
// defaultLocalClient is the default LocalClient when using the legacy
// defaultClient is the default Client when using the legacy
// package-level functions.
var defaultLocalClient LocalClient
var defaultClient Client
// LocalClient is a client to Tailscale's "LocalAPI", communicating with the
// Client is a client to Tailscale's "LocalAPI", communicating with the
// Tailscale daemon on the local machine. Its API is not necessarily stable and
// subject to changes between releases. Some API calls have stricter
// compatibility guarantees, once they've been widely adopted. See method docs
@@ -56,11 +59,17 @@ var defaultLocalClient LocalClient
//
// Any exported fields should be set before using methods on the type
// and not changed thereafter.
type LocalClient struct {
type Client struct {
// Dial optionally specifies an alternate func that connects to the local
// machine's tailscaled or equivalent. If nil, a default is used.
Dial func(ctx context.Context, network, addr string) (net.Conn, error)
// Transport optionally specifies an alternate [http.RoundTripper]
// used to execute HTTP requests. If nil, a default [http.Transport] is used,
// potentially with custom dialing logic from [Dial].
// It is primarily used for testing.
Transport http.RoundTripper
// Socket specifies an alternate path to the local Tailscale socket.
// If empty, a platform-specific default is used.
Socket string
@@ -84,21 +93,21 @@ type LocalClient struct {
tsClientOnce sync.Once
}
func (lc *LocalClient) socket() string {
func (lc *Client) socket() string {
if lc.Socket != "" {
return lc.Socket
}
return paths.DefaultTailscaledSocket()
}
func (lc *LocalClient) dialer() func(ctx context.Context, network, addr string) (net.Conn, error) {
func (lc *Client) dialer() func(ctx context.Context, network, addr string) (net.Conn, error) {
if lc.Dial != nil {
return lc.Dial
}
return lc.defaultDialer
}
func (lc *LocalClient) defaultDialer(ctx context.Context, network, addr string) (net.Conn, error) {
func (lc *Client) defaultDialer(ctx context.Context, network, addr string) (net.Conn, error) {
if addr != "local-tailscaled.sock:80" {
return nil, fmt.Errorf("unexpected URL address %q", addr)
}
@@ -124,13 +133,13 @@ func (lc *LocalClient) defaultDialer(ctx context.Context, network, addr string)
// authenticating to the local Tailscale daemon vary by platform.
//
// DoLocalRequest may mutate the request to add Authorization headers.
func (lc *LocalClient) DoLocalRequest(req *http.Request) (*http.Response, error) {
func (lc *Client) DoLocalRequest(req *http.Request) (*http.Response, error) {
req.Header.Set("Tailscale-Cap", strconv.Itoa(int(tailcfg.CurrentCapabilityVersion)))
lc.tsClientOnce.Do(func() {
lc.tsClient = &http.Client{
Transport: &http.Transport{
DialContext: lc.dialer(),
},
Transport: cmp.Or(lc.Transport, http.RoundTripper(
&http.Transport{DialContext: lc.dialer()}),
),
}
})
if !lc.OmitAuth {
@@ -141,7 +150,7 @@ func (lc *LocalClient) DoLocalRequest(req *http.Request) (*http.Response, error)
return lc.tsClient.Do(req)
}
func (lc *LocalClient) doLocalRequestNiceError(req *http.Request) (*http.Response, error) {
func (lc *Client) doLocalRequestNiceError(req *http.Request) (*http.Response, error) {
res, err := lc.DoLocalRequest(req)
if err == nil {
if server := res.Header.Get("Tailscale-Version"); server != "" && server != envknob.IPCVersion() && onVersionMismatch != nil {
@@ -230,12 +239,17 @@ func SetVersionMismatchHandler(f func(clientVer, serverVer string)) {
onVersionMismatch = f
}
func (lc *LocalClient) send(ctx context.Context, method, path string, wantStatus int, body io.Reader) ([]byte, error) {
slurp, _, err := lc.sendWithHeaders(ctx, method, path, wantStatus, body, nil)
func (lc *Client) send(ctx context.Context, method, path string, wantStatus int, body io.Reader) ([]byte, error) {
var headers http.Header
if reason := apitype.RequestReasonKey.Value(ctx); reason != "" {
reasonBase64 := base64.StdEncoding.EncodeToString([]byte(reason))
headers = http.Header{apitype.RequestReasonHeader: {reasonBase64}}
}
slurp, _, err := lc.sendWithHeaders(ctx, method, path, wantStatus, body, headers)
return slurp, err
}
func (lc *LocalClient) sendWithHeaders(
func (lc *Client) sendWithHeaders(
ctx context.Context,
method,
path string,
@@ -274,15 +288,15 @@ type httpStatusError struct {
HTTPStatus int
}
func (lc *LocalClient) get200(ctx context.Context, path string) ([]byte, error) {
func (lc *Client) get200(ctx context.Context, path string) ([]byte, error) {
return lc.send(ctx, "GET", path, 200, nil)
}
// WhoIs returns the owner of the remoteAddr, which must be an IP or IP:port.
//
// Deprecated: use LocalClient.WhoIs.
// Deprecated: use Client.WhoIs.
func WhoIs(ctx context.Context, remoteAddr string) (*apitype.WhoIsResponse, error) {
return defaultLocalClient.WhoIs(ctx, remoteAddr)
return defaultClient.WhoIs(ctx, remoteAddr)
}
func decodeJSON[T any](b []byte) (ret T, err error) {
@@ -300,7 +314,7 @@ func decodeJSON[T any](b []byte) (ret T, err error) {
// For connections proxied by tailscaled, this looks up the owner of the given
// address as TCP first, falling back to UDP; if you want to only check a
// specific address family, use WhoIsProto.
func (lc *LocalClient) WhoIs(ctx context.Context, remoteAddr string) (*apitype.WhoIsResponse, error) {
func (lc *Client) WhoIs(ctx context.Context, remoteAddr string) (*apitype.WhoIsResponse, error) {
body, err := lc.get200(ctx, "/localapi/v0/whois?addr="+url.QueryEscape(remoteAddr))
if err != nil {
if hs, ok := err.(httpStatusError); ok && hs.HTTPStatus == http.StatusNotFound {
@@ -317,7 +331,7 @@ var ErrPeerNotFound = errors.New("peer not found")
// WhoIsNodeKey returns the owner of the given wireguard public key.
//
// If not found, the error is ErrPeerNotFound.
func (lc *LocalClient) WhoIsNodeKey(ctx context.Context, key key.NodePublic) (*apitype.WhoIsResponse, error) {
func (lc *Client) WhoIsNodeKey(ctx context.Context, key key.NodePublic) (*apitype.WhoIsResponse, error) {
body, err := lc.get200(ctx, "/localapi/v0/whois?addr="+url.QueryEscape(key.String()))
if err != nil {
if hs, ok := err.(httpStatusError); ok && hs.HTTPStatus == http.StatusNotFound {
@@ -332,7 +346,7 @@ func (lc *LocalClient) WhoIsNodeKey(ctx context.Context, key key.NodePublic) (*a
// IP:port, for the given protocol (tcp or udp).
//
// If not found, the error is ErrPeerNotFound.
func (lc *LocalClient) WhoIsProto(ctx context.Context, proto, remoteAddr string) (*apitype.WhoIsResponse, error) {
func (lc *Client) WhoIsProto(ctx context.Context, proto, remoteAddr string) (*apitype.WhoIsResponse, error) {
body, err := lc.get200(ctx, "/localapi/v0/whois?proto="+url.QueryEscape(proto)+"&addr="+url.QueryEscape(remoteAddr))
if err != nil {
if hs, ok := err.(httpStatusError); ok && hs.HTTPStatus == http.StatusNotFound {
@@ -344,19 +358,19 @@ func (lc *LocalClient) WhoIsProto(ctx context.Context, proto, remoteAddr string)
}
// Goroutines returns a dump of the Tailscale daemon's current goroutines.
func (lc *LocalClient) Goroutines(ctx context.Context) ([]byte, error) {
func (lc *Client) Goroutines(ctx context.Context) ([]byte, error) {
return lc.get200(ctx, "/localapi/v0/goroutines")
}
// DaemonMetrics returns the Tailscale daemon's metrics in
// the Prometheus text exposition format.
func (lc *LocalClient) DaemonMetrics(ctx context.Context) ([]byte, error) {
func (lc *Client) DaemonMetrics(ctx context.Context) ([]byte, error) {
return lc.get200(ctx, "/localapi/v0/metrics")
}
// UserMetrics returns the user metrics in
// the Prometheus text exposition format.
func (lc *LocalClient) UserMetrics(ctx context.Context) ([]byte, error) {
func (lc *Client) UserMetrics(ctx context.Context) ([]byte, error) {
return lc.get200(ctx, "/localapi/v0/usermetrics")
}
@@ -365,7 +379,7 @@ func (lc *LocalClient) UserMetrics(ctx context.Context) ([]byte, error) {
// metric is created and initialized to delta.
//
// IncrementCounter does not support gauge metrics or negative delta values.
func (lc *LocalClient) IncrementCounter(ctx context.Context, name string, delta int) error {
func (lc *Client) IncrementCounter(ctx context.Context, name string, delta int) error {
type metricUpdate struct {
Name string `json:"name"`
Type string `json:"type"`
@@ -384,7 +398,7 @@ func (lc *LocalClient) IncrementCounter(ctx context.Context, name string, delta
// TailDaemonLogs returns a stream the Tailscale daemon's logs as they arrive.
// Close the context to stop the stream.
func (lc *LocalClient) TailDaemonLogs(ctx context.Context) (io.Reader, error) {
func (lc *Client) TailDaemonLogs(ctx context.Context) (io.Reader, error) {
req, err := http.NewRequestWithContext(ctx, "GET", "http://"+apitype.LocalAPIHost+"/localapi/v0/logtap", nil)
if err != nil {
return nil, err
@@ -400,7 +414,7 @@ func (lc *LocalClient) TailDaemonLogs(ctx context.Context) (io.Reader, error) {
}
// Pprof returns a pprof profile of the Tailscale daemon.
func (lc *LocalClient) Pprof(ctx context.Context, pprofType string, sec int) ([]byte, error) {
func (lc *Client) Pprof(ctx context.Context, pprofType string, sec int) ([]byte, error) {
var secArg string
if sec < 0 || sec > 300 {
return nil, errors.New("duration out of range")
@@ -433,7 +447,7 @@ type BugReportOpts struct {
//
// The opts type specifies options to pass to the Tailscale daemon when
// generating this bug report.
func (lc *LocalClient) BugReportWithOpts(ctx context.Context, opts BugReportOpts) (string, error) {
func (lc *Client) BugReportWithOpts(ctx context.Context, opts BugReportOpts) (string, error) {
qparams := make(url.Values)
if opts.Note != "" {
qparams.Set("note", opts.Note)
@@ -478,13 +492,13 @@ func (lc *LocalClient) BugReportWithOpts(ctx context.Context, opts BugReportOpts
//
// This is the same as calling BugReportWithOpts and only specifying the Note
// field.
func (lc *LocalClient) BugReport(ctx context.Context, note string) (string, error) {
func (lc *Client) BugReport(ctx context.Context, note string) (string, error) {
return lc.BugReportWithOpts(ctx, BugReportOpts{Note: note})
}
// DebugAction invokes a debug action, such as "rebind" or "restun".
// These are development tools and subject to change or removal over time.
func (lc *LocalClient) DebugAction(ctx context.Context, action string) error {
func (lc *Client) DebugAction(ctx context.Context, action string) error {
body, err := lc.send(ctx, "POST", "/localapi/v0/debug?action="+url.QueryEscape(action), 200, nil)
if err != nil {
return fmt.Errorf("error %w: %s", err, body)
@@ -492,9 +506,20 @@ func (lc *LocalClient) DebugAction(ctx context.Context, action string) error {
return nil
}
// DebugActionBody invokes a debug action with a body parameter, such as
// "debug-force-prefer-derp".
// These are development tools and subject to change or removal over time.
func (lc *Client) DebugActionBody(ctx context.Context, action string, rbody io.Reader) error {
body, err := lc.send(ctx, "POST", "/localapi/v0/debug?action="+url.QueryEscape(action), 200, rbody)
if err != nil {
return fmt.Errorf("error %w: %s", err, body)
}
return nil
}
// DebugResultJSON invokes a debug action and returns its result as something JSON-able.
// These are development tools and subject to change or removal over time.
func (lc *LocalClient) DebugResultJSON(ctx context.Context, action string) (any, error) {
func (lc *Client) DebugResultJSON(ctx context.Context, action string) (any, error) {
body, err := lc.send(ctx, "POST", "/localapi/v0/debug?action="+url.QueryEscape(action), 200, nil)
if err != nil {
return nil, fmt.Errorf("error %w: %s", err, body)
@@ -537,7 +562,7 @@ type DebugPortmapOpts struct {
// process.
//
// opts can be nil; if so, default values will be used.
func (lc *LocalClient) DebugPortmap(ctx context.Context, opts *DebugPortmapOpts) (io.ReadCloser, error) {
func (lc *Client) DebugPortmap(ctx context.Context, opts *DebugPortmapOpts) (io.ReadCloser, error) {
vals := make(url.Values)
if opts == nil {
opts = &DebugPortmapOpts{}
@@ -572,7 +597,7 @@ func (lc *LocalClient) DebugPortmap(ctx context.Context, opts *DebugPortmapOpts)
// SetDevStoreKeyValue set a statestore key/value. It's only meant for development.
// The schema (including when keys are re-read) is not a stable interface.
func (lc *LocalClient) SetDevStoreKeyValue(ctx context.Context, key, value string) error {
func (lc *Client) SetDevStoreKeyValue(ctx context.Context, key, value string) error {
body, err := lc.send(ctx, "POST", "/localapi/v0/dev-set-state-store?"+(url.Values{
"key": {key},
"value": {value},
@@ -586,7 +611,7 @@ func (lc *LocalClient) SetDevStoreKeyValue(ctx context.Context, key, value strin
// SetComponentDebugLogging sets component's debug logging enabled for
// the provided duration. If the duration is in the past, the debug logging
// is disabled.
func (lc *LocalClient) SetComponentDebugLogging(ctx context.Context, component string, d time.Duration) error {
func (lc *Client) SetComponentDebugLogging(ctx context.Context, component string, d time.Duration) error {
body, err := lc.send(ctx, "POST",
fmt.Sprintf("/localapi/v0/component-debug-logging?component=%s&secs=%d",
url.QueryEscape(component), int64(d.Seconds())), 200, nil)
@@ -607,25 +632,25 @@ func (lc *LocalClient) SetComponentDebugLogging(ctx context.Context, component s
// Status returns the Tailscale daemon's status.
func Status(ctx context.Context) (*ipnstate.Status, error) {
return defaultLocalClient.Status(ctx)
return defaultClient.Status(ctx)
}
// Status returns the Tailscale daemon's status.
func (lc *LocalClient) Status(ctx context.Context) (*ipnstate.Status, error) {
func (lc *Client) Status(ctx context.Context) (*ipnstate.Status, error) {
return lc.status(ctx, "")
}
// StatusWithoutPeers returns the Tailscale daemon's status, without the peer info.
func StatusWithoutPeers(ctx context.Context) (*ipnstate.Status, error) {
return defaultLocalClient.StatusWithoutPeers(ctx)
return defaultClient.StatusWithoutPeers(ctx)
}
// StatusWithoutPeers returns the Tailscale daemon's status, without the peer info.
func (lc *LocalClient) StatusWithoutPeers(ctx context.Context) (*ipnstate.Status, error) {
func (lc *Client) StatusWithoutPeers(ctx context.Context) (*ipnstate.Status, error) {
return lc.status(ctx, "?peers=false")
}
func (lc *LocalClient) status(ctx context.Context, queryString string) (*ipnstate.Status, error) {
func (lc *Client) status(ctx context.Context, queryString string) (*ipnstate.Status, error) {
body, err := lc.get200(ctx, "/localapi/v0/status"+queryString)
if err != nil {
return nil, err
@@ -636,7 +661,7 @@ func (lc *LocalClient) status(ctx context.Context, queryString string) (*ipnstat
// IDToken is a request to get an OIDC ID token for an audience.
// The token can be presented to any resource provider which offers OIDC
// Federation.
func (lc *LocalClient) IDToken(ctx context.Context, aud string) (*tailcfg.TokenResponse, error) {
func (lc *Client) IDToken(ctx context.Context, aud string) (*tailcfg.TokenResponse, error) {
body, err := lc.get200(ctx, "/localapi/v0/id-token?aud="+url.QueryEscape(aud))
if err != nil {
return nil, err
@@ -648,14 +673,14 @@ func (lc *LocalClient) IDToken(ctx context.Context, aud string) (*tailcfg.TokenR
// received by the Tailscale daemon in its staging/cache directory but not yet
// transferred by the user's CLI or GUI client and written to a user's home
// directory somewhere.
func (lc *LocalClient) WaitingFiles(ctx context.Context) ([]apitype.WaitingFile, error) {
func (lc *Client) WaitingFiles(ctx context.Context) ([]apitype.WaitingFile, error) {
return lc.AwaitWaitingFiles(ctx, 0)
}
// AwaitWaitingFiles is like WaitingFiles but takes a duration to await for an answer.
// If the duration is 0, it will return immediately. The duration is respected at second
// granularity only. If no files are available, it returns (nil, nil).
func (lc *LocalClient) AwaitWaitingFiles(ctx context.Context, d time.Duration) ([]apitype.WaitingFile, error) {
func (lc *Client) AwaitWaitingFiles(ctx context.Context, d time.Duration) ([]apitype.WaitingFile, error) {
path := "/localapi/v0/files/?waitsec=" + fmt.Sprint(int(d.Seconds()))
body, err := lc.get200(ctx, path)
if err != nil {
@@ -664,12 +689,12 @@ func (lc *LocalClient) AwaitWaitingFiles(ctx context.Context, d time.Duration) (
return decodeJSON[[]apitype.WaitingFile](body)
}
func (lc *LocalClient) DeleteWaitingFile(ctx context.Context, baseName string) error {
func (lc *Client) DeleteWaitingFile(ctx context.Context, baseName string) error {
_, err := lc.send(ctx, "DELETE", "/localapi/v0/files/"+url.PathEscape(baseName), http.StatusNoContent, nil)
return err
}
func (lc *LocalClient) GetWaitingFile(ctx context.Context, baseName string) (rc io.ReadCloser, size int64, err error) {
func (lc *Client) GetWaitingFile(ctx context.Context, baseName string) (rc io.ReadCloser, size int64, err error) {
req, err := http.NewRequestWithContext(ctx, "GET", "http://"+apitype.LocalAPIHost+"/localapi/v0/files/"+url.PathEscape(baseName), nil)
if err != nil {
return nil, 0, err
@@ -690,7 +715,7 @@ func (lc *LocalClient) GetWaitingFile(ctx context.Context, baseName string) (rc
return res.Body, res.ContentLength, nil
}
func (lc *LocalClient) FileTargets(ctx context.Context) ([]apitype.FileTarget, error) {
func (lc *Client) FileTargets(ctx context.Context) ([]apitype.FileTarget, error) {
body, err := lc.get200(ctx, "/localapi/v0/file-targets")
if err != nil {
return nil, err
@@ -702,7 +727,7 @@ func (lc *LocalClient) FileTargets(ctx context.Context) ([]apitype.FileTarget, e
//
// A size of -1 means unknown.
// The name parameter is the original filename, not escaped.
func (lc *LocalClient) PushFile(ctx context.Context, target tailcfg.StableNodeID, size int64, name string, r io.Reader) error {
func (lc *Client) PushFile(ctx context.Context, target tailcfg.StableNodeID, size int64, name string, r io.Reader) error {
req, err := http.NewRequestWithContext(ctx, "PUT", "http://"+apitype.LocalAPIHost+"/localapi/v0/file-put/"+string(target)+"/"+url.PathEscape(name), r)
if err != nil {
return err
@@ -725,7 +750,7 @@ func (lc *LocalClient) PushFile(ctx context.Context, target tailcfg.StableNodeID
// CheckIPForwarding asks the local Tailscale daemon whether it looks like the
// machine is properly configured to forward IP packets as a subnet router
// or exit node.
func (lc *LocalClient) CheckIPForwarding(ctx context.Context) error {
func (lc *Client) CheckIPForwarding(ctx context.Context) error {
body, err := lc.get200(ctx, "/localapi/v0/check-ip-forwarding")
if err != nil {
return err
@@ -745,7 +770,7 @@ func (lc *LocalClient) CheckIPForwarding(ctx context.Context) error {
// CheckUDPGROForwarding asks the local Tailscale daemon whether it looks like
// the machine is optimally configured to forward UDP packets as a subnet router
// or exit node.
func (lc *LocalClient) CheckUDPGROForwarding(ctx context.Context) error {
func (lc *Client) CheckUDPGROForwarding(ctx context.Context) error {
body, err := lc.get200(ctx, "/localapi/v0/check-udp-gro-forwarding")
if err != nil {
return err
@@ -766,7 +791,7 @@ func (lc *LocalClient) CheckUDPGROForwarding(ctx context.Context) error {
// node. This can be done to improve performance of tailnet nodes acting as exit
// nodes or subnet routers.
// See https://tailscale.com/kb/1320/performance-best-practices#linux-optimizations-for-subnet-routers-and-exit-nodes
func (lc *LocalClient) SetUDPGROForwarding(ctx context.Context) error {
func (lc *Client) SetUDPGROForwarding(ctx context.Context) error {
body, err := lc.get200(ctx, "/localapi/v0/set-udp-gro-forwarding")
if err != nil {
return err
@@ -789,12 +814,12 @@ func (lc *LocalClient) SetUDPGROForwarding(ctx context.Context) error {
// work. Currently (2022-04-18) this only checks for SSH server compatibility.
// Note that EditPrefs does the same validation as this, so call CheckPrefs before
// EditPrefs is not necessary.
func (lc *LocalClient) CheckPrefs(ctx context.Context, p *ipn.Prefs) error {
func (lc *Client) CheckPrefs(ctx context.Context, p *ipn.Prefs) error {
_, err := lc.send(ctx, "POST", "/localapi/v0/check-prefs", http.StatusOK, jsonBody(p))
return err
}
func (lc *LocalClient) GetPrefs(ctx context.Context) (*ipn.Prefs, error) {
func (lc *Client) GetPrefs(ctx context.Context) (*ipn.Prefs, error) {
body, err := lc.get200(ctx, "/localapi/v0/prefs")
if err != nil {
return nil, err
@@ -806,7 +831,12 @@ func (lc *LocalClient) GetPrefs(ctx context.Context) (*ipn.Prefs, error) {
return &p, nil
}
func (lc *LocalClient) EditPrefs(ctx context.Context, mp *ipn.MaskedPrefs) (*ipn.Prefs, error) {
// EditPrefs updates the [ipn.Prefs] of the current Tailscale profile, applying the changes in mp.
// It returns an error if the changes cannot be applied, such as due to the caller's access rights
// or a policy restriction. An optional reason or justification for the request can be
// provided as a context value using [apitype.RequestReasonKey]. If permitted by policy,
// access may be granted, and the reason will be logged for auditing purposes.
func (lc *Client) EditPrefs(ctx context.Context, mp *ipn.MaskedPrefs) (*ipn.Prefs, error) {
body, err := lc.send(ctx, "PATCH", "/localapi/v0/prefs", http.StatusOK, jsonBody(mp))
if err != nil {
return nil, err
@@ -814,9 +844,36 @@ func (lc *LocalClient) EditPrefs(ctx context.Context, mp *ipn.MaskedPrefs) (*ipn
return decodeJSON[*ipn.Prefs](body)
}
// GetEffectivePolicy returns the effective policy for the specified scope.
func (lc *Client) GetEffectivePolicy(ctx context.Context, scope setting.PolicyScope) (*setting.Snapshot, error) {
scopeID, err := scope.MarshalText()
if err != nil {
return nil, err
}
body, err := lc.get200(ctx, "/localapi/v0/policy/"+string(scopeID))
if err != nil {
return nil, err
}
return decodeJSON[*setting.Snapshot](body)
}
// ReloadEffectivePolicy reloads the effective policy for the specified scope
// by reading and merging policy settings from all applicable policy sources.
func (lc *Client) ReloadEffectivePolicy(ctx context.Context, scope setting.PolicyScope) (*setting.Snapshot, error) {
scopeID, err := scope.MarshalText()
if err != nil {
return nil, err
}
body, err := lc.send(ctx, "POST", "/localapi/v0/policy/"+string(scopeID), 200, http.NoBody)
if err != nil {
return nil, err
}
return decodeJSON[*setting.Snapshot](body)
}
// GetDNSOSConfig returns the system DNS configuration for the current device.
// That is, it returns the DNS configuration that the system would use if Tailscale weren't being used.
func (lc *LocalClient) GetDNSOSConfig(ctx context.Context) (*apitype.DNSOSConfig, error) {
func (lc *Client) GetDNSOSConfig(ctx context.Context) (*apitype.DNSOSConfig, error) {
body, err := lc.get200(ctx, "/localapi/v0/dns-osconfig")
if err != nil {
return nil, err
@@ -831,7 +888,7 @@ func (lc *LocalClient) GetDNSOSConfig(ctx context.Context) (*apitype.DNSOSConfig
// QueryDNS executes a DNS query for a name (`google.com.`) and query type (`CNAME`).
// It returns the raw DNS response bytes and the resolvers that were used to answer the query
// (often just one, but can be more if we raced multiple resolvers).
func (lc *LocalClient) QueryDNS(ctx context.Context, name string, queryType string) (bytes []byte, resolvers []*dnstype.Resolver, err error) {
func (lc *Client) QueryDNS(ctx context.Context, name string, queryType string) (bytes []byte, resolvers []*dnstype.Resolver, err error) {
body, err := lc.get200(ctx, fmt.Sprintf("/localapi/v0/dns-query?name=%s&type=%s", url.QueryEscape(name), queryType))
if err != nil {
return nil, nil, err
@@ -844,20 +901,20 @@ func (lc *LocalClient) QueryDNS(ctx context.Context, name string, queryType stri
}
// StartLoginInteractive starts an interactive login.
func (lc *LocalClient) StartLoginInteractive(ctx context.Context) error {
func (lc *Client) StartLoginInteractive(ctx context.Context) error {
_, err := lc.send(ctx, "POST", "/localapi/v0/login-interactive", http.StatusNoContent, nil)
return err
}
// Start applies the configuration specified in opts, and starts the
// state machine.
func (lc *LocalClient) Start(ctx context.Context, opts ipn.Options) error {
func (lc *Client) Start(ctx context.Context, opts ipn.Options) error {
_, err := lc.send(ctx, "POST", "/localapi/v0/start", http.StatusNoContent, jsonBody(opts))
return err
}
// Logout logs out the current node.
func (lc *LocalClient) Logout(ctx context.Context) error {
func (lc *Client) Logout(ctx context.Context) error {
_, err := lc.send(ctx, "POST", "/localapi/v0/logout", http.StatusNoContent, nil)
return err
}
@@ -876,7 +933,7 @@ func (lc *LocalClient) Logout(ctx context.Context) error {
// This is a low-level interface; it's expected that most Tailscale
// users use a higher level interface to getting/using TLS
// certificates.
func (lc *LocalClient) SetDNS(ctx context.Context, name, value string) error {
func (lc *Client) SetDNS(ctx context.Context, name, value string) error {
v := url.Values{}
v.Set("name", name)
v.Set("value", value)
@@ -890,7 +947,7 @@ func (lc *LocalClient) SetDNS(ctx context.Context, name, value string) error {
// tailscaled), a FQDN, or an IP address.
//
// The ctx is only used for the duration of the call, not the lifetime of the net.Conn.
func (lc *LocalClient) DialTCP(ctx context.Context, host string, port uint16) (net.Conn, error) {
func (lc *Client) DialTCP(ctx context.Context, host string, port uint16) (net.Conn, error) {
return lc.UserDial(ctx, "tcp", host, port)
}
@@ -901,7 +958,7 @@ func (lc *LocalClient) DialTCP(ctx context.Context, host string, port uint16) (n
//
// The ctx is only used for the duration of the call, not the lifetime of the
// net.Conn.
func (lc *LocalClient) UserDial(ctx context.Context, network, host string, port uint16) (net.Conn, error) {
func (lc *Client) UserDial(ctx context.Context, network, host string, port uint16) (net.Conn, error) {
connCh := make(chan net.Conn, 1)
trace := httptrace.ClientTrace{
GotConn: func(info httptrace.GotConnInfo) {
@@ -952,7 +1009,7 @@ func (lc *LocalClient) UserDial(ctx context.Context, network, host string, port
// CurrentDERPMap returns the current DERPMap that is being used by the local tailscaled.
// It is intended to be used with netcheck to see availability of DERPs.
func (lc *LocalClient) CurrentDERPMap(ctx context.Context) (*tailcfg.DERPMap, error) {
func (lc *Client) CurrentDERPMap(ctx context.Context) (*tailcfg.DERPMap, error) {
var derpMap tailcfg.DERPMap
res, err := lc.send(ctx, "GET", "/localapi/v0/derpmap", 200, nil)
if err != nil {
@@ -968,9 +1025,9 @@ func (lc *LocalClient) CurrentDERPMap(ctx context.Context) (*tailcfg.DERPMap, er
//
// It returns a cached certificate from disk if it's still valid.
//
// Deprecated: use LocalClient.CertPair.
// Deprecated: use Client.CertPair.
func CertPair(ctx context.Context, domain string) (certPEM, keyPEM []byte, err error) {
return defaultLocalClient.CertPair(ctx, domain)
return defaultClient.CertPair(ctx, domain)
}
// CertPair returns a cert and private key for the provided DNS domain.
@@ -978,7 +1035,7 @@ func CertPair(ctx context.Context, domain string) (certPEM, keyPEM []byte, err e
// It returns a cached certificate from disk if it's still valid.
//
// API maturity: this is considered a stable API.
func (lc *LocalClient) CertPair(ctx context.Context, domain string) (certPEM, keyPEM []byte, err error) {
func (lc *Client) CertPair(ctx context.Context, domain string) (certPEM, keyPEM []byte, err error) {
return lc.CertPairWithValidity(ctx, domain, 0)
}
@@ -991,7 +1048,7 @@ func (lc *LocalClient) CertPair(ctx context.Context, domain string) (certPEM, ke
// valid, but for less than minValidity, it will be synchronously renewed.
//
// API maturity: this is considered a stable API.
func (lc *LocalClient) CertPairWithValidity(ctx context.Context, domain string, minValidity time.Duration) (certPEM, keyPEM []byte, err error) {
func (lc *Client) CertPairWithValidity(ctx context.Context, domain string, minValidity time.Duration) (certPEM, keyPEM []byte, err error) {
res, err := lc.send(ctx, "GET", fmt.Sprintf("/localapi/v0/cert/%s?type=pair&min_validity=%s", domain, minValidity), 200, nil)
if err != nil {
return nil, nil, err
@@ -1017,9 +1074,9 @@ func (lc *LocalClient) CertPairWithValidity(ctx context.Context, domain string,
// It's the right signature to use as the value of
// tls.Config.GetCertificate.
//
// Deprecated: use LocalClient.GetCertificate.
// Deprecated: use Client.GetCertificate.
func GetCertificate(hi *tls.ClientHelloInfo) (*tls.Certificate, error) {
return defaultLocalClient.GetCertificate(hi)
return defaultClient.GetCertificate(hi)
}
// GetCertificate fetches a TLS certificate for the TLS ClientHello in hi.
@@ -1030,7 +1087,7 @@ func GetCertificate(hi *tls.ClientHelloInfo) (*tls.Certificate, error) {
// tls.Config.GetCertificate.
//
// API maturity: this is considered a stable API.
func (lc *LocalClient) GetCertificate(hi *tls.ClientHelloInfo) (*tls.Certificate, error) {
func (lc *Client) GetCertificate(hi *tls.ClientHelloInfo) (*tls.Certificate, error) {
if hi == nil || hi.ServerName == "" {
return nil, errors.New("no SNI ServerName")
}
@@ -1056,13 +1113,13 @@ func (lc *LocalClient) GetCertificate(hi *tls.ClientHelloInfo) (*tls.Certificate
// ExpandSNIName expands bare label name into the most likely actual TLS cert name.
//
// Deprecated: use LocalClient.ExpandSNIName.
// Deprecated: use Client.ExpandSNIName.
func ExpandSNIName(ctx context.Context, name string) (fqdn string, ok bool) {
return defaultLocalClient.ExpandSNIName(ctx, name)
return defaultClient.ExpandSNIName(ctx, name)
}
// ExpandSNIName expands bare label name into the most likely actual TLS cert name.
func (lc *LocalClient) ExpandSNIName(ctx context.Context, name string) (fqdn string, ok bool) {
func (lc *Client) ExpandSNIName(ctx context.Context, name string) (fqdn string, ok bool) {
st, err := lc.StatusWithoutPeers(ctx)
if err != nil {
return "", false
@@ -1090,7 +1147,7 @@ type PingOpts struct {
// Ping sends a ping of the provided type to the provided IP and waits
// for its response. The opts type specifies additional options.
func (lc *LocalClient) PingWithOpts(ctx context.Context, ip netip.Addr, pingtype tailcfg.PingType, opts PingOpts) (*ipnstate.PingResult, error) {
func (lc *Client) PingWithOpts(ctx context.Context, ip netip.Addr, pingtype tailcfg.PingType, opts PingOpts) (*ipnstate.PingResult, error) {
v := url.Values{}
v.Set("ip", ip.String())
v.Set("size", strconv.Itoa(opts.Size))
@@ -1104,12 +1161,12 @@ func (lc *LocalClient) PingWithOpts(ctx context.Context, ip netip.Addr, pingtype
// Ping sends a ping of the provided type to the provided IP and waits
// for its response.
func (lc *LocalClient) Ping(ctx context.Context, ip netip.Addr, pingtype tailcfg.PingType) (*ipnstate.PingResult, error) {
func (lc *Client) Ping(ctx context.Context, ip netip.Addr, pingtype tailcfg.PingType) (*ipnstate.PingResult, error) {
return lc.PingWithOpts(ctx, ip, pingtype, PingOpts{})
}
// NetworkLockStatus fetches information about the tailnet key authority, if one is configured.
func (lc *LocalClient) NetworkLockStatus(ctx context.Context) (*ipnstate.NetworkLockStatus, error) {
func (lc *Client) NetworkLockStatus(ctx context.Context) (*ipnstate.NetworkLockStatus, error) {
body, err := lc.send(ctx, "GET", "/localapi/v0/tka/status", 200, nil)
if err != nil {
return nil, fmt.Errorf("error: %w", err)
@@ -1120,7 +1177,7 @@ func (lc *LocalClient) NetworkLockStatus(ctx context.Context) (*ipnstate.Network
// NetworkLockInit initializes the tailnet key authority.
//
// TODO(tom): Plumb through disablement secrets.
func (lc *LocalClient) NetworkLockInit(ctx context.Context, keys []tka.Key, disablementValues [][]byte, supportDisablement []byte) (*ipnstate.NetworkLockStatus, error) {
func (lc *Client) NetworkLockInit(ctx context.Context, keys []tka.Key, disablementValues [][]byte, supportDisablement []byte) (*ipnstate.NetworkLockStatus, error) {
var b bytes.Buffer
type initRequest struct {
Keys []tka.Key
@@ -1141,7 +1198,7 @@ func (lc *LocalClient) NetworkLockInit(ctx context.Context, keys []tka.Key, disa
// NetworkLockWrapPreauthKey wraps a pre-auth key with information to
// enable unattended bringup in the locked tailnet.
func (lc *LocalClient) NetworkLockWrapPreauthKey(ctx context.Context, preauthKey string, tkaKey key.NLPrivate) (string, error) {
func (lc *Client) NetworkLockWrapPreauthKey(ctx context.Context, preauthKey string, tkaKey key.NLPrivate) (string, error) {
encodedPrivate, err := tkaKey.MarshalText()
if err != nil {
return "", err
@@ -1164,7 +1221,7 @@ func (lc *LocalClient) NetworkLockWrapPreauthKey(ctx context.Context, preauthKey
}
// NetworkLockModify adds and/or removes key(s) to the tailnet key authority.
func (lc *LocalClient) NetworkLockModify(ctx context.Context, addKeys, removeKeys []tka.Key) error {
func (lc *Client) NetworkLockModify(ctx context.Context, addKeys, removeKeys []tka.Key) error {
var b bytes.Buffer
type modifyRequest struct {
AddKeys []tka.Key
@@ -1183,7 +1240,7 @@ func (lc *LocalClient) NetworkLockModify(ctx context.Context, addKeys, removeKey
// NetworkLockSign signs the specified node-key and transmits that signature to the control plane.
// rotationPublic, if specified, must be an ed25519 public key.
func (lc *LocalClient) NetworkLockSign(ctx context.Context, nodeKey key.NodePublic, rotationPublic []byte) error {
func (lc *Client) NetworkLockSign(ctx context.Context, nodeKey key.NodePublic, rotationPublic []byte) error {
var b bytes.Buffer
type signRequest struct {
NodeKey key.NodePublic
@@ -1201,7 +1258,7 @@ func (lc *LocalClient) NetworkLockSign(ctx context.Context, nodeKey key.NodePubl
}
// NetworkLockAffectedSigs returns all signatures signed by the specified keyID.
func (lc *LocalClient) NetworkLockAffectedSigs(ctx context.Context, keyID tkatype.KeyID) ([]tkatype.MarshaledSignature, error) {
func (lc *Client) NetworkLockAffectedSigs(ctx context.Context, keyID tkatype.KeyID) ([]tkatype.MarshaledSignature, error) {
body, err := lc.send(ctx, "POST", "/localapi/v0/tka/affected-sigs", 200, bytes.NewReader(keyID))
if err != nil {
return nil, fmt.Errorf("error: %w", err)
@@ -1210,7 +1267,7 @@ func (lc *LocalClient) NetworkLockAffectedSigs(ctx context.Context, keyID tkatyp
}
// NetworkLockLog returns up to maxEntries number of changes to network-lock state.
func (lc *LocalClient) NetworkLockLog(ctx context.Context, maxEntries int) ([]ipnstate.NetworkLockUpdate, error) {
func (lc *Client) NetworkLockLog(ctx context.Context, maxEntries int) ([]ipnstate.NetworkLockUpdate, error) {
v := url.Values{}
v.Set("limit", fmt.Sprint(maxEntries))
body, err := lc.send(ctx, "GET", "/localapi/v0/tka/log?"+v.Encode(), 200, nil)
@@ -1221,7 +1278,7 @@ func (lc *LocalClient) NetworkLockLog(ctx context.Context, maxEntries int) ([]ip
}
// NetworkLockForceLocalDisable forcibly shuts down network lock on this node.
func (lc *LocalClient) NetworkLockForceLocalDisable(ctx context.Context) error {
func (lc *Client) NetworkLockForceLocalDisable(ctx context.Context) error {
// This endpoint expects an empty JSON stanza as the payload.
var b bytes.Buffer
if err := json.NewEncoder(&b).Encode(struct{}{}); err != nil {
@@ -1236,7 +1293,7 @@ func (lc *LocalClient) NetworkLockForceLocalDisable(ctx context.Context) error {
// 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) {
func (lc *Client) NetworkLockVerifySigningDeeplink(ctx context.Context, url string) (*tka.DeeplinkValidationResult, error) {
vr := struct {
URL string
}{url}
@@ -1250,7 +1307,7 @@ func (lc *LocalClient) NetworkLockVerifySigningDeeplink(ctx context.Context, url
}
// 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) {
func (lc *Client) NetworkLockGenRecoveryAUM(ctx context.Context, removeKeys []tkatype.KeyID, forkFrom tka.AUMHash) ([]byte, error) {
vr := struct {
Keys []tkatype.KeyID
ForkFrom string
@@ -1265,7 +1322,7 @@ func (lc *LocalClient) NetworkLockGenRecoveryAUM(ctx context.Context, removeKeys
}
// 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) {
func (lc *Client) 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 {
@@ -1276,7 +1333,7 @@ func (lc *LocalClient) NetworkLockCosignRecoveryAUM(ctx context.Context, aum tka
}
// NetworkLockSubmitRecoveryAUM submits a recovery AUM to the control plane.
func (lc *LocalClient) NetworkLockSubmitRecoveryAUM(ctx context.Context, aum tka.AUM) error {
func (lc *Client) 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 {
@@ -1287,7 +1344,7 @@ func (lc *LocalClient) NetworkLockSubmitRecoveryAUM(ctx context.Context, aum tka
// 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 {
func (lc *Client) SetServeConfig(ctx context.Context, config *ipn.ServeConfig) error {
h := make(http.Header)
if config != nil {
h.Set("If-Match", config.ETag)
@@ -1299,8 +1356,19 @@ func (lc *LocalClient) SetServeConfig(ctx context.Context, config *ipn.ServeConf
return nil
}
// DisconnectControl shuts down all connections to control, thus making control consider this node inactive. This can be
// run on HA subnet router or app connector replicas before shutting them down to ensure peers get told to switch over
// to another replica whilst there is still some grace period for the existing connections to terminate.
func (lc *Client) DisconnectControl(ctx context.Context) error {
_, _, err := lc.sendWithHeaders(ctx, "POST", "/localapi/v0/disconnect-control", 200, nil, nil)
if err != nil {
return fmt.Errorf("error disconnecting control: %w", err)
}
return nil
}
// NetworkLockDisable shuts down network-lock across the tailnet.
func (lc *LocalClient) NetworkLockDisable(ctx context.Context, secret []byte) error {
func (lc *Client) NetworkLockDisable(ctx context.Context, secret []byte) error {
if _, err := lc.send(ctx, "POST", "/localapi/v0/tka/disable", 200, bytes.NewReader(secret)); err != nil {
return fmt.Errorf("error: %w", err)
}
@@ -1310,7 +1378,7 @@ func (lc *LocalClient) NetworkLockDisable(ctx context.Context, secret []byte) er
// GetServeConfig return the current serve config.
//
// If the serve config is empty, it returns (nil, nil).
func (lc *LocalClient) GetServeConfig(ctx context.Context) (*ipn.ServeConfig, error) {
func (lc *Client) GetServeConfig(ctx context.Context) (*ipn.ServeConfig, error) {
body, h, err := lc.sendWithHeaders(ctx, "GET", "/localapi/v0/serve-config", 200, nil, nil)
if err != nil {
return nil, fmt.Errorf("getting serve config: %w", err)
@@ -1385,7 +1453,7 @@ func (r jsonReader) Read(p []byte) (n int, err error) {
}
// ProfileStatus returns the current profile and the list of all profiles.
func (lc *LocalClient) ProfileStatus(ctx context.Context) (current ipn.LoginProfile, all []ipn.LoginProfile, err error) {
func (lc *Client) ProfileStatus(ctx context.Context) (current ipn.LoginProfile, all []ipn.LoginProfile, err error) {
body, err := lc.send(ctx, "GET", "/localapi/v0/profiles/current", 200, nil)
if err != nil {
return
@@ -1403,7 +1471,7 @@ func (lc *LocalClient) ProfileStatus(ctx context.Context) (current ipn.LoginProf
}
// ReloadConfig reloads the config file, if possible.
func (lc *LocalClient) ReloadConfig(ctx context.Context) (ok bool, err error) {
func (lc *Client) ReloadConfig(ctx context.Context) (ok bool, err error) {
body, err := lc.send(ctx, "POST", "/localapi/v0/reload-config", 200, nil)
if err != nil {
return
@@ -1421,13 +1489,13 @@ func (lc *LocalClient) ReloadConfig(ctx context.Context) (ok bool, err error) {
// SwitchToEmptyProfile creates and switches to a new unnamed profile. The new
// profile is not assigned an ID until it is persisted after a successful login.
// In order to login to the new profile, the user must call LoginInteractive.
func (lc *LocalClient) SwitchToEmptyProfile(ctx context.Context) error {
func (lc *Client) SwitchToEmptyProfile(ctx context.Context) error {
_, err := lc.send(ctx, "PUT", "/localapi/v0/profiles/", http.StatusCreated, nil)
return err
}
// SwitchProfile switches to the given profile.
func (lc *LocalClient) SwitchProfile(ctx context.Context, profile ipn.ProfileID) error {
func (lc *Client) SwitchProfile(ctx context.Context, profile ipn.ProfileID) error {
_, err := lc.send(ctx, "POST", "/localapi/v0/profiles/"+url.PathEscape(string(profile)), 204, nil)
return err
}
@@ -1435,7 +1503,7 @@ func (lc *LocalClient) SwitchProfile(ctx context.Context, profile ipn.ProfileID)
// DeleteProfile removes the profile with the given ID.
// If the profile is the current profile, an empty profile
// will be selected as if SwitchToEmptyProfile was called.
func (lc *LocalClient) DeleteProfile(ctx context.Context, profile ipn.ProfileID) error {
func (lc *Client) DeleteProfile(ctx context.Context, profile ipn.ProfileID) error {
_, err := lc.send(ctx, "DELETE", "/localapi/v0/profiles"+url.PathEscape(string(profile)), http.StatusNoContent, nil)
return err
}
@@ -1452,7 +1520,7 @@ func (lc *LocalClient) DeleteProfile(ctx context.Context, profile ipn.ProfileID)
// to block until the feature has been enabled.
//
// 2023-08-09: Valid feature values are "serve" and "funnel".
func (lc *LocalClient) QueryFeature(ctx context.Context, feature string) (*tailcfg.QueryFeatureResponse, error) {
func (lc *Client) 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 {
@@ -1461,7 +1529,7 @@ func (lc *LocalClient) QueryFeature(ctx context.Context, feature string) (*tailc
return decodeJSON[*tailcfg.QueryFeatureResponse](body)
}
func (lc *LocalClient) DebugDERPRegion(ctx context.Context, regionIDOrCode string) (*ipnstate.DebugDERPRegionReport, error) {
func (lc *Client) 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)
if err != nil {
@@ -1471,7 +1539,7 @@ func (lc *LocalClient) DebugDERPRegion(ctx context.Context, regionIDOrCode strin
}
// DebugPacketFilterRules returns the packet filter rules for the current device.
func (lc *LocalClient) DebugPacketFilterRules(ctx context.Context) ([]tailcfg.FilterRule, error) {
func (lc *Client) DebugPacketFilterRules(ctx context.Context) ([]tailcfg.FilterRule, error) {
body, err := lc.send(ctx, "POST", "/localapi/v0/debug-packet-filter-rules", 200, nil)
if err != nil {
return nil, fmt.Errorf("error %w: %s", err, body)
@@ -1482,7 +1550,7 @@ func (lc *LocalClient) DebugPacketFilterRules(ctx context.Context) ([]tailcfg.Fi
// DebugSetExpireIn marks the current node key to expire in d.
//
// This is meant primarily for debug and testing.
func (lc *LocalClient) DebugSetExpireIn(ctx context.Context, d time.Duration) error {
func (lc *Client) DebugSetExpireIn(ctx context.Context, d time.Duration) error {
v := url.Values{"expiry": {fmt.Sprint(time.Now().Add(d).Unix())}}
_, err := lc.send(ctx, "POST", "/localapi/v0/set-expiry-sooner?"+v.Encode(), 200, nil)
return err
@@ -1492,7 +1560,7 @@ func (lc *LocalClient) DebugSetExpireIn(ctx context.Context, d time.Duration) er
//
// The provided context does not determine the lifetime of the
// returned io.ReadCloser.
func (lc *LocalClient) StreamDebugCapture(ctx context.Context) (io.ReadCloser, error) {
func (lc *Client) StreamDebugCapture(ctx context.Context) (io.ReadCloser, error) {
req, err := http.NewRequestWithContext(ctx, "POST", "http://"+apitype.LocalAPIHost+"/localapi/v0/debug-capture", nil)
if err != nil {
return nil, err
@@ -1518,7 +1586,7 @@ func (lc *LocalClient) StreamDebugCapture(ctx context.Context) (io.ReadCloser, e
// resources.
//
// A default set of ipn.Notify messages are returned but the set can be modified by mask.
func (lc *LocalClient) WatchIPNBus(ctx context.Context, mask ipn.NotifyWatchOpt) (*IPNBusWatcher, error) {
func (lc *Client) WatchIPNBus(ctx context.Context, mask ipn.NotifyWatchOpt) (*IPNBusWatcher, error) {
req, err := http.NewRequestWithContext(ctx, "GET",
"http://"+apitype.LocalAPIHost+"/localapi/v0/watch-ipn-bus?mask="+fmt.Sprint(mask),
nil)
@@ -1544,7 +1612,7 @@ func (lc *LocalClient) WatchIPNBus(ctx context.Context, mask ipn.NotifyWatchOpt)
// CheckUpdate returns a tailcfg.ClientVersion indicating whether or not an update is available
// to be installed via the LocalAPI. In case the LocalAPI can't install updates, it returns a
// ClientVersion that says that we are up to date.
func (lc *LocalClient) CheckUpdate(ctx context.Context) (*tailcfg.ClientVersion, error) {
func (lc *Client) CheckUpdate(ctx context.Context) (*tailcfg.ClientVersion, error) {
body, err := lc.get200(ctx, "/localapi/v0/update/check")
if err != nil {
return nil, err
@@ -1560,7 +1628,7 @@ func (lc *LocalClient) CheckUpdate(ctx context.Context) (*tailcfg.ClientVersion,
// To turn it on, there must have been a previously used exit node.
// The most previously used one is reused.
// This is a convenience method for GUIs. To select an actual one, update the prefs.
func (lc *LocalClient) SetUseExitNode(ctx context.Context, on bool) error {
func (lc *Client) SetUseExitNode(ctx context.Context, on bool) error {
_, err := lc.send(ctx, "POST", "/localapi/v0/set-use-exit-node-enabled?enabled="+strconv.FormatBool(on), http.StatusOK, nil)
return err
}
@@ -1568,7 +1636,7 @@ func (lc *LocalClient) SetUseExitNode(ctx context.Context, on bool) error {
// DriveSetServerAddr instructs Taildrive to use the server at addr to access
// the filesystem. This is used on platforms like Windows and MacOS to let
// Taildrive know to use the file server running in the GUI app.
func (lc *LocalClient) DriveSetServerAddr(ctx context.Context, addr string) error {
func (lc *Client) DriveSetServerAddr(ctx context.Context, addr string) error {
_, err := lc.send(ctx, "PUT", "/localapi/v0/drive/fileserver-address", http.StatusCreated, strings.NewReader(addr))
return err
}
@@ -1576,14 +1644,14 @@ func (lc *LocalClient) DriveSetServerAddr(ctx context.Context, addr string) erro
// DriveShareSet adds or updates the given share in the list of shares that
// Taildrive will serve to remote nodes. If a share with the same name already
// exists, the existing share is replaced/updated.
func (lc *LocalClient) DriveShareSet(ctx context.Context, share *drive.Share) error {
func (lc *Client) DriveShareSet(ctx context.Context, share *drive.Share) error {
_, err := lc.send(ctx, "PUT", "/localapi/v0/drive/shares", http.StatusCreated, jsonBody(share))
return err
}
// DriveShareRemove removes the share with the given name from the list of
// shares that Taildrive will serve to remote nodes.
func (lc *LocalClient) DriveShareRemove(ctx context.Context, name string) error {
func (lc *Client) DriveShareRemove(ctx context.Context, name string) error {
_, err := lc.send(
ctx,
"DELETE",
@@ -1594,7 +1662,7 @@ func (lc *LocalClient) DriveShareRemove(ctx context.Context, name string) error
}
// DriveShareRename renames the share from old to new name.
func (lc *LocalClient) DriveShareRename(ctx context.Context, oldName, newName string) error {
func (lc *Client) DriveShareRename(ctx context.Context, oldName, newName string) error {
_, err := lc.send(
ctx,
"POST",
@@ -1606,7 +1674,7 @@ func (lc *LocalClient) DriveShareRename(ctx context.Context, oldName, newName st
// DriveShareList returns the list of shares that drive is currently serving
// to remote nodes.
func (lc *LocalClient) DriveShareList(ctx context.Context) ([]*drive.Share, error) {
func (lc *Client) DriveShareList(ctx context.Context) ([]*drive.Share, error) {
result, err := lc.get200(ctx, "/localapi/v0/drive/shares")
if err != nil {
return nil, err
@@ -1617,7 +1685,7 @@ func (lc *LocalClient) DriveShareList(ctx context.Context) ([]*drive.Share, erro
}
// IPNBusWatcher is an active subscription (watch) of the local tailscaled IPN bus.
// It's returned by LocalClient.WatchIPNBus.
// It's returned by Client.WatchIPNBus.
//
// It must be closed when done.
type IPNBusWatcher struct {
@@ -1641,7 +1709,7 @@ func (w *IPNBusWatcher) Close() error {
}
// Next returns the next ipn.Notify from the stream.
// If the context from LocalClient.WatchIPNBus is done, that error is returned.
// If the context from Client.WatchIPNBus is done, that error is returned.
func (w *IPNBusWatcher) Next() (ipn.Notify, error) {
var n ipn.Notify
if err := w.dec.Decode(&n); err != nil {
@@ -1654,7 +1722,7 @@ func (w *IPNBusWatcher) Next() (ipn.Notify, error) {
}
// SuggestExitNode requests an exit node suggestion and returns the exit node's details.
func (lc *LocalClient) SuggestExitNode(ctx context.Context) (apitype.ExitNodeSuggestionResponse, error) {
func (lc *Client) SuggestExitNode(ctx context.Context) (apitype.ExitNodeSuggestionResponse, error) {
body, err := lc.get200(ctx, "/localapi/v0/suggest-exit-node")
if err != nil {
return apitype.ExitNodeSuggestionResponse{}, err

View File

@@ -12,6 +12,7 @@ import (
"fmt"
"net/http"
"net/netip"
"net/url"
)
// ACLRow defines a rule that grants access by a set of users or groups to a set
@@ -83,7 +84,7 @@ func (c *Client) ACL(ctx context.Context) (acl *ACL, err error) {
}
}()
path := fmt.Sprintf("%s/api/v2/tailnet/%s/acl", c.baseURL(), c.tailnet)
path := c.BuildTailnetURL("acl")
req, err := http.NewRequestWithContext(ctx, "GET", path, nil)
if err != nil {
return nil, err
@@ -97,7 +98,7 @@ func (c *Client) ACL(ctx context.Context) (acl *ACL, err error) {
// If status code was not successful, return the error.
// TODO: Change the check for the StatusCode to include other 2XX success codes.
if resp.StatusCode != http.StatusOK {
return nil, handleErrorResponse(b, resp)
return nil, HandleErrorResponse(b, resp)
}
// Otherwise, try to decode the response.
@@ -126,7 +127,7 @@ func (c *Client) ACLHuJSON(ctx context.Context) (acl *ACLHuJSON, err error) {
}
}()
path := fmt.Sprintf("%s/api/v2/tailnet/%s/acl?details=1", c.baseURL(), c.tailnet)
path := c.BuildTailnetURL("acl", url.Values{"details": {"1"}})
req, err := http.NewRequestWithContext(ctx, "GET", path, nil)
if err != nil {
return nil, err
@@ -138,7 +139,7 @@ func (c *Client) ACLHuJSON(ctx context.Context) (acl *ACLHuJSON, err error) {
}
if resp.StatusCode != http.StatusOK {
return nil, handleErrorResponse(b, resp)
return nil, HandleErrorResponse(b, resp)
}
data := struct {
@@ -146,7 +147,7 @@ func (c *Client) ACLHuJSON(ctx context.Context) (acl *ACLHuJSON, err error) {
Warnings []string `json:"warnings"`
}{}
if err := json.Unmarshal(b, &data); err != nil {
return nil, err
return nil, fmt.Errorf("json.Unmarshal %q: %w", b, err)
}
acl = &ACLHuJSON{
@@ -184,7 +185,7 @@ func (e ACLTestError) Error() string {
}
func (c *Client) aclPOSTRequest(ctx context.Context, body []byte, avoidCollisions bool, etag, acceptHeader string) ([]byte, string, error) {
path := fmt.Sprintf("%s/api/v2/tailnet/%s/acl", c.baseURL(), c.tailnet)
path := c.BuildTailnetURL("acl")
req, err := http.NewRequestWithContext(ctx, "POST", path, bytes.NewBuffer(body))
if err != nil {
return nil, "", err
@@ -328,7 +329,7 @@ type ACLPreview struct {
}
func (c *Client) previewACLPostRequest(ctx context.Context, body []byte, previewType string, previewFor string) (res *ACLPreviewResponse, err error) {
path := fmt.Sprintf("%s/api/v2/tailnet/%s/acl/preview", c.baseURL(), c.tailnet)
path := c.BuildTailnetURL("acl", "preview")
req, err := http.NewRequestWithContext(ctx, "POST", path, bytes.NewBuffer(body))
if err != nil {
return nil, err
@@ -350,7 +351,7 @@ func (c *Client) previewACLPostRequest(ctx context.Context, body []byte, preview
// If status code was not successful, return the error.
// TODO: Change the check for the StatusCode to include other 2XX success codes.
if resp.StatusCode != http.StatusOK {
return nil, handleErrorResponse(b, resp)
return nil, HandleErrorResponse(b, resp)
}
if err = json.Unmarshal(b, &res); err != nil {
return nil, err
@@ -488,7 +489,7 @@ func (c *Client) ValidateACLJSON(ctx context.Context, source, dest string) (test
return nil, err
}
path := fmt.Sprintf("%s/api/v2/tailnet/%s/acl/validate", c.baseURL(), c.tailnet)
path := c.BuildTailnetURL("acl", "validate")
req, err := http.NewRequestWithContext(ctx, "POST", path, bytes.NewBuffer(postData))
if err != nil {
return nil, err

View File

@@ -7,11 +7,29 @@ package apitype
import (
"tailscale.com/tailcfg"
"tailscale.com/types/dnstype"
"tailscale.com/util/ctxkey"
)
// LocalAPIHost is the Host header value used by the LocalAPI.
const LocalAPIHost = "local-tailscaled.sock"
// RequestReasonHeader is the header used to pass justification for a LocalAPI request,
// such as when a user wants to perform an action they don't have permission for,
// and a policy allows it with justification. As of 2025-01-29, it is only used to
// allow a user to disconnect Tailscale when the "always-on" mode is enabled.
//
// The header value is base64-encoded using the standard encoding defined in RFC 4648.
//
// See tailscale/corp#26146.
const RequestReasonHeader = "X-Tailscale-Reason"
// RequestReasonKey is the context key used to pass the request reason
// when making a LocalAPI request via [local.Client].
// It's value is a raw string. An empty string means no reason was provided.
//
// See tailscale/corp#26146.
var RequestReasonKey = ctxkey.New(RequestReasonHeader, "")
// 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 {

View File

@@ -79,6 +79,13 @@ type Device struct {
// Tailscale have attempted to collect this from the device but it has not
// opted in, PostureIdentity will have Disabled=true.
PostureIdentity *DevicePostureIdentity `json:"postureIdentity"`
// TailnetLockKey is the tailnet lock public key of the node as a hex string.
TailnetLockKey string `json:"tailnetLockKey,omitempty"`
// TailnetLockErr indicates an issue with the tailnet lock node-key signature
// on this device. This field is only populated when tailnet lock is enabled.
TailnetLockErr string `json:"tailnetLockError,omitempty"`
}
type DevicePostureIdentity struct {
@@ -131,7 +138,7 @@ func (c *Client) Devices(ctx context.Context, fields *DeviceFieldsOpts) (deviceL
}
}()
path := fmt.Sprintf("%s/api/v2/tailnet/%s/devices", c.baseURL(), c.tailnet)
path := c.BuildTailnetURL("devices")
req, err := http.NewRequestWithContext(ctx, "GET", path, nil)
if err != nil {
return nil, err
@@ -149,7 +156,7 @@ func (c *Client) Devices(ctx context.Context, fields *DeviceFieldsOpts) (deviceL
// If status code was not successful, return the error.
// TODO: Change the check for the StatusCode to include other 2XX success codes.
if resp.StatusCode != http.StatusOK {
return nil, handleErrorResponse(b, resp)
return nil, HandleErrorResponse(b, resp)
}
var devices GetDevicesResponse
@@ -188,7 +195,7 @@ func (c *Client) Device(ctx context.Context, deviceID string, fields *DeviceFiel
// If status code was not successful, return the error.
// TODO: Change the check for the StatusCode to include other 2XX success codes.
if resp.StatusCode != http.StatusOK {
return nil, handleErrorResponse(b, resp)
return nil, HandleErrorResponse(b, resp)
}
err = json.Unmarshal(b, &device)
@@ -221,7 +228,7 @@ func (c *Client) DeleteDevice(ctx context.Context, deviceID string) (err error)
// If status code was not successful, return the error.
// TODO: Change the check for the StatusCode to include other 2XX success codes.
if resp.StatusCode != http.StatusOK {
return handleErrorResponse(b, resp)
return HandleErrorResponse(b, resp)
}
return nil
}
@@ -253,7 +260,7 @@ func (c *Client) SetAuthorized(ctx context.Context, deviceID string, authorized
// If status code was not successful, return the error.
// TODO: Change the check for the StatusCode to include other 2XX success codes.
if resp.StatusCode != http.StatusOK {
return handleErrorResponse(b, resp)
return HandleErrorResponse(b, resp)
}
return nil
@@ -281,7 +288,7 @@ func (c *Client) SetTags(ctx context.Context, deviceID string, tags []string) er
// If status code was not successful, return the error.
// TODO: Change the check for the StatusCode to include other 2XX success codes.
if resp.StatusCode != http.StatusOK {
return handleErrorResponse(b, resp)
return HandleErrorResponse(b, resp)
}
return nil

View File

@@ -44,7 +44,7 @@ type DNSPreferences struct {
}
func (c *Client) dnsGETRequest(ctx context.Context, endpoint string) ([]byte, error) {
path := fmt.Sprintf("%s/api/v2/tailnet/%s/dns/%s", c.baseURL(), c.tailnet, endpoint)
path := c.BuildTailnetURL("dns", endpoint)
req, err := http.NewRequestWithContext(ctx, "GET", path, nil)
if err != nil {
return nil, err
@@ -57,14 +57,14 @@ func (c *Client) dnsGETRequest(ctx context.Context, endpoint string) ([]byte, er
// If status code was not successful, return the error.
// TODO: Change the check for the StatusCode to include other 2XX success codes.
if resp.StatusCode != http.StatusOK {
return nil, handleErrorResponse(b, resp)
return nil, HandleErrorResponse(b, resp)
}
return b, nil
}
func (c *Client) dnsPOSTRequest(ctx context.Context, endpoint string, postData any) ([]byte, error) {
path := fmt.Sprintf("%s/api/v2/tailnet/%s/dns/%s", c.baseURL(), c.tailnet, endpoint)
path := c.BuildTailnetURL("dns", endpoint)
data, err := json.Marshal(&postData)
if err != nil {
return nil, err
@@ -84,7 +84,7 @@ func (c *Client) dnsPOSTRequest(ctx context.Context, endpoint string, postData a
// If status code was not successful, return the error.
// TODO: Change the check for the StatusCode to include other 2XX success codes.
if resp.StatusCode != http.StatusOK {
return nil, handleErrorResponse(b, resp)
return nil, HandleErrorResponse(b, resp)
}
return b, nil

View File

@@ -40,7 +40,7 @@ type KeyDeviceCreateCapabilities struct {
// Keys returns the list of keys for the current user.
func (c *Client) Keys(ctx context.Context) ([]string, error) {
path := fmt.Sprintf("%s/api/v2/tailnet/%s/keys", c.baseURL(), c.tailnet)
path := c.BuildTailnetURL("keys")
req, err := http.NewRequestWithContext(ctx, "GET", path, nil)
if err != nil {
return nil, err
@@ -51,7 +51,7 @@ func (c *Client) Keys(ctx context.Context) ([]string, error) {
return nil, err
}
if resp.StatusCode != http.StatusOK {
return nil, handleErrorResponse(b, resp)
return nil, HandleErrorResponse(b, resp)
}
var keys struct {
@@ -99,7 +99,7 @@ func (c *Client) CreateKeyWithExpiry(ctx context.Context, caps KeyCapabilities,
return "", nil, err
}
path := fmt.Sprintf("%s/api/v2/tailnet/%s/keys", c.baseURL(), c.tailnet)
path := c.BuildTailnetURL("keys")
req, err := http.NewRequestWithContext(ctx, "POST", path, bytes.NewReader(bs))
if err != nil {
return "", nil, err
@@ -110,7 +110,7 @@ func (c *Client) CreateKeyWithExpiry(ctx context.Context, caps KeyCapabilities,
return "", nil, err
}
if resp.StatusCode != http.StatusOK {
return "", nil, handleErrorResponse(b, resp)
return "", nil, HandleErrorResponse(b, resp)
}
var key struct {
@@ -126,7 +126,7 @@ func (c *Client) CreateKeyWithExpiry(ctx context.Context, caps KeyCapabilities,
// Key returns the metadata for the given key ID. Currently, capabilities are
// only returned for auth keys, API keys only return general metadata.
func (c *Client) Key(ctx context.Context, id string) (*Key, error) {
path := fmt.Sprintf("%s/api/v2/tailnet/%s/keys/%s", c.baseURL(), c.tailnet, id)
path := c.BuildTailnetURL("keys", id)
req, err := http.NewRequestWithContext(ctx, "GET", path, nil)
if err != nil {
return nil, err
@@ -137,7 +137,7 @@ func (c *Client) Key(ctx context.Context, id string) (*Key, error) {
return nil, err
}
if resp.StatusCode != http.StatusOK {
return nil, handleErrorResponse(b, resp)
return nil, HandleErrorResponse(b, resp)
}
var key Key
@@ -149,7 +149,7 @@ func (c *Client) Key(ctx context.Context, id string) (*Key, error) {
// DeleteKey deletes the key with the given ID.
func (c *Client) DeleteKey(ctx context.Context, id string) error {
path := fmt.Sprintf("%s/api/v2/tailnet/%s/keys/%s", c.baseURL(), c.tailnet, id)
path := c.BuildTailnetURL("keys", id)
req, err := http.NewRequestWithContext(ctx, "DELETE", path, nil)
if err != nil {
return err
@@ -160,7 +160,7 @@ func (c *Client) DeleteKey(ctx context.Context, id string) error {
return err
}
if resp.StatusCode != http.StatusOK {
return handleErrorResponse(b, resp)
return HandleErrorResponse(b, resp)
}
return nil
}

View File

@@ -0,0 +1,106 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package tailscale
import (
"context"
"crypto/tls"
"tailscale.com/client/local"
"tailscale.com/client/tailscale/apitype"
"tailscale.com/ipn/ipnstate"
)
// ErrPeerNotFound is an alias for tailscale.com/client/local.
//
// Deprecated: import tailscale.com/client/local instead.
var ErrPeerNotFound = local.ErrPeerNotFound
// LocalClient is an alias for tailscale.com/client/local.
//
// Deprecated: import tailscale.com/client/local instead.
type LocalClient = local.Client
// IPNBusWatcher is an alias for tailscale.com/client/local.
//
// Deprecated: import tailscale.com/client/local instead.
type IPNBusWatcher = local.IPNBusWatcher
// BugReportOpts is an alias for tailscale.com/client/local.
//
// Deprecated: import tailscale.com/client/local instead.
type BugReportOpts = local.BugReportOpts
// DebugPortMapOpts is an alias for tailscale.com/client/local.
//
// Deprecated: import tailscale.com/client/local instead.
type DebugPortmapOpts = local.DebugPortmapOpts
// PingOpts is an alias for tailscale.com/client/local.
//
// Deprecated: import tailscale.com/client/local instead.
type PingOpts = local.PingOpts
// GetCertificate is an alias for tailscale.com/client/local.
//
// Deprecated: import tailscale.com/client/local instead.
func GetCertificate(hi *tls.ClientHelloInfo) (*tls.Certificate, error) {
return local.GetCertificate(hi)
}
// SetVersionMismatchHandler is an alias for tailscale.com/client/local.
//
// Deprecated: import tailscale.com/client/local instead.
func SetVersionMismatchHandler(f func(clientVer, serverVer string)) {
local.SetVersionMismatchHandler(f)
}
// IsAccessDeniedError is an alias for tailscale.com/client/local.
//
// Deprecated: import tailscale.com/client/local instead.
func IsAccessDeniedError(err error) bool {
return local.IsAccessDeniedError(err)
}
// IsPreconditionsFailedError is an alias for tailscale.com/client/local.
//
// Deprecated: import tailscale.com/client/local instead.
func IsPreconditionsFailedError(err error) bool {
return local.IsPreconditionsFailedError(err)
}
// WhoIs is an alias for tailscale.com/client/local.
//
// Deprecated: import tailscale.com/client/local instead.
func WhoIs(ctx context.Context, remoteAddr string) (*apitype.WhoIsResponse, error) {
return local.WhoIs(ctx, remoteAddr)
}
// Status is an alias for tailscale.com/client/local.
//
// Deprecated: import tailscale.com/client/local instead.
func Status(ctx context.Context) (*ipnstate.Status, error) {
return local.Status(ctx)
}
// StatusWithoutPeers is an alias for tailscale.com/client/local.
//
// Deprecated: import tailscale.com/client/local instead.
func StatusWithoutPeers(ctx context.Context) (*ipnstate.Status, error) {
return local.StatusWithoutPeers(ctx)
}
// CertPair is an alias for tailscale.com/client/local.
//
// Deprecated: import tailscale.com/client/local instead.
func CertPair(ctx context.Context, domain string) (certPEM, keyPEM []byte, err error) {
return local.CertPair(ctx, domain)
}
// ExpandSNIName is an alias for tailscale.com/client/local.
//
// Deprecated: import tailscale.com/client/local instead.
func ExpandSNIName(ctx context.Context, name string) (fqdn string, ok bool) {
return local.ExpandSNIName(ctx, name)
}

View File

@@ -44,7 +44,7 @@ func (c *Client) Routes(ctx context.Context, deviceID string) (routes *Routes, e
// If status code was not successful, return the error.
// TODO: Change the check for the StatusCode to include other 2XX success codes.
if resp.StatusCode != http.StatusOK {
return nil, handleErrorResponse(b, resp)
return nil, HandleErrorResponse(b, resp)
}
var sr Routes
@@ -84,7 +84,7 @@ func (c *Client) SetRoutes(ctx context.Context, deviceID string, subnets []netip
// If status code was not successful, return the error.
// TODO: Change the check for the StatusCode to include other 2XX success codes.
if resp.StatusCode != http.StatusOK {
return nil, handleErrorResponse(b, resp)
return nil, HandleErrorResponse(b, resp)
}
var srr *Routes

View File

@@ -9,7 +9,6 @@ import (
"context"
"fmt"
"net/http"
"net/url"
"tailscale.com/util/httpm"
)
@@ -22,7 +21,7 @@ func (c *Client) TailnetDeleteRequest(ctx context.Context, tailnetID string) (er
}
}()
path := fmt.Sprintf("%s/api/v2/tailnet/%s", c.baseURL(), url.PathEscape(string(tailnetID)))
path := c.BuildTailnetURL("tailnet")
req, err := http.NewRequestWithContext(ctx, httpm.DELETE, path, nil)
if err != nil {
return err
@@ -35,7 +34,7 @@ func (c *Client) TailnetDeleteRequest(ctx context.Context, tailnetID string) (er
}
if resp.StatusCode != http.StatusOK {
return handleErrorResponse(b, resp)
return HandleErrorResponse(b, resp)
}
return nil

View File

@@ -3,11 +3,12 @@
//go:build go1.19
// Package tailscale contains Go clients for the Tailscale LocalAPI and
// Tailscale control plane API.
// Package tailscale contains a Go client for the Tailscale control plane API.
//
// Warning: this package is in development and makes no API compatibility
// promises as of 2022-04-29. It is subject to change at any time.
// This package is only intended for internal and transitional use.
//
// Deprecated: the official control plane client is available at
// tailscale.com/client/tailscale/v2.
package tailscale
import (
@@ -16,13 +17,12 @@ import (
"fmt"
"io"
"net/http"
"net/url"
"path"
)
// I_Acknowledge_This_API_Is_Unstable must be set true to use this package
// for now. It was added 2022-04-29 when it was moved to this git repo
// and will be removed when the public API has settled.
//
// TODO(bradfitz): remove this after the we're happy with the public API.
// for now. This package is being replaced by tailscale.com/client/tailscale/v2.
var I_Acknowledge_This_API_Is_Unstable = false
// TODO: use url.PathEscape() for deviceID and tailnets when constructing requests.
@@ -36,6 +36,8 @@ const maxReadSize = 10 << 20
//
// Use NewClient to instantiate one. Exported fields should be set before
// the client is used and not changed thereafter.
//
// Deprecated: use tailscale.com/client/tailscale/v2 instead.
type Client struct {
// tailnet is the globally unique identifier for a Tailscale network, such
// as "example.com" or "user@gmail.com".
@@ -51,6 +53,9 @@ type Client struct {
// HTTPClient optionally specifies an alternate HTTP client to use.
// If nil, http.DefaultClient is used.
HTTPClient *http.Client
// UserAgent optionally specifies an alternate User-Agent header
UserAgent string
}
func (c *Client) httpClient() *http.Client {
@@ -60,6 +65,46 @@ func (c *Client) httpClient() *http.Client {
return http.DefaultClient
}
// BuildURL builds a url to http(s)://<apiserver>/api/v2/<slash-separated-pathElements>
// using the given pathElements. It url escapes each path element, so the
// caller doesn't need to worry about that. The last item of pathElements can
// be of type url.Values to add a query string to the URL.
//
// For example, BuildURL(devices, 5) with the default server URL would result in
// https://api.tailscale.com/api/v2/devices/5.
func (c *Client) BuildURL(pathElements ...any) string {
elem := make([]string, 1, len(pathElements)+1)
elem[0] = "/api/v2"
var query string
for i, pathElement := range pathElements {
if uv, ok := pathElement.(url.Values); ok && i == len(pathElements)-1 {
query = uv.Encode()
} else {
elem = append(elem, url.PathEscape(fmt.Sprint(pathElement)))
}
}
url := c.baseURL() + path.Join(elem...)
if query != "" {
url += "?" + query
}
return url
}
// BuildTailnetURL builds a url to http(s)://<apiserver>/api/v2/tailnet/<tailnet>/<slash-separated-pathElements>
// using the given pathElements. It url escapes each path element, so the
// caller doesn't need to worry about that. The last item of pathElements can
// be of type url.Values to add a query string to the URL.
//
// For example, BuildTailnetURL(policy, validate) with the default server URL and a tailnet of "example.com"
// would result in https://api.tailscale.com/api/v2/tailnet/example.com/policy/validate.
func (c *Client) BuildTailnetURL(pathElements ...any) string {
allElements := make([]any, 2, len(pathElements)+2)
allElements[0] = "tailnet"
allElements[1] = c.tailnet
allElements = append(allElements, pathElements...)
return c.BuildURL(allElements...)
}
func (c *Client) baseURL() string {
if c.BaseURL != "" {
return c.BaseURL
@@ -95,10 +140,13 @@ func (c *Client) setAuth(r *http.Request) {
// If httpClient is nil, then http.DefaultClient is used.
// "api.tailscale.com" is set as the BaseURL for the returned client
// and can be changed manually by the user.
//
// Deprecated: use tailscale.com/client/tailscale/v2 instead.
func NewClient(tailnet string, auth AuthMethod) *Client {
return &Client{
tailnet: tailnet,
auth: auth,
tailnet: tailnet,
auth: auth,
UserAgent: "tailscale-client-oss",
}
}
@@ -110,17 +158,16 @@ func (c *Client) Do(req *http.Request) (*http.Response, error) {
return nil, errors.New("use of Client without setting I_Acknowledge_This_API_Is_Unstable")
}
c.setAuth(req)
if c.UserAgent != "" {
req.Header.Set("User-Agent", c.UserAgent)
}
return c.httpClient().Do(req)
}
// sendRequest add the authentication key to the request and sends it. It
// receives the response and reads up to 10MB of it.
func (c *Client) sendRequest(req *http.Request) ([]byte, *http.Response, error) {
if !I_Acknowledge_This_API_Is_Unstable {
return nil, nil, errors.New("use of Client without setting I_Acknowledge_This_API_Is_Unstable")
}
c.setAuth(req)
resp, err := c.httpClient().Do(req)
resp, err := c.Do(req)
if err != nil {
return nil, resp, err
}
@@ -145,12 +192,14 @@ func (e ErrResponse) Error() string {
return fmt.Sprintf("Status: %d, Message: %q", e.Status, e.Message)
}
// handleErrorResponse decodes the error message from the server and returns
// HandleErrorResponse decodes the error message from the server and returns
// an ErrResponse from it.
func handleErrorResponse(b []byte, resp *http.Response) error {
//
// Deprecated: use tailscale.com/client/tailscale/v2 instead.
func HandleErrorResponse(b []byte, resp *http.Response) error {
var errResp ErrResponse
if err := json.Unmarshal(b, &errResp); err != nil {
return err
return fmt.Errorf("json.Unmarshal %q: %w", b, err)
}
errResp.Status = resp.StatusCode
return errResp

View File

@@ -22,10 +22,11 @@ import (
"time"
"github.com/gorilla/csrf"
"tailscale.com/client/tailscale"
"tailscale.com/client/local"
"tailscale.com/client/tailscale/apitype"
"tailscale.com/clientupdate"
"tailscale.com/envknob"
"tailscale.com/envknob/featureknob"
"tailscale.com/hostinfo"
"tailscale.com/ipn"
"tailscale.com/ipn/ipnstate"
@@ -49,7 +50,7 @@ type Server struct {
mode ServerMode
logf logger.Logf
lc *tailscale.LocalClient
lc *local.Client
timeNow func() time.Time
// devMode indicates that the server run with frontend assets
@@ -88,8 +89,8 @@ type Server struct {
type ServerMode string
const (
// LoginServerMode serves a readonly login client for logging a
// node into a tailnet, and viewing a readonly interface of the
// LoginServerMode serves a read-only login client for logging a
// node into a tailnet, and viewing a read-only interface of the
// node's current Tailscale settings.
//
// In this mode, API calls are authenticated via platform auth.
@@ -109,7 +110,7 @@ const (
// This mode restricts the app to only being assessible over Tailscale,
// and API calls are authenticated via browser sessions associated with
// the source's Tailscale identity. If the source browser does not have
// a valid session, a readonly version of the app is displayed.
// a valid session, a read-only version of the app is displayed.
ManageServerMode ServerMode = "manage"
)
@@ -124,9 +125,9 @@ type ServerOpts struct {
// PathPrefix is the URL prefix added to requests by CGI or reverse proxy.
PathPrefix string
// LocalClient is the tailscale.LocalClient to use for this web server.
// LocalClient is the local.Client to use for this web server.
// If nil, a new one will be created.
LocalClient *tailscale.LocalClient
LocalClient *local.Client
// TimeNow optionally provides a time function.
// time.Now is used as default.
@@ -165,7 +166,7 @@ func NewServer(opts ServerOpts) (s *Server, err error) {
return nil, fmt.Errorf("invalid Mode provided")
}
if opts.LocalClient == nil {
opts.LocalClient = &tailscale.LocalClient{}
opts.LocalClient = &local.Client{}
}
s = &Server{
mode: opts.Mode,
@@ -202,25 +203,9 @@ func NewServer(opts ServerOpts) (s *Server, err error) {
}
s.assetsHandler, s.assetsCleanup = assetsHandler(s.devMode)
var metric string // clientmetric to report on startup
// Create handler for "/api" requests with CSRF protection.
// We don't require secure cookies, since the web client is regularly used
// on network appliances that are served on local non-https URLs.
// The client is secured by limiting the interface it listens on,
// or by authenticating requests before they reach the web client.
csrfProtect := csrf.Protect(s.csrfKey(), csrf.Secure(false))
switch s.mode {
case LoginServerMode:
s.apiHandler = csrfProtect(http.HandlerFunc(s.serveLoginAPI))
metric = "web_login_client_initialization"
case ReadOnlyServerMode:
s.apiHandler = csrfProtect(http.HandlerFunc(s.serveLoginAPI))
metric = "web_readonly_client_initialization"
case ManageServerMode:
s.apiHandler = csrfProtect(http.HandlerFunc(s.serveAPI))
metric = "web_client_initialization"
}
var metric string
s.apiHandler, metric = s.modeAPIHandler(s.mode)
s.apiHandler = s.withCSRF(s.apiHandler)
// Don't block startup on reporting metric.
// Report in separate go routine with 5 second timeout.
@@ -233,6 +218,39 @@ func NewServer(opts ServerOpts) (s *Server, err error) {
return s, nil
}
func (s *Server) withCSRF(h http.Handler) http.Handler {
csrfProtect := csrf.Protect(s.csrfKey(), csrf.Secure(false))
// ref https://github.com/tailscale/tailscale/pull/14822
// signal to the CSRF middleware that the request is being served over
// plaintext HTTP to skip TLS-only header checks.
withSetPlaintext := func(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
r = csrf.PlaintextHTTPRequest(r)
h.ServeHTTP(w, r)
})
}
// NB: the order of the withSetPlaintext and csrfProtect calls is important
// to ensure that we signal to the CSRF middleware that the request is being
// served over plaintext HTTP and not over TLS as it presumes by default.
return withSetPlaintext(csrfProtect(h))
}
func (s *Server) modeAPIHandler(mode ServerMode) (http.Handler, string) {
switch mode {
case LoginServerMode:
return http.HandlerFunc(s.serveLoginAPI), "web_login_client_initialization"
case ReadOnlyServerMode:
return http.HandlerFunc(s.serveLoginAPI), "web_readonly_client_initialization"
case ManageServerMode:
return http.HandlerFunc(s.serveAPI), "web_client_initialization"
default: // invalid mode
log.Fatalf("invalid mode: %v", mode)
}
return nil, ""
}
func (s *Server) Shutdown() {
s.logf("web.Server: shutting down")
if s.assetsCleanup != nil {
@@ -317,7 +335,8 @@ func (s *Server) requireTailscaleIP(w http.ResponseWriter, r *http.Request) (han
ipv6ServiceHost = "[" + tsaddr.TailscaleServiceIPv6String + "]"
)
// allow requests on quad-100 (or ipv6 equivalent)
if r.Host == ipv4ServiceHost || r.Host == ipv6ServiceHost {
host := strings.TrimSuffix(r.Host, ":80")
if host == ipv4ServiceHost || host == ipv6ServiceHost {
return false
}
@@ -694,16 +713,16 @@ func (s *Server) serveAPIAuth(w http.ResponseWriter, r *http.Request) {
switch {
case sErr != nil && errors.Is(sErr, errNotUsingTailscale):
s.lc.IncrementCounter(r.Context(), "web_client_viewing_local", 1)
resp.Authorized = false // restricted to the readonly view
resp.Authorized = false // restricted to the read-only view
case sErr != nil && errors.Is(sErr, errNotOwner):
s.lc.IncrementCounter(r.Context(), "web_client_viewing_not_owner", 1)
resp.Authorized = false // restricted to the readonly view
resp.Authorized = false // restricted to the read-only view
case sErr != nil && errors.Is(sErr, errTaggedLocalSource):
s.lc.IncrementCounter(r.Context(), "web_client_viewing_local_tag", 1)
resp.Authorized = false // restricted to the readonly view
resp.Authorized = false // restricted to the read-only view
case sErr != nil && errors.Is(sErr, errTaggedRemoteSource):
s.lc.IncrementCounter(r.Context(), "web_client_viewing_remote_tag", 1)
resp.Authorized = false // restricted to the readonly view
resp.Authorized = false // restricted to the read-only view
case sErr != nil && !errors.Is(sErr, errNoSession):
// Any other error.
http.Error(w, sErr.Error(), http.StatusInternalServerError)
@@ -803,8 +822,8 @@ type nodeData struct {
DeviceName string
TailnetName string // TLS cert name
DomainName string
IPv4 string
IPv6 string
IPv4 netip.Addr
IPv6 netip.Addr
OS string
IPNVersion string
@@ -863,10 +882,14 @@ func (s *Server) serveGetNodeData(w http.ResponseWriter, r *http.Request) {
return
}
filterRules, _ := s.lc.DebugPacketFilterRules(r.Context())
ipv4, ipv6 := s.selfNodeAddresses(r, st)
data := &nodeData{
ID: st.Self.ID,
Status: st.BackendState,
DeviceName: strings.Split(st.Self.DNSName, ".")[0],
IPv4: ipv4,
IPv6: ipv6,
OS: st.Self.OS,
IPNVersion: strings.Split(st.Version, "-")[0],
Profile: st.User[st.Self.UserID],
@@ -886,10 +909,6 @@ func (s *Server) serveGetNodeData(w http.ResponseWriter, r *http.Request) {
ACLAllowsAnyIncomingTraffic: s.aclsAllowAccess(filterRules),
}
ipv4, ipv6 := s.selfNodeAddresses(r, st)
data.IPv4 = ipv4.String()
data.IPv6 = ipv6.String()
if hostinfo.GetEnvType() == hostinfo.HomeAssistantAddOn && data.URLPrefix == "" {
// X-Ingress-Path is the path prefix in use for Home Assistant
// https://developers.home-assistant.io/docs/add-ons/presentation#ingress
@@ -960,37 +979,16 @@ func (s *Server) serveGetNodeData(w http.ResponseWriter, r *http.Request) {
}
func availableFeatures() map[string]bool {
env := hostinfo.GetEnvType()
features := map[string]bool{
"advertise-exit-node": true, // available on all platforms
"advertise-routes": true, // available on all platforms
"use-exit-node": canUseExitNode(env) == nil,
"ssh": envknob.CanRunTailscaleSSH() == nil,
"use-exit-node": featureknob.CanUseExitNode() == nil,
"ssh": featureknob.CanRunTailscaleSSH() == nil,
"auto-update": version.IsUnstableBuild() && clientupdate.CanAutoUpdate(),
}
if env == hostinfo.HomeAssistantAddOn {
// Setting SSH on Home Assistant causes trouble on startup
// (since the flag is not being passed to `tailscale up`).
// Although Tailscale SSH does work here,
// it's not terribly useful since it's running in a separate container.
features["ssh"] = false
}
return features
}
func canUseExitNode(env hostinfo.EnvType) error {
switch dist := distro.Get(); dist {
case distro.Synology, // see https://github.com/tailscale/tailscale/issues/1995
distro.QNAP,
distro.Unraid:
return fmt.Errorf("Tailscale exit nodes cannot be used on %s.", dist)
}
if env == hostinfo.HomeAssistantAddOn {
return errors.New("Tailscale exit nodes cannot be used on Home Assistant.")
}
return nil
}
// aclsAllowAccess returns whether tailnet ACLs (as expressed in the provided filter rules)
// permit any devices to access the local web client.
// This does not currently check whether a specific device can connect, just any device.

View File

@@ -27,6 +27,8 @@ import (
"strconv"
"strings"
"tailscale.com/hostinfo"
"tailscale.com/types/lazy"
"tailscale.com/types/logger"
"tailscale.com/util/cmpver"
"tailscale.com/version"
@@ -169,6 +171,12 @@ func NewUpdater(args Arguments) (*Updater, error) {
type updateFunction func() error
func (up *Updater) getUpdateFunction() (fn updateFunction, canAutoUpdate bool) {
hi := hostinfo.New()
// We don't know how to update custom tsnet binaries, it's up to the user.
if hi.Package == "tsnet" {
return nil, false
}
switch runtime.GOOS {
case "windows":
return up.updateWindows, true
@@ -242,9 +250,13 @@ func (up *Updater) getUpdateFunction() (fn updateFunction, canAutoUpdate bool) {
return nil, false
}
var canAutoUpdateCache lazy.SyncValue[bool]
// CanAutoUpdate reports whether auto-updating via the clientupdate package
// is supported for the current os/distro.
func CanAutoUpdate() bool {
func CanAutoUpdate() bool { return canAutoUpdateCache.Get(canAutoUpdateUncached) }
func canAutoUpdateUncached() bool {
if version.IsMacSysExt() {
// Macsys uses Sparkle for auto-updates, which doesn't have an update
// function in this package.

View File

@@ -21,6 +21,7 @@ import (
"tailscale.com/types/netmap"
"tailscale.com/types/persist"
"tailscale.com/types/structs"
"tailscale.com/util/clientmetric"
"tailscale.com/util/execqueue"
)
@@ -118,6 +119,7 @@ type Auto struct {
updateCh chan struct{} // readable when we should inform the server of a change
observer Observer // called to update Client status; always non-nil
observerQueue execqueue.ExecQueue
shutdownFn func() // to be called prior to shutdown or nil
unregisterHealthWatch func()
@@ -131,6 +133,8 @@ type Auto struct {
// the server.
lastUpdateGen updateGen
lastStatus atomic.Pointer[Status]
paused bool // whether we should stop making HTTP requests
unpauseWaiters []chan bool // chans that gets sent true (once) on wake, or false on Shutdown
loggedIn bool // true if currently logged in
@@ -186,6 +190,7 @@ func NewNoStart(opts Options) (_ *Auto, err error) {
mapDone: make(chan struct{}),
updateDone: make(chan struct{}),
observer: opts.Observer,
shutdownFn: opts.Shutdown,
}
c.authCtx, c.authCancel = context.WithCancel(context.Background())
c.authCtx = sockstats.WithSockStats(c.authCtx, sockstats.LabelControlClientAuto, opts.Logf)
@@ -596,21 +601,90 @@ func (c *Auto) sendStatus(who string, err error, url string, nm *netmap.NetworkM
// not logged in.
nm = nil
}
new := Status{
newSt := &Status{
URL: url,
Persist: p,
NetMap: nm,
Err: err,
state: state,
}
c.lastStatus.Store(newSt)
// Launch a new goroutine to avoid blocking the caller while the observer
// does its thing, which may result in a call back into the client.
metricQueued.Add(1)
c.observerQueue.Add(func() {
c.observer.SetControlClientStatus(c, new)
if canSkipStatus(newSt, c.lastStatus.Load()) {
metricSkippable.Add(1)
if !c.direct.controlKnobs.DisableSkipStatusQueue.Load() {
metricSkipped.Add(1)
return
}
}
c.observer.SetControlClientStatus(c, *newSt)
// Best effort stop retaining the memory now that we've sent it to the
// observer (LocalBackend). We CAS here because the caller goroutine is
// doing a Store which we want to win a race. This is only a memory
// optimization and is not for correctness.
//
// If the CAS fails, that means somebody else's Store replaced our
// pointer (so mission accomplished: our netmap is no longer retained in
// any case) and that Store caller will be responsible for removing
// their own netmap (or losing their race too, down the chain).
// Eventually the last caller will win this CAS and zero lastStatus.
c.lastStatus.CompareAndSwap(newSt, nil)
})
}
var (
metricQueued = clientmetric.NewCounter("controlclient_auto_status_queued")
metricSkippable = clientmetric.NewCounter("controlclient_auto_status_queue_skippable")
metricSkipped = clientmetric.NewCounter("controlclient_auto_status_queue_skipped")
)
// canSkipStatus reports whether we can skip sending s1, knowing
// that s2 is enqueued sometime in the future after s1.
//
// s1 must be non-nil. s2 may be nil.
func canSkipStatus(s1, s2 *Status) bool {
if s2 == nil {
// Nothing in the future.
return false
}
if s1 == s2 {
// If the last item in the queue is the same as s1,
// we can't skip it.
return false
}
if s1.Err != nil || s1.URL != "" {
// If s1 has an error or a URL, we shouldn't skip it, lest the error go
// away in s2 or in-between. We want to make sure all the subsystems see
// it. Plus there aren't many of these, so not worth skipping.
return false
}
if !s1.Persist.Equals(s2.Persist) || s1.state != s2.state {
// If s1 has a different Persist or state than s2,
// don't skip it. We only care about skipping the typical
// entries where the only difference is the NetMap.
return false
}
// If nothing above precludes it, and both s1 and s2 have NetMaps, then
// we can skip it, because s2's NetMap is a newer version and we can
// jump straight from whatever state we had before to s2's state,
// without passing through s1's state first. A NetMap is regrettably a
// full snapshot of the state, not an incremental delta. We're slowly
// moving towards passing around only deltas around internally at all
// layers, but this is explicitly the case where we didn't have a delta
// path for the message we received over the wire and had to resort
// to the legacy full NetMap path. And then we can get behind processing
// these full NetMap snapshots in LocalBackend/wgengine/magicsock/netstack
// and this path (when it returns true) lets us skip over useless work
// and not get behind in the queue. This matters in particular for tailnets
// that are both very large + very churny.
return s1.NetMap != nil && s2.NetMap != nil
}
func (c *Auto) Login(flags LoginFlags) {
c.logf("client.Login(%v)", flags)
@@ -683,6 +757,7 @@ func (c *Auto) Shutdown() {
return
}
c.logf("client.Shutdown ...")
shutdownFn := c.shutdownFn
direct := c.direct
c.closed = true
@@ -695,6 +770,10 @@ func (c *Auto) Shutdown() {
c.unpauseWaiters = nil
c.mu.Unlock()
if shutdownFn != nil {
shutdownFn()
}
c.unregisterHealthWatch()
<-c.authDone
<-c.mapDone

View File

@@ -15,7 +15,6 @@ import (
"log"
"net"
"net/http"
"net/http/httptest"
"net/netip"
"net/url"
"os"
@@ -42,6 +41,7 @@ import (
"tailscale.com/net/tsdial"
"tailscale.com/net/tshttpproxy"
"tailscale.com/tailcfg"
"tailscale.com/tempfork/httprec"
"tailscale.com/tka"
"tailscale.com/tstime"
"tailscale.com/types/key"
@@ -156,6 +156,11 @@ type Options struct {
// If we receive a new DialPlan from the server, this value will be
// updated.
DialPlan ControlDialPlanner
// Shutdown is an optional function that will be called before client shutdown is
// attempted. It is used to allow the client to clean up any resources or complete any
// tasks that are dependent on a live client.
Shutdown func()
}
// ControlDialPlanner is the interface optionally supplied when creating a
@@ -650,7 +655,7 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
c.logf("RegisterReq sign error: %v", err)
}
}
if debugRegister() {
if DevKnob.DumpRegister() {
j, _ := json.MarshalIndent(request, "", "\t")
c.logf("RegisterRequest: %s", j)
}
@@ -691,7 +696,7 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
c.logf("error decoding RegisterResponse with server key %s and machine key %s: %v", serverKey, machinePrivKey.Public(), err)
return regen, opt.URL, nil, fmt.Errorf("register request: %v", err)
}
if debugRegister() {
if DevKnob.DumpRegister() {
j, _ := json.MarshalIndent(resp, "", "\t")
c.logf("RegisterResponse: %s", j)
}
@@ -877,7 +882,7 @@ func (c *Direct) sendMapRequest(ctx context.Context, isStreaming bool, nu Netmap
c.logf("[v1] PollNetMap: stream=%v ep=%v", isStreaming, epStrs)
vlogf := logger.Discard
if DevKnob.DumpNetMaps() {
if DevKnob.DumpNetMapsVerbose() {
// TODO(bradfitz): update this to use "[v2]" prefix perhaps? but we don't
// want to upload it always.
vlogf = c.logf
@@ -1003,7 +1008,9 @@ func (c *Direct) sendMapRequest(ctx context.Context, isStreaming bool, nu Netmap
if persist == c.persist {
newPersist := persist.AsStruct()
newPersist.NodeID = nm.SelfNode.StableID()
newPersist.UserProfile = nm.UserProfiles[nm.User()]
if up, ok := nm.UserProfiles[nm.User()]; ok {
newPersist.UserProfile = *up.AsStruct()
}
c.persist = newPersist.View()
persist = c.persist
@@ -1170,11 +1177,6 @@ func decode(res *http.Response, v any) error {
return json.Unmarshal(msg, v)
}
var (
debugMap = envknob.RegisterBool("TS_DEBUG_MAP")
debugRegister = envknob.RegisterBool("TS_DEBUG_REGISTER")
)
var jsonEscapedZero = []byte(`\u0000`)
// decodeMsg is responsible for uncompressing msg and unmarshaling into v.
@@ -1183,7 +1185,7 @@ func (c *Direct) decodeMsg(compressedMsg []byte, v any) error {
if err != nil {
return err
}
if debugMap() {
if DevKnob.DumpNetMaps() {
var buf bytes.Buffer
json.Indent(&buf, b, "", " ")
log.Printf("MapResponse: %s", buf.Bytes())
@@ -1205,7 +1207,7 @@ func encode(v any) ([]byte, error) {
if err != nil {
return nil, err
}
if debugMap() {
if DevKnob.DumpNetMaps() {
if _, ok := v.(*tailcfg.MapRequest); ok {
log.Printf("MapRequest: %s", b)
}
@@ -1253,18 +1255,25 @@ func loadServerPubKeys(ctx context.Context, httpc *http.Client, serverURL string
var DevKnob = initDevKnob()
type devKnobs struct {
DumpNetMaps func() bool
ForceProxyDNS func() bool
StripEndpoints func() bool // strip endpoints from control (only use disco messages)
StripCaps func() bool // strip all local node's control-provided capabilities
DumpRegister func() bool
DumpNetMaps func() bool
DumpNetMapsVerbose func() bool
ForceProxyDNS func() bool
StripEndpoints func() bool // strip endpoints from control (only use disco messages)
StripHomeDERP func() bool // strip Home DERP from control
StripCaps func() bool // strip all local node's control-provided capabilities
}
func initDevKnob() devKnobs {
nm := envknob.RegisterInt("TS_DEBUG_MAP")
return devKnobs{
DumpNetMaps: envknob.RegisterBool("TS_DEBUG_NETMAP"),
ForceProxyDNS: envknob.RegisterBool("TS_DEBUG_PROXY_DNS"),
StripEndpoints: envknob.RegisterBool("TS_DEBUG_STRIP_ENDPOINTS"),
StripCaps: envknob.RegisterBool("TS_DEBUG_STRIP_CAPS"),
DumpNetMaps: func() bool { return nm() > 0 },
DumpNetMapsVerbose: func() bool { return nm() > 1 },
DumpRegister: envknob.RegisterBool("TS_DEBUG_REGISTER"),
ForceProxyDNS: envknob.RegisterBool("TS_DEBUG_PROXY_DNS"),
StripEndpoints: envknob.RegisterBool("TS_DEBUG_STRIP_ENDPOINTS"),
StripHomeDERP: envknob.RegisterBool("TS_DEBUG_STRIP_HOME_DERP"),
StripCaps: envknob.RegisterBool("TS_DEBUG_STRIP_CAPS"),
}
}
@@ -1384,7 +1393,7 @@ func answerC2NPing(logf logger.Logf, c2nHandler http.Handler, c *http.Client, pr
handlerCtx, cancel := context.WithTimeout(context.Background(), handlerTimeout)
defer cancel()
hreq = hreq.WithContext(handlerCtx)
rec := httptest.NewRecorder()
rec := httprec.NewRecorder()
c2nHandler.ServeHTTP(rec, hreq)
cancel()
@@ -1643,6 +1652,97 @@ func (c *Direct) ReportHealthChange(w *health.Warnable, us *health.UnhealthyStat
res.Body.Close()
}
// SetDeviceAttrs does a synchronous call to the control plane to update
// the node's attributes.
//
// See docs on [tailcfg.SetDeviceAttributesRequest] for background.
func (c *Auto) SetDeviceAttrs(ctx context.Context, attrs tailcfg.AttrUpdate) error {
return c.direct.SetDeviceAttrs(ctx, attrs)
}
// SetDeviceAttrs does a synchronous call to the control plane to update
// the node's attributes.
//
// See docs on [tailcfg.SetDeviceAttributesRequest] for background.
func (c *Direct) SetDeviceAttrs(ctx context.Context, attrs tailcfg.AttrUpdate) error {
nc, err := c.getNoiseClient()
if err != nil {
return fmt.Errorf("%w: %w", errNoNoiseClient, err)
}
nodeKey, ok := c.GetPersist().PublicNodeKeyOK()
if !ok {
return errNoNodeKey
}
if c.panicOnUse {
panic("tainted client")
}
req := &tailcfg.SetDeviceAttributesRequest{
NodeKey: nodeKey,
Version: tailcfg.CurrentCapabilityVersion,
Update: attrs,
}
// TODO(bradfitz): unify the callers using doWithBody vs those using
// DoNoiseRequest. There seems to be a ~50/50 split and they're very close,
// but doWithBody sets the load balancing header and auto-JSON-encodes the
// body, but DoNoiseRequest is exported. Clean it up so they're consistent
// one way or another.
ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
defer cancel()
res, err := nc.doWithBody(ctx, "PATCH", "/machine/set-device-attr", nodeKey, req)
if err != nil {
return err
}
defer res.Body.Close()
all, _ := io.ReadAll(res.Body)
if res.StatusCode != 200 {
return fmt.Errorf("HTTP error from control plane: %v: %s", res.Status, all)
}
return nil
}
// SendAuditLog implements [auditlog.Transport] by sending an audit log synchronously to the control plane.
//
// See docs on [tailcfg.AuditLogRequest] and [auditlog.Logger] for background.
func (c *Auto) SendAuditLog(ctx context.Context, auditLog tailcfg.AuditLogRequest) (err error) {
return c.direct.sendAuditLog(ctx, auditLog)
}
func (c *Direct) sendAuditLog(ctx context.Context, auditLog tailcfg.AuditLogRequest) (err error) {
nc, err := c.getNoiseClient()
if err != nil {
return fmt.Errorf("%w: %w", errNoNoiseClient, err)
}
nodeKey, ok := c.GetPersist().PublicNodeKeyOK()
if !ok {
return errNoNodeKey
}
req := &tailcfg.AuditLogRequest{
Version: tailcfg.CurrentCapabilityVersion,
NodeKey: nodeKey,
Action: auditLog.Action,
Details: auditLog.Details,
}
if c.panicOnUse {
panic("tainted client")
}
res, err := nc.post(ctx, "/machine/audit-log", nodeKey, req)
if err != nil {
return fmt.Errorf("%w: %w", errHTTPPostFailure, err)
}
defer res.Body.Close()
if res.StatusCode != 200 {
all, _ := io.ReadAll(res.Body)
return errBadHTTPResponse(res.StatusCode, string(all))
}
return nil
}
func addLBHeader(req *http.Request, nodeKey key.NodePublic) {
if !nodeKey.IsZero() {
req.Header.Add(tailcfg.LBHeader, nodeKey.String())

51
vendor/tailscale.com/control/controlclient/errors.go generated vendored Normal file
View File

@@ -0,0 +1,51 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package controlclient
import (
"errors"
"fmt"
"net/http"
)
// apiResponseError is an error type that can be returned by controlclient
// api requests.
//
// It wraps an underlying error and a flag for clients to query if the
// error is retryable via the Retryable() method.
type apiResponseError struct {
err error
retryable bool
}
// Error implements [error].
func (e *apiResponseError) Error() string {
return e.err.Error()
}
// Retryable reports whether the error is retryable.
func (e *apiResponseError) Retryable() bool {
return e.retryable
}
func (e *apiResponseError) Unwrap() error { return e.err }
var (
errNoNodeKey = &apiResponseError{errors.New("no node key"), true}
errNoNoiseClient = &apiResponseError{errors.New("no noise client"), true}
errHTTPPostFailure = &apiResponseError{errors.New("http failure"), true}
)
func errBadHTTPResponse(code int, msg string) error {
retryable := false
switch code {
case http.StatusTooManyRequests,
http.StatusInternalServerError,
http.StatusBadGateway,
http.StatusServiceUnavailable,
http.StatusGatewayTimeout:
retryable = true
}
return &apiResponseError{fmt.Errorf("http error %d: %s", code, msg), retryable}
}

View File

@@ -7,14 +7,12 @@ import (
"cmp"
"context"
"encoding/json"
"fmt"
"maps"
"net"
"reflect"
"runtime"
"runtime/debug"
"slices"
"sort"
"strconv"
"sync"
"time"
@@ -31,6 +29,7 @@ import (
"tailscale.com/util/clientmetric"
"tailscale.com/util/mak"
"tailscale.com/util/set"
"tailscale.com/util/slicesx"
"tailscale.com/wgengine/filter"
)
@@ -75,11 +74,10 @@ type mapSession struct {
lastPrintMap time.Time
lastNode tailcfg.NodeView
lastCapSet set.Set[tailcfg.NodeCapability]
peers map[tailcfg.NodeID]*tailcfg.NodeView // pointer to view (oddly). same pointers as sortedPeers.
sortedPeers []*tailcfg.NodeView // same pointers as peers, but sorted by Node.ID
peers map[tailcfg.NodeID]tailcfg.NodeView
lastDNSConfig *tailcfg.DNSConfig
lastDERPMap *tailcfg.DERPMap
lastUserProfile map[tailcfg.UserID]tailcfg.UserProfile
lastUserProfile map[tailcfg.UserID]tailcfg.UserProfileView
lastPacketFilterRules views.Slice[tailcfg.FilterRule] // concatenation of all namedPacketFilters
namedPacketFilters map[string]views.Slice[tailcfg.FilterRule]
lastParsedPacketFilter []filter.Match
@@ -91,7 +89,6 @@ type mapSession struct {
lastPopBrowserURL string
lastTKAInfo *tailcfg.TKAInfo
lastNetmapSummary string // from NetworkMap.VeryConcise
lastMaxExpiry time.Duration
}
// newMapSession returns a mostly unconfigured new mapSession.
@@ -106,7 +103,7 @@ func newMapSession(privateNodeKey key.NodePrivate, nu NetmapUpdater, controlKnob
privateNodeKey: privateNodeKey,
publicNodeKey: privateNodeKey.Public(),
lastDNSConfig: new(tailcfg.DNSConfig),
lastUserProfile: map[tailcfg.UserID]tailcfg.UserProfile{},
lastUserProfile: map[tailcfg.UserID]tailcfg.UserProfileView{},
// Non-nil no-op defaults, to be optionally overridden by the caller.
logf: logger.Discard,
@@ -167,6 +164,7 @@ func (ms *mapSession) HandleNonKeepAliveMapResponse(ctx context.Context, resp *t
// For responses that mutate the self node, check for updated nodeAttrs.
if resp.Node != nil {
upgradeNode(resp.Node)
if DevKnob.StripCaps() {
resp.Node.Capabilities = nil
resp.Node.CapMap = nil
@@ -182,6 +180,13 @@ func (ms *mapSession) HandleNonKeepAliveMapResponse(ctx context.Context, resp *t
ms.controlKnobs.UpdateFromNodeAttributes(resp.Node.CapMap)
}
for _, p := range resp.Peers {
upgradeNode(p)
}
for _, p := range resp.PeersChanged {
upgradeNode(p)
}
// Call Node.InitDisplayNames on any changed nodes.
initDisplayNames(cmp.Or(resp.Node.View(), ms.lastNode), resp)
@@ -217,6 +222,33 @@ func (ms *mapSession) HandleNonKeepAliveMapResponse(ctx context.Context, resp *t
return nil
}
// upgradeNode upgrades Node fields from the server into the modern forms
// not using deprecated fields.
func upgradeNode(n *tailcfg.Node) {
if n == nil {
return
}
if n.LegacyDERPString != "" {
if n.HomeDERP == 0 {
ip, portStr, err := net.SplitHostPort(n.LegacyDERPString)
if ip == tailcfg.DerpMagicIP && err == nil {
port, err := strconv.Atoi(portStr)
if err == nil {
n.HomeDERP = port
}
}
}
n.LegacyDERPString = ""
}
if DevKnob.StripHomeDERP() {
n.HomeDERP = 0
}
if n.AllowedIPs == nil {
n.AllowedIPs = slices.Clone(n.Addresses)
}
}
func (ms *mapSession) tryHandleIncrementally(res *tailcfg.MapResponse) bool {
if ms.controlKnobs != nil && ms.controlKnobs.DisableDeltaUpdates.Load() {
return false
@@ -260,13 +292,22 @@ func (ms *mapSession) updateStateFromResponse(resp *tailcfg.MapResponse) {
}
for _, up := range resp.UserProfiles {
ms.lastUserProfile[up.ID] = up
ms.lastUserProfile[up.ID] = up.View()
}
// TODO(bradfitz): clean up old user profiles? maybe not worth it.
if dm := resp.DERPMap; dm != nil {
ms.vlogf("netmap: new map contains DERP map")
// Guard against the control server accidentally sending
// a nil region definition, which at least Headscale was
// observed to send.
for rid, r := range dm.Regions {
if r == nil {
delete(dm.Regions, rid)
}
}
// 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 {
@@ -345,9 +386,6 @@ func (ms *mapSession) updateStateFromResponse(resp *tailcfg.MapResponse) {
if resp.TKAInfo != nil {
ms.lastTKAInfo = resp.TKAInfo
}
if resp.MaxKeyDuration > 0 {
ms.lastMaxExpiry = resp.MaxKeyDuration
}
}
var (
@@ -366,16 +404,11 @@ var (
patchifiedPeerEqual = clientmetric.NewCounter("controlclient_patchified_peer_equal")
)
// updatePeersStateFromResponseres updates ms.peers and ms.sortedPeers from res. It takes ownership of res.
// updatePeersStateFromResponseres updates ms.peers from resp.
// It takes ownership of resp.
func (ms *mapSession) updatePeersStateFromResponse(resp *tailcfg.MapResponse) (stats updateStats) {
defer func() {
if stats.removed > 0 || stats.added > 0 {
ms.rebuildSorted()
}
}()
if ms.peers == nil {
ms.peers = make(map[tailcfg.NodeID]*tailcfg.NodeView)
ms.peers = make(map[tailcfg.NodeID]tailcfg.NodeView)
}
if len(resp.Peers) > 0 {
@@ -384,12 +417,12 @@ func (ms *mapSession) updatePeersStateFromResponse(resp *tailcfg.MapResponse) (s
keep := make(map[tailcfg.NodeID]bool, len(resp.Peers))
for _, n := range resp.Peers {
keep[n.ID] = true
if vp, ok := ms.peers[n.ID]; ok {
lenBefore := len(ms.peers)
ms.peers[n.ID] = n.View()
if len(ms.peers) == lenBefore {
stats.changed++
*vp = n.View()
} else {
stats.added++
ms.peers[n.ID] = ptr.To(n.View())
}
}
for id := range ms.peers {
@@ -410,12 +443,12 @@ func (ms *mapSession) updatePeersStateFromResponse(resp *tailcfg.MapResponse) (s
}
for _, n := range resp.PeersChanged {
if vp, ok := ms.peers[n.ID]; ok {
lenBefore := len(ms.peers)
ms.peers[n.ID] = n.View()
if len(ms.peers) == lenBefore {
stats.changed++
*vp = n.View()
} else {
stats.added++
ms.peers[n.ID] = ptr.To(n.View())
}
}
@@ -427,7 +460,7 @@ func (ms *mapSession) updatePeersStateFromResponse(resp *tailcfg.MapResponse) (s
} else {
mut.LastSeen = nil
}
*vp = mut.View()
ms.peers[nodeID] = mut.View()
stats.changed++
}
}
@@ -436,7 +469,7 @@ func (ms *mapSession) updatePeersStateFromResponse(resp *tailcfg.MapResponse) (s
if vp, ok := ms.peers[nodeID]; ok {
mut := vp.AsStruct()
mut.Online = ptr.To(online)
*vp = mut.View()
ms.peers[nodeID] = mut.View()
stats.changed++
}
}
@@ -449,7 +482,7 @@ func (ms *mapSession) updatePeersStateFromResponse(resp *tailcfg.MapResponse) (s
stats.changed++
mut := vp.AsStruct()
if pc.DERPRegion != 0 {
mut.DERP = fmt.Sprintf("%s:%v", tailcfg.DerpMagicIP, pc.DERPRegion)
mut.HomeDERP = pc.DERPRegion
patchDERPRegion.Add(1)
}
if pc.Cap != 0 {
@@ -488,31 +521,12 @@ func (ms *mapSession) updatePeersStateFromResponse(resp *tailcfg.MapResponse) (s
mut.CapMap = v
patchCapMap.Add(1)
}
*vp = mut.View()
ms.peers[pc.NodeID] = mut.View()
}
return
}
// rebuildSorted rebuilds ms.sortedPeers from ms.peers. It should be called
// after any additions or removals from peers.
func (ms *mapSession) rebuildSorted() {
if ms.sortedPeers == nil {
ms.sortedPeers = make([]*tailcfg.NodeView, 0, len(ms.peers))
} else {
if len(ms.sortedPeers) > len(ms.peers) {
clear(ms.sortedPeers[len(ms.peers):])
}
ms.sortedPeers = ms.sortedPeers[:0]
}
for _, p := range ms.peers {
ms.sortedPeers = append(ms.sortedPeers, p)
}
sort.Slice(ms.sortedPeers, func(i, j int) bool {
return ms.sortedPeers[i].ID() < ms.sortedPeers[j].ID()
})
}
func (ms *mapSession) addUserProfile(nm *netmap.NetworkMap, userID tailcfg.UserID) {
if userID == 0 {
return
@@ -576,7 +590,7 @@ func (ms *mapSession) patchifyPeer(n *tailcfg.Node) (_ *tailcfg.PeerChange, ok b
if !ok {
return nil, false
}
return peerChangeDiff(*was, n)
return peerChangeDiff(was, n)
}
// peerChangeDiff returns the difference from 'was' to 'n', if possible.
@@ -656,17 +670,13 @@ func peerChangeDiff(was tailcfg.NodeView, n *tailcfg.Node) (_ *tailcfg.PeerChang
if !views.SliceEqual(was.Endpoints(), views.SliceOf(n.Endpoints)) {
pc().Endpoints = slices.Clone(n.Endpoints)
}
case "DERP":
if was.DERP() != n.DERP {
ip, portStr, err := net.SplitHostPort(n.DERP)
if err != nil || ip != "127.3.3.40" {
return nil, false
}
port, err := strconv.Atoi(portStr)
if err != nil || port < 1 || port > 65535 {
return nil, false
}
pc().DERPRegion = port
case "LegacyDERPString":
if was.LegacyDERPString() != "" || n.LegacyDERPString != "" {
panic("unexpected; caller should've already called upgradeNode")
}
case "HomeDERP":
if was.HomeDERP() != n.HomeDERP {
pc().DERPRegion = n.HomeDERP
}
case "Hostinfo":
if !was.Hostinfo().Valid() && !n.Hostinfo.Valid() {
@@ -688,21 +698,23 @@ func peerChangeDiff(was tailcfg.NodeView, n *tailcfg.Node) (_ *tailcfg.PeerChang
}
case "CapMap":
if len(n.CapMap) != was.CapMap().Len() {
// If they have different lengths, they're different.
if n.CapMap == nil {
pc().CapMap = make(tailcfg.NodeCapMap)
} else {
pc().CapMap = maps.Clone(n.CapMap)
}
break
}
was.CapMap().Range(func(k tailcfg.NodeCapability, v views.Slice[tailcfg.RawMessage]) bool {
nv, ok := n.CapMap[k]
if !ok || !views.SliceEqual(v, views.SliceOf(nv)) {
pc().CapMap = maps.Clone(n.CapMap)
return false
} else {
// If they have the same length, check that all their keys
// have the same values.
for k, v := range was.CapMap().All() {
nv, ok := n.CapMap[k]
if !ok || !views.SliceEqual(v, views.SliceOf(nv)) {
pc().CapMap = maps.Clone(n.CapMap)
break
}
}
return true
})
}
case "Tags":
if !views.SliceEqual(was.Tags(), views.SliceOf(n.Tags)) {
return nil, false
@@ -712,13 +724,11 @@ func peerChangeDiff(was tailcfg.NodeView, n *tailcfg.Node) (_ *tailcfg.PeerChang
return nil, false
}
case "Online":
wasOnline := was.Online()
if n.Online != nil && wasOnline != nil && *n.Online != *wasOnline {
if wasOnline, ok := was.Online().GetOk(); ok && n.Online != nil && *n.Online != wasOnline {
pc().Online = ptr.To(*n.Online)
}
case "LastSeen":
wasSeen := was.LastSeen()
if n.LastSeen != nil && wasSeen != nil && !wasSeen.Equal(*n.LastSeen) {
if wasSeen, ok := was.LastSeen().GetOk(); ok && n.LastSeen != nil && !wasSeen.Equal(*n.LastSeen) {
pc().LastSeen = ptr.To(*n.LastSeen)
}
case "MachineAuthorized":
@@ -743,18 +753,18 @@ func peerChangeDiff(was tailcfg.NodeView, n *tailcfg.Node) (_ *tailcfg.PeerChang
}
case "SelfNodeV4MasqAddrForThisPeer":
va, vb := was.SelfNodeV4MasqAddrForThisPeer(), n.SelfNodeV4MasqAddrForThisPeer
if va == nil && vb == nil {
if !va.Valid() && vb == nil {
continue
}
if va == nil || vb == nil || *va != *vb {
if va, ok := va.GetOk(); !ok || vb == nil || va != *vb {
return nil, false
}
case "SelfNodeV6MasqAddrForThisPeer":
va, vb := was.SelfNodeV6MasqAddrForThisPeer(), n.SelfNodeV6MasqAddrForThisPeer
if va == nil && vb == nil {
if !va.Valid() && vb == nil {
continue
}
if va == nil || vb == nil || *va != *vb {
if va, ok := va.GetOk(); !ok || vb == nil || va != *vb {
return nil, false
}
case "ExitNodeDNSResolvers":
@@ -778,21 +788,26 @@ func peerChangeDiff(was tailcfg.NodeView, n *tailcfg.Node) (_ *tailcfg.PeerChang
return ret, true
}
func (ms *mapSession) sortedPeers() []tailcfg.NodeView {
ret := slicesx.MapValues(ms.peers)
slices.SortFunc(ret, func(a, b tailcfg.NodeView) int {
return cmp.Compare(a.ID(), b.ID())
})
return ret
}
// netmap returns a fully populated NetworkMap from the last state seen from
// a call to updateStateFromResponse, filling in omitted
// information from prior MapResponse values.
func (ms *mapSession) netmap() *netmap.NetworkMap {
peerViews := make([]tailcfg.NodeView, len(ms.sortedPeers))
for i, vp := range ms.sortedPeers {
peerViews[i] = *vp
}
peerViews := ms.sortedPeers()
nm := &netmap.NetworkMap{
NodeKey: ms.publicNodeKey,
PrivateKey: ms.privateNodeKey,
MachineKey: ms.machinePubKey,
Peers: peerViews,
UserProfiles: make(map[tailcfg.UserID]tailcfg.UserProfile),
UserProfiles: make(map[tailcfg.UserID]tailcfg.UserProfileView),
Domain: ms.lastDomain,
DomainAuditLogID: ms.lastDomainAuditLogID,
DNS: *ms.lastDNSConfig,
@@ -803,7 +818,6 @@ func (ms *mapSession) netmap() *netmap.NetworkMap {
DERPMap: ms.lastDERPMap,
ControlHealth: ms.lastHealth,
TKAEnabled: ms.lastTKAInfo != nil && !ms.lastTKAInfo.Disabled,
MaxKeyDuration: ms.lastMaxExpiry,
}
if ms.lastTKAInfo != nil && ms.lastTKAInfo.Head != "" {

View File

@@ -11,13 +11,13 @@ import (
"errors"
"math"
"net/http"
"net/netip"
"net/url"
"sync"
"time"
"golang.org/x/net/http2"
"tailscale.com/control/controlhttp"
"tailscale.com/envknob"
"tailscale.com/health"
"tailscale.com/internal/noiseconn"
"tailscale.com/net/dnscache"
@@ -30,7 +30,6 @@ import (
"tailscale.com/util/mak"
"tailscale.com/util/multierr"
"tailscale.com/util/singleflight"
"tailscale.com/util/testenv"
)
// NoiseClient provides a http.Client to connect to tailcontrol over
@@ -107,35 +106,45 @@ type NoiseOpts struct {
DialPlan func() *tailcfg.ControlDialPlan
}
// controlIsPlaintext is whether we should assume that the controlplane is only accessible
// over plaintext HTTP (as the first hop, before the ts2021 encryption begins).
// This is used by some tests which don't have a real TLS certificate.
var controlIsPlaintext = envknob.RegisterBool("TS_CONTROL_IS_PLAINTEXT_HTTP")
// 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) {
logf := opts.Logf
u, err := url.Parse(opts.ServerURL)
if err != nil {
return nil, err
}
if u.Scheme != "http" && u.Scheme != "https" {
return nil, errors.New("invalid ServerURL scheme, must be http or https")
}
var httpPort string
var httpsPort string
addr, _ := netip.ParseAddr(u.Hostname())
isPrivateHost := addr.IsPrivate() || addr.IsLoopback() || u.Hostname() == "localhost"
if port := u.Port(); port != "" {
// If there is an explicit port specified, trust the scheme and hope for the best
if u.Scheme == "http" {
// If there is an explicit port specified, entirely rely on the scheme,
// unless it's http with a private host in which case we never try using HTTPS.
if u.Scheme == "https" {
httpPort = ""
httpsPort = port
} else if u.Scheme == "http" {
httpPort = port
httpsPort = "443"
if (testenv.InTest() || controlIsPlaintext()) && (u.Hostname() == "127.0.0.1" || u.Hostname() == "localhost") {
if isPrivateHost {
logf("setting empty HTTPS port with http scheme and private host %s", u.Hostname())
httpsPort = ""
}
} else {
httpPort = "80"
httpsPort = port
}
} else if u.Scheme == "http" && isPrivateHost {
// Whenever the scheme is http and the hostname is an IP address, do not set the HTTPS port,
// as there cannot be a TLS certificate issued for an IP, unless it's a public IP.
httpPort = "80"
httpsPort = ""
} else {
// Otherwise, use the standard ports
httpPort = "80"
@@ -387,17 +396,20 @@ func (nc *NoiseClient) dial(ctx context.Context) (*noiseconn.Conn, error) {
// post does a POST to the control server at the given path, JSON-encoding body.
// The provided nodeKey is an optional load balancing hint.
func (nc *NoiseClient) post(ctx context.Context, path string, nodeKey key.NodePublic, body any) (*http.Response, error) {
return nc.doWithBody(ctx, "POST", path, nodeKey, body)
}
func (nc *NoiseClient) doWithBody(ctx context.Context, method, path string, nodeKey key.NodePublic, body any) (*http.Response, error) {
jbody, err := json.Marshal(body)
if err != nil {
return nil, err
}
req, err := http.NewRequestWithContext(ctx, "POST", "https://"+nc.host+path, bytes.NewReader(jbody))
req, err := http.NewRequestWithContext(ctx, method, "https://"+nc.host+path, bytes.NewReader(jbody))
if err != nil {
return nil, err
}
addLBHeader(req, nodeKey)
req.Header.Set("Content-Type", "application/json")
conn, err := nc.getConn(ctx)
if err != nil {
return nil, err

View File

@@ -13,7 +13,6 @@ import (
"crypto/x509"
"errors"
"fmt"
"sync"
"time"
"github.com/tailscale/certstore"
@@ -22,11 +21,6 @@ import (
"tailscale.com/util/syspolicy"
)
var getMachineCertificateSubjectOnce struct {
sync.Once
v string // Subject of machine certificate to search for
}
// getMachineCertificateSubject returns the exact name of a Subject that needs
// to be present in an identity's certificate chain to sign a RegisterRequest,
// formatted as per pkix.Name.String(). The Subject may be that of the identity
@@ -37,11 +31,8 @@ var getMachineCertificateSubjectOnce struct {
//
// Example: "CN=Tailscale Inc Test Root CA,OU=Tailscale Inc Test Certificate Authority,O=Tailscale Inc,ST=ON,C=CA"
func getMachineCertificateSubject() string {
getMachineCertificateSubjectOnce.Do(func() {
getMachineCertificateSubjectOnce.v, _ = syspolicy.GetString(syspolicy.MachineCertificateSubject, "")
})
return getMachineCertificateSubjectOnce.v
machineCertSubject, _ := syspolicy.GetString(syspolicy.MachineCertificateSubject, "")
return machineCertSubject
}
var (

View File

@@ -38,6 +38,7 @@ import (
"time"
"tailscale.com/control/controlbase"
"tailscale.com/control/controlhttp/controlhttpcommon"
"tailscale.com/envknob"
"tailscale.com/health"
"tailscale.com/net/dnscache"
@@ -95,6 +96,9 @@ func (a *Dialer) httpsFallbackDelay() time.Duration {
var _ = envknob.RegisterBool("TS_USE_CONTROL_DIAL_PLAN") // to record at init time whether it's in use
func (a *Dialer) dial(ctx context.Context) (*ClientConn, error) {
a.logPort80Failure.Store(true)
// If we don't have a dial plan, just fall back to dialing the single
// host we know about.
useDialPlan := envknob.BoolDefaultTrue("TS_USE_CONTROL_DIAL_PLAN")
@@ -277,7 +281,9 @@ func (d *Dialer) forceNoise443() bool {
// This heuristic works around networks where port 80 is MITMed and
// appears to work for a bit post-Upgrade but then gets closed,
// such as seen in https://github.com/tailscale/tailscale/issues/13597.
d.logf("controlhttp: forcing port 443 dial due to recent noise dial")
if d.logPort80Failure.CompareAndSwap(true, false) {
d.logf("controlhttp: forcing port 443 dial due to recent noise dial")
}
return true
}
@@ -571,9 +577,9 @@ func (a *Dialer) tryURLUpgrade(ctx context.Context, u *url.URL, optAddr netip.Ad
Method: "POST",
URL: u,
Header: http.Header{
"Upgrade": []string{upgradeHeaderValue},
"Connection": []string{"upgrade"},
handshakeHeaderName: []string{base64.StdEncoding.EncodeToString(init)},
"Upgrade": []string{controlhttpcommon.UpgradeHeaderValue},
"Connection": []string{"upgrade"},
controlhttpcommon.HandshakeHeaderName: []string{base64.StdEncoding.EncodeToString(init)},
},
}
req = req.WithContext(ctx)
@@ -597,7 +603,7 @@ func (a *Dialer) tryURLUpgrade(ctx context.Context, u *url.URL, optAddr netip.Ad
return nil, fmt.Errorf("httptrace didn't provide a connection")
}
if next := resp.Header.Get("Upgrade"); next != upgradeHeaderValue {
if next := resp.Header.Get("Upgrade"); next != controlhttpcommon.UpgradeHeaderValue {
resp.Body.Close()
return nil, fmt.Errorf("server switched to unexpected protocol %q", next)
}

View File

@@ -12,6 +12,7 @@ import (
"github.com/coder/websocket"
"tailscale.com/control/controlbase"
"tailscale.com/control/controlhttp/controlhttpcommon"
"tailscale.com/net/wsconn"
)
@@ -42,11 +43,11 @@ func (d *Dialer) Dial(ctx context.Context) (*ClientConn, error) {
// Can't set HTTP headers on the websocket request, so we have to to send
// the handshake via an HTTP header.
RawQuery: url.Values{
handshakeHeaderName: []string{base64.StdEncoding.EncodeToString(init)},
controlhttpcommon.HandshakeHeaderName: []string{base64.StdEncoding.EncodeToString(init)},
}.Encode(),
}
wsConn, _, err := websocket.Dial(ctx, wsURL.String(), &websocket.DialOptions{
Subprotocols: []string{upgradeHeaderValue},
Subprotocols: []string{controlhttpcommon.UpgradeHeaderValue},
})
if err != nil {
return nil, err

View File

@@ -6,6 +6,7 @@ package controlhttp
import (
"net/http"
"net/url"
"sync/atomic"
"time"
"tailscale.com/health"
@@ -18,15 +19,6 @@ import (
)
const (
// upgradeHeader is the value of the Upgrade HTTP header used to
// indicate the Tailscale control protocol.
upgradeHeaderValue = "tailscale-control-protocol"
// handshakeHeaderName is the HTTP request header that can
// optionally contain base64-encoded initial handshake
// payload, to save an RTT.
handshakeHeaderName = "X-Tailscale-Handshake"
// serverUpgradePath is where the server-side HTTP handler to
// to do the protocol switch is located.
serverUpgradePath = "/ts2021"
@@ -85,6 +77,8 @@ type Dialer struct {
// dropped.
Logf logger.Logf
// NetMon is the [netmon.Monitor] to use for this Dialer. It must be
// non-nil.
NetMon *netmon.Monitor
// HealthTracker, if non-nil, is the health tracker to use.
@@ -97,6 +91,11 @@ type Dialer struct {
proxyFunc func(*http.Request) (*url.URL, error) // or nil
// logPort80Failure is whether we should log about port 80 interceptions
// and forcing a port 443 dial. We do this only once per "dial" method
// which can result in many concurrent racing dialHost calls.
logPort80Failure atomic.Bool
// For tests only
drainFinished chan struct{}
omitCertErrorLogging bool

View File

@@ -0,0 +1,15 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// Package controlhttpcommon contains common constants for used
// by the controlhttp client and controlhttpserver packages.
package controlhttpcommon
// UpgradeHeader is the value of the Upgrade HTTP header used to
// indicate the Tailscale control protocol.
const UpgradeHeaderValue = "tailscale-control-protocol"
// handshakeHeaderName is the HTTP request header that can
// optionally contain base64-encoded initial handshake
// payload, to save an RTT.
const HandshakeHeaderName = "X-Tailscale-Handshake"

View File

@@ -1,217 +0,0 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build !ios
package controlhttp
import (
"context"
"encoding/base64"
"errors"
"fmt"
"io"
"net"
"net/http"
"strings"
"time"
"github.com/coder/websocket"
"tailscale.com/control/controlbase"
"tailscale.com/net/netutil"
"tailscale.com/net/wsconn"
"tailscale.com/types/key"
)
// AcceptHTTP upgrades the HTTP request given by w and r into a Tailscale
// control protocol base transport connection.
//
// AcceptHTTP always writes an HTTP response to w. The caller must not attempt
// their own response after calling AcceptHTTP.
//
// earlyWrite optionally specifies a func to write to the noise connection
// (encrypted). It receives the negotiated version and a writer to write to, if
// desired.
func AcceptHTTP(ctx context.Context, w http.ResponseWriter, r *http.Request, private key.MachinePrivate, earlyWrite func(protocolVersion int, w io.Writer) error) (*controlbase.Conn, error) {
return acceptHTTP(ctx, w, r, private, earlyWrite)
}
func acceptHTTP(ctx context.Context, w http.ResponseWriter, r *http.Request, private key.MachinePrivate, earlyWrite func(protocolVersion int, w io.Writer) error) (_ *controlbase.Conn, retErr error) {
next := strings.ToLower(r.Header.Get("Upgrade"))
if next == "" {
http.Error(w, "missing next protocol", http.StatusBadRequest)
return nil, errors.New("no next protocol in HTTP request")
}
if next == "websocket" {
return acceptWebsocket(ctx, w, r, private)
}
if next != upgradeHeaderValue {
http.Error(w, "unknown next protocol", http.StatusBadRequest)
return nil, fmt.Errorf("client requested unhandled next protocol %q", next)
}
initB64 := r.Header.Get(handshakeHeaderName)
if initB64 == "" {
http.Error(w, "missing Tailscale handshake header", http.StatusBadRequest)
return nil, errors.New("no tailscale handshake header in HTTP request")
}
init, err := base64.StdEncoding.DecodeString(initB64)
if err != nil {
http.Error(w, "invalid tailscale handshake header", http.StatusBadRequest)
return nil, fmt.Errorf("decoding base64 handshake header: %v", err)
}
hijacker, ok := w.(http.Hijacker)
if !ok {
http.Error(w, "make request over HTTP/1", http.StatusBadRequest)
return nil, errors.New("can't hijack client connection")
}
w.Header().Set("Upgrade", upgradeHeaderValue)
w.Header().Set("Connection", "upgrade")
w.WriteHeader(http.StatusSwitchingProtocols)
conn, brw, err := hijacker.Hijack()
if err != nil {
return nil, fmt.Errorf("hijacking client connection: %w", err)
}
defer func() {
if retErr != nil {
conn.Close()
}
}()
if err := brw.Flush(); err != nil {
return nil, fmt.Errorf("flushing hijacked HTTP buffer: %w", err)
}
conn = netutil.NewDrainBufConn(conn, brw.Reader)
cwc := newWriteCorkingConn(conn)
nc, err := controlbase.Server(ctx, cwc, private, init)
if err != nil {
return nil, fmt.Errorf("noise handshake failed: %w", err)
}
if earlyWrite != nil {
if deadline, ok := ctx.Deadline(); ok {
if err := conn.SetDeadline(deadline); err != nil {
return nil, fmt.Errorf("setting conn deadline: %w", err)
}
defer conn.SetDeadline(time.Time{})
}
if err := earlyWrite(nc.ProtocolVersion(), nc); err != nil {
return nil, err
}
}
if err := cwc.uncork(); err != nil {
return nil, err
}
return nc, nil
}
// acceptWebsocket upgrades a WebSocket connection (from a client that cannot
// speak HTTP) to a Tailscale control protocol base transport connection.
func acceptWebsocket(ctx context.Context, w http.ResponseWriter, r *http.Request, private key.MachinePrivate) (*controlbase.Conn, error) {
c, err := websocket.Accept(w, r, &websocket.AcceptOptions{
Subprotocols: []string{upgradeHeaderValue},
OriginPatterns: []string{"*"},
// Disable compression because we transmit Noise messages that are not
// compressible.
// Additionally, Safari has a broken implementation of compression
// (see https://github.com/nhooyr/websocket/issues/218) that makes
// enabling it actively harmful.
CompressionMode: websocket.CompressionDisabled,
})
if err != nil {
return nil, fmt.Errorf("Could not accept WebSocket connection %v", err)
}
if c.Subprotocol() != upgradeHeaderValue {
c.Close(websocket.StatusPolicyViolation, "client must speak the control subprotocol")
return nil, fmt.Errorf("Unexpected subprotocol %q", c.Subprotocol())
}
if err := r.ParseForm(); err != nil {
c.Close(websocket.StatusPolicyViolation, "Could not parse parameters")
return nil, fmt.Errorf("parse query parameters: %v", err)
}
initB64 := r.Form.Get(handshakeHeaderName)
if initB64 == "" {
c.Close(websocket.StatusPolicyViolation, "missing Tailscale handshake parameter")
return nil, errors.New("no tailscale handshake parameter in HTTP request")
}
init, err := base64.StdEncoding.DecodeString(initB64)
if err != nil {
c.Close(websocket.StatusPolicyViolation, "invalid tailscale handshake parameter")
return nil, fmt.Errorf("decoding base64 handshake parameter: %v", err)
}
conn := wsconn.NetConn(ctx, c, websocket.MessageBinary, r.RemoteAddr)
nc, err := controlbase.Server(ctx, conn, private, init)
if err != nil {
conn.Close()
return nil, fmt.Errorf("noise handshake failed: %w", err)
}
return nc, nil
}
// corkConn is a net.Conn wrapper that initially buffers all writes until uncork
// is called. If the conn is corked and a Read occurs, the Read will flush any
// buffered (corked) write.
//
// Until uncorked, Read/Write/uncork may be not called concurrently.
//
// Deadlines still work, but a corked write ignores deadlines until a Read or
// uncork goes to do that Write.
//
// Use newWriteCorkingConn to create one.
type corkConn struct {
net.Conn
corked bool
buf []byte // corked data
}
func newWriteCorkingConn(c net.Conn) *corkConn {
return &corkConn{Conn: c, corked: true}
}
func (c *corkConn) Write(b []byte) (int, error) {
if c.corked {
c.buf = append(c.buf, b...)
return len(b), nil
}
return c.Conn.Write(b)
}
func (c *corkConn) Read(b []byte) (int, error) {
if c.corked {
if err := c.flush(); err != nil {
return 0, err
}
}
return c.Conn.Read(b)
}
// uncork flushes any buffered data and uncorks the connection so future Writes
// don't buffer. It may not be called concurrently with reads or writes and
// may only be called once.
func (c *corkConn) uncork() error {
if !c.corked {
panic("usage error; uncork called twice") // worth panicking to catch misuse
}
err := c.flush()
c.corked = false
return err
}
func (c *corkConn) flush() error {
if len(c.buf) == 0 {
return nil
}
_, err := c.Conn.Write(c.buf)
c.buf = nil
return err
}

View File

@@ -6,6 +6,8 @@
package controlknobs
import (
"fmt"
"reflect"
"sync/atomic"
"tailscale.com/syncs"
@@ -103,6 +105,11 @@ type Knobs struct {
// DisableCaptivePortalDetection is whether the node should not perform captive portal detection
// automatically when the network state changes.
DisableCaptivePortalDetection atomic.Bool
// DisableSkipStatusQueue is whether the node should disable skipping
// of queued netmap.NetworkMap between the controlclient and LocalBackend.
// See tailscale/tailscale#14768.
DisableSkipStatusQueue atomic.Bool
}
// UpdateFromNodeAttributes updates k (if non-nil) based on the provided self
@@ -132,6 +139,7 @@ func (k *Knobs) UpdateFromNodeAttributes(capMap tailcfg.NodeCapMap) {
disableLocalDNSOverrideViaNRPT = has(tailcfg.NodeAttrDisableLocalDNSOverrideViaNRPT)
disableCryptorouting = has(tailcfg.NodeAttrDisableMagicSockCryptoRouting)
disableCaptivePortalDetection = has(tailcfg.NodeAttrDisableCaptivePortalDetection)
disableSkipStatusQueue = has(tailcfg.NodeAttrDisableSkipStatusQueue)
)
if has(tailcfg.NodeAttrOneCGNATEnable) {
@@ -159,6 +167,7 @@ func (k *Knobs) UpdateFromNodeAttributes(capMap tailcfg.NodeCapMap) {
k.DisableLocalDNSOverrideViaNRPT.Store(disableLocalDNSOverrideViaNRPT)
k.DisableCryptorouting.Store(disableCryptorouting)
k.DisableCaptivePortalDetection.Store(disableCaptivePortalDetection)
k.DisableSkipStatusQueue.Store(disableSkipStatusQueue)
}
// AsDebugJSON returns k as something that can be marshalled with json.Marshal
@@ -167,25 +176,19 @@ func (k *Knobs) AsDebugJSON() map[string]any {
if k == nil {
return nil
}
return map[string]any{
"DisableUPnP": k.DisableUPnP.Load(),
"KeepFullWGConfig": k.KeepFullWGConfig.Load(),
"RandomizeClientPort": k.RandomizeClientPort.Load(),
"OneCGNAT": k.OneCGNAT.Load(),
"ForceBackgroundSTUN": k.ForceBackgroundSTUN.Load(),
"DisableDeltaUpdates": k.DisableDeltaUpdates.Load(),
"PeerMTUEnable": k.PeerMTUEnable.Load(),
"DisableDNSForwarderTCPRetries": k.DisableDNSForwarderTCPRetries.Load(),
"SilentDisco": k.SilentDisco.Load(),
"LinuxForceIPTables": k.LinuxForceIPTables.Load(),
"LinuxForceNfTables": k.LinuxForceNfTables.Load(),
"SeamlessKeyRenewal": k.SeamlessKeyRenewal.Load(),
"ProbeUDPLifetime": k.ProbeUDPLifetime.Load(),
"AppCStoreRoutes": k.AppCStoreRoutes.Load(),
"UserDialUseRoutes": k.UserDialUseRoutes.Load(),
"DisableSplitDNSWhenNoCustomResolvers": k.DisableSplitDNSWhenNoCustomResolvers.Load(),
"DisableLocalDNSOverrideViaNRPT": k.DisableLocalDNSOverrideViaNRPT.Load(),
"DisableCryptorouting": k.DisableCryptorouting.Load(),
"DisableCaptivePortalDetection": k.DisableCaptivePortalDetection.Load(),
ret := map[string]any{}
rt := reflect.TypeFor[Knobs]()
rv := reflect.ValueOf(k).Elem() // of *k
for i := 0; i < rt.NumField(); i++ {
name := rt.Field(i).Name
switch v := rv.Field(i).Addr().Interface().(type) {
case *atomic.Bool:
ret[name] = v.Load()
case *syncs.AtomicValue[opt.Bool]:
ret[name] = v.Load()
default:
panic(fmt.Sprintf("unknown field type %T for %v", v, name))
}
}
return ret
}

20
vendor/tailscale.com/derp/derp.go generated vendored
View File

@@ -18,6 +18,7 @@ import (
"errors"
"fmt"
"io"
"net"
"time"
)
@@ -79,8 +80,7 @@ const (
// 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).
// record of B
framePeerGone = frameType(0x08) // 32B pub key of peer that's gone + 1 byte reason
// framePeerPresent is like framePeerGone, but for other members of the DERP
@@ -131,8 +131,8 @@ const (
type PeerGoneReasonType byte
const (
PeerGoneReasonDisconnected = PeerGoneReasonType(0x00) // peer disconnected from this server
PeerGoneReasonNotHere = PeerGoneReasonType(0x01) // server doesn't know about this peer, unexpected
PeerGoneReasonDisconnected = PeerGoneReasonType(0x00) // is only sent when a peer disconnects from this server
PeerGoneReasonNotHere = PeerGoneReasonType(0x01) // server doesn't know about this peer
PeerGoneReasonMeshConnBroke = PeerGoneReasonType(0xf0) // invented by Client.RunWatchConnectionLoop on disconnect; not sent on the wire
)
@@ -147,6 +147,7 @@ const (
PeerPresentIsRegular = 1 << 0
PeerPresentIsMeshPeer = 1 << 1
PeerPresentIsProber = 1 << 2
PeerPresentNotIdeal = 1 << 3 // client said derp server is not its Region.Nodes[0] ideal node
)
var bin = binary.BigEndian
@@ -254,3 +255,14 @@ func writeFrame(bw *bufio.Writer, t frameType, b []byte) error {
}
return bw.Flush()
}
// Conn is the subset of the underlying net.Conn the DERP Server needs.
// It is a defined type so that non-net connections can be used.
type Conn interface {
io.WriteCloser
LocalAddr() net.Addr
// The *Deadline methods follow the semantics of net.Conn.
SetDeadline(time.Time) error
SetReadDeadline(time.Time) error
SetWriteDeadline(time.Time) error
}

View File

@@ -23,9 +23,9 @@ import (
"math"
"math/big"
"math/rand/v2"
"net"
"net/http"
"net/netip"
"os"
"os/exec"
"runtime"
"strconv"
@@ -36,6 +36,7 @@ import (
"go4.org/mem"
"golang.org/x/sync/errgroup"
"tailscale.com/client/local"
"tailscale.com/client/tailscale"
"tailscale.com/disco"
"tailscale.com/envknob"
@@ -46,6 +47,7 @@ import (
"tailscale.com/tstime/rate"
"tailscale.com/types/key"
"tailscale.com/types/logger"
"tailscale.com/util/ctxkey"
"tailscale.com/util/mak"
"tailscale.com/util/set"
"tailscale.com/util/slicesx"
@@ -56,6 +58,16 @@ import (
// verbosely log whenever DERP drops a packet.
var verboseDropKeys = map[key.NodePublic]bool{}
// IdealNodeHeader is the HTTP request header sent on DERP HTTP client requests
// to indicate that they're connecting to their ideal (Region.Nodes[0]) node.
// The HTTP header value is the name of the node they wish they were connected
// to. This is an optional header.
const IdealNodeHeader = "Ideal-Node"
// IdealNodeContextKey is the context key used to pass the IdealNodeHeader value
// from the HTTP handler to the DERP server's Accept method.
var IdealNodeContextKey = ctxkey.New[string]("ideal-node", "")
func init() {
keys := envknob.String("TS_DEBUG_VERBOSE_DROPS")
if keys == "" {
@@ -72,10 +84,19 @@ func init() {
}
const (
perClientSendQueueDepth = 32 // packets buffered for sending
writeTimeout = 2 * time.Second
defaultPerClientSendQueueDepth = 32 // default packets buffered for sending
DefaultTCPWiteTimeout = 2 * time.Second
privilegedWriteTimeout = 30 * time.Second // for clients with the mesh key
)
func getPerClientSendQueueDepth() int {
if v, ok := envknob.LookupInt("TS_DEBUG_DERP_PER_CLIENT_SEND_QUEUE_DEPTH"); ok {
return v
}
return defaultPerClientSendQueueDepth
}
// dupPolicy is a temporary (2021-08-30) mechanism to change the policy
// of how duplicate connection for the same key are handled.
type dupPolicy int8
@@ -91,6 +112,14 @@ const (
disableFighters
)
// packetKind is the kind of packet being sent through DERP
type packetKind string
const (
packetKindDisco packetKind = "disco"
packetKindOther packetKind = "other"
)
type align64 [0]atomic.Int64 // for side effect of its 64-bit alignment
// Server is a DERP server.
@@ -108,44 +137,40 @@ type Server struct {
metaCert []byte // the encoded x509 cert to send after LetsEncrypt cert+intermediate
dupPolicy dupPolicy
debug bool
localClient local.Client
// Counters:
packetsSent, bytesSent expvar.Int
packetsRecv, bytesRecv expvar.Int
packetsRecvByKind metrics.LabelMap
packetsRecvDisco *expvar.Int
packetsRecvOther *expvar.Int
_ align64
packetsDropped expvar.Int
packetsDroppedReason metrics.LabelMap
packetsDroppedReasonCounters []*expvar.Int // indexed by dropReason
packetsDroppedType metrics.LabelMap
packetsDroppedTypeDisco *expvar.Int
packetsDroppedTypeOther *expvar.Int
_ 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
gotPing expvar.Int // number of ping frames from client
sentPong expvar.Int // number of pong frames enqueued to client
accepts expvar.Int
curClients expvar.Int
curHomeClients expvar.Int // ones with preferred
dupClientKeys expvar.Int // current number of public keys we have 2+ connections for
dupClientConns expvar.Int // current number of connections sharing a public key
dupClientConnTotal expvar.Int // total number of accepted connections when a dup key existed
unknownFrames expvar.Int
homeMovesIn expvar.Int // established clients announce home server moves in
homeMovesOut expvar.Int // established clients announce home server moves out
multiForwarderCreated expvar.Int
multiForwarderDeleted expvar.Int
removePktForwardOther expvar.Int
avgQueueDuration *uint64 // In milliseconds; accessed atomically
tcpRtt metrics.LabelMap // histogram
meshUpdateBatchSize *metrics.Histogram
meshUpdateLoopCount *metrics.Histogram
bufferedWriteFrames *metrics.Histogram // how many sendLoop frames (or groups of related frames) get written per flush
packetsSent, bytesSent expvar.Int
packetsRecv, bytesRecv expvar.Int
packetsRecvByKind metrics.LabelMap
packetsRecvDisco *expvar.Int
packetsRecvOther *expvar.Int
_ 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
gotPing expvar.Int // number of ping frames from client
sentPong expvar.Int // number of pong frames enqueued to client
accepts expvar.Int
curClients expvar.Int
curClientsNotIdeal expvar.Int
curHomeClients expvar.Int // ones with preferred
dupClientKeys expvar.Int // current number of public keys we have 2+ connections for
dupClientConns expvar.Int // current number of connections sharing a public key
dupClientConnTotal expvar.Int // total number of accepted connections when a dup key existed
unknownFrames expvar.Int
homeMovesIn expvar.Int // established clients announce home server moves in
homeMovesOut expvar.Int // established clients announce home server moves out
multiForwarderCreated expvar.Int
multiForwarderDeleted expvar.Int
removePktForwardOther expvar.Int
sclientWriteTimeouts expvar.Int
avgQueueDuration *uint64 // In milliseconds; accessed atomically
tcpRtt metrics.LabelMap // histogram
meshUpdateBatchSize *metrics.Histogram
meshUpdateLoopCount *metrics.Histogram
bufferedWriteFrames *metrics.Histogram // how many sendLoop frames (or groups of related frames) get written per flush
// verifyClientsLocalTailscaled only accepts client connections to the DERP
// server if the clientKey is a known peer in the network, as specified by a
@@ -175,6 +200,11 @@ type Server struct {
// maps from netip.AddrPort to a client's public key
keyOfAddr map[netip.AddrPort]key.NodePublic
// Sets the client send queue depth for the server.
perClientSendQueueDepth int
tcpWriteTimeout time.Duration
clock tstime.Clock
}
@@ -314,16 +344,16 @@ type PacketForwarder interface {
String() string
}
// Conn is the subset of the underlying net.Conn the DERP Server needs.
// It is a defined type so that non-net connections can be used.
type Conn interface {
io.WriteCloser
LocalAddr() net.Addr
// The *Deadline methods follow the semantics of net.Conn.
SetDeadline(time.Time) error
SetReadDeadline(time.Time) error
SetWriteDeadline(time.Time) error
}
var packetsDropped = metrics.NewMultiLabelMap[dropReasonKindLabels](
"derp_packets_dropped",
"counter",
"DERP packets dropped by reason and by kind")
var bytesDropped = metrics.NewMultiLabelMap[dropReasonKindLabels](
"derp_bytes_dropped",
"counter",
"DERP bytes dropped by reason and by kind",
)
// NewServer returns a new DERP server. It doesn't listen on its own.
// Connections are given to it via Server.Accept.
@@ -332,59 +362,100 @@ func NewServer(privateKey key.NodePrivate, logf logger.Logf) *Server {
runtime.ReadMemStats(&ms)
s := &Server{
debug: envknob.Bool("DERP_DEBUG_LOGS"),
privateKey: privateKey,
publicKey: privateKey.Public(),
logf: logf,
limitedLogf: logger.RateLimitedFn(logf, 30*time.Second, 5, 100),
packetsRecvByKind: metrics.LabelMap{Label: "kind"},
packetsDroppedReason: metrics.LabelMap{Label: "reason"},
packetsDroppedType: metrics.LabelMap{Label: "type"},
clients: map[key.NodePublic]*clientSet{},
clientsMesh: map[key.NodePublic]PacketForwarder{},
netConns: map[Conn]chan struct{}{},
memSys0: ms.Sys,
watchers: set.Set[*sclient]{},
peerGoneWatchers: map[key.NodePublic]set.HandleSet[func(key.NodePublic)]{},
avgQueueDuration: new(uint64),
tcpRtt: metrics.LabelMap{Label: "le"},
meshUpdateBatchSize: metrics.NewHistogram([]float64{0, 1, 2, 5, 10, 20, 50, 100, 200, 500, 1000}),
meshUpdateLoopCount: metrics.NewHistogram([]float64{0, 1, 2, 5, 10, 20, 50, 100}),
bufferedWriteFrames: metrics.NewHistogram([]float64{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 15, 20, 25, 50, 100}),
keyOfAddr: map[netip.AddrPort]key.NodePublic{},
clock: tstime.StdClock{},
debug: envknob.Bool("DERP_DEBUG_LOGS"),
privateKey: privateKey,
publicKey: privateKey.Public(),
logf: logf,
limitedLogf: logger.RateLimitedFn(logf, 30*time.Second, 5, 100),
packetsRecvByKind: metrics.LabelMap{Label: "kind"},
clients: map[key.NodePublic]*clientSet{},
clientsMesh: map[key.NodePublic]PacketForwarder{},
netConns: map[Conn]chan struct{}{},
memSys0: ms.Sys,
watchers: set.Set[*sclient]{},
peerGoneWatchers: map[key.NodePublic]set.HandleSet[func(key.NodePublic)]{},
avgQueueDuration: new(uint64),
tcpRtt: metrics.LabelMap{Label: "le"},
meshUpdateBatchSize: metrics.NewHistogram([]float64{0, 1, 2, 5, 10, 20, 50, 100, 200, 500, 1000}),
meshUpdateLoopCount: metrics.NewHistogram([]float64{0, 1, 2, 5, 10, 20, 50, 100}),
bufferedWriteFrames: metrics.NewHistogram([]float64{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 15, 20, 25, 50, 100}),
keyOfAddr: map[netip.AddrPort]key.NodePublic{},
clock: tstime.StdClock{},
tcpWriteTimeout: DefaultTCPWiteTimeout,
}
s.initMetacert()
s.packetsRecvDisco = s.packetsRecvByKind.Get("disco")
s.packetsRecvOther = s.packetsRecvByKind.Get("other")
s.packetsRecvDisco = s.packetsRecvByKind.Get(string(packetKindDisco))
s.packetsRecvOther = s.packetsRecvByKind.Get(string(packetKindOther))
s.packetsDroppedReasonCounters = s.genPacketsDroppedReasonCounters()
genDroppedCounters()
s.packetsDroppedTypeDisco = s.packetsDroppedType.Get("disco")
s.packetsDroppedTypeOther = s.packetsDroppedType.Get("other")
s.perClientSendQueueDepth = getPerClientSendQueueDepth()
return s
}
func (s *Server) genPacketsDroppedReasonCounters() []*expvar.Int {
getMetric := s.packetsDroppedReason.Get
ret := []*expvar.Int{
dropReasonUnknownDest: getMetric("unknown_dest"),
dropReasonUnknownDestOnFwd: getMetric("unknown_dest_on_fwd"),
dropReasonGoneDisconnected: getMetric("gone_disconnected"),
dropReasonQueueHead: getMetric("queue_head"),
dropReasonQueueTail: getMetric("queue_tail"),
dropReasonWriteError: getMetric("write_error"),
dropReasonDupClient: getMetric("dup_client"),
func genDroppedCounters() {
initMetrics := func(reason dropReason) {
packetsDropped.Add(dropReasonKindLabels{
Kind: string(packetKindDisco),
Reason: string(reason),
}, 0)
packetsDropped.Add(dropReasonKindLabels{
Kind: string(packetKindOther),
Reason: string(reason),
}, 0)
bytesDropped.Add(dropReasonKindLabels{
Kind: string(packetKindDisco),
Reason: string(reason),
}, 0)
bytesDropped.Add(dropReasonKindLabels{
Kind: string(packetKindOther),
Reason: string(reason),
}, 0)
}
if len(ret) != int(numDropReasons) {
panic("dropReason metrics out of sync")
}
for i := range numDropReasons {
if ret[i] == nil {
panic("dropReason metrics out of sync")
getMetrics := func(reason dropReason) []expvar.Var {
return []expvar.Var{
packetsDropped.Get(dropReasonKindLabels{
Kind: string(packetKindDisco),
Reason: string(reason),
}),
packetsDropped.Get(dropReasonKindLabels{
Kind: string(packetKindOther),
Reason: string(reason),
}),
bytesDropped.Get(dropReasonKindLabels{
Kind: string(packetKindDisco),
Reason: string(reason),
}),
bytesDropped.Get(dropReasonKindLabels{
Kind: string(packetKindOther),
Reason: string(reason),
}),
}
}
dropReasons := []dropReason{
dropReasonUnknownDest,
dropReasonUnknownDestOnFwd,
dropReasonGoneDisconnected,
dropReasonQueueHead,
dropReasonQueueTail,
dropReasonWriteError,
dropReasonDupClient,
}
for _, dr := range dropReasons {
initMetrics(dr)
m := getMetrics(dr)
if len(m) != 4 {
panic("dropReason metrics out of sync")
}
for _, v := range m {
if v == nil {
panic("dropReason metrics out of sync")
}
}
}
return ret
}
// SetMesh sets the pre-shared key that regional DERP servers used to mesh
@@ -415,6 +486,23 @@ func (s *Server) SetVerifyClientURLFailOpen(v bool) {
s.verifyClientsURLFailOpen = v
}
// SetTailscaledSocketPath sets the unix socket path to use to talk to
// tailscaled if client verification is enabled.
//
// If unset or set to the empty string, the default path for the operating
// system is used.
func (s *Server) SetTailscaledSocketPath(path string) {
s.localClient.Socket = path
s.localClient.UseSocketOnly = path != ""
}
// SetTCPWriteTimeout sets the timeout for writing to connected clients.
// This timeout does not apply to mesh connections.
// Defaults to 2 seconds.
func (s *Server) SetTCPWriteTimeout(d time.Duration) {
s.tcpWriteTimeout = d
}
// HasMeshKey reports whether the server is configured with a mesh key.
func (s *Server) HasMeshKey() bool { return s.meshKey != "" }
@@ -600,6 +688,9 @@ func (s *Server) registerClient(c *sclient) {
}
s.keyOfAddr[c.remoteIPPort] = c.key
s.curClients.Add(1)
if c.isNotIdealConn {
s.curClientsNotIdeal.Add(1)
}
s.broadcastPeerStateChangeLocked(c.key, c.remoteIPPort, c.presentFlags(), true)
}
@@ -690,6 +781,9 @@ func (s *Server) unregisterClient(c *sclient) {
if c.preferred {
s.curHomeClients.Add(-1)
}
if c.isNotIdealConn {
s.curClientsNotIdeal.Add(-1)
}
}
// addPeerGoneFromRegionWatcher adds a function to be called when peer is gone
@@ -806,8 +900,8 @@ func (s *Server) accept(ctx context.Context, nc Conn, brw *bufio.ReadWriter, rem
return fmt.Errorf("receive client key: %v", err)
}
clientAP, _ := netip.ParseAddrPort(remoteAddr)
if err := s.verifyClient(ctx, clientKey, clientInfo, clientAP.Addr()); err != nil {
remoteIPPort, _ := netip.ParseAddrPort(remoteAddr)
if err := s.verifyClient(ctx, clientKey, clientInfo, remoteIPPort.Addr()); err != nil {
return fmt.Errorf("client %v rejected: %v", clientKey, err)
}
@@ -817,8 +911,6 @@ func (s *Server) accept(ctx context.Context, nc Conn, brw *bufio.ReadWriter, rem
ctx, cancel := context.WithCancel(ctx)
defer cancel()
remoteIPPort, _ := netip.ParseAddrPort(remoteAddr)
c := &sclient{
connNum: connNum,
s: s,
@@ -830,11 +922,12 @@ func (s *Server) accept(ctx context.Context, nc Conn, brw *bufio.ReadWriter, rem
done: ctx.Done(),
remoteIPPort: remoteIPPort,
connectedAt: s.clock.Now(),
sendQueue: make(chan pkt, perClientSendQueueDepth),
discoSendQueue: make(chan pkt, perClientSendQueueDepth),
sendQueue: make(chan pkt, s.perClientSendQueueDepth),
discoSendQueue: make(chan pkt, s.perClientSendQueueDepth),
sendPongCh: make(chan [8]byte, 1),
peerGone: make(chan peerGoneMsg),
canMesh: s.isMeshPeer(clientInfo),
isNotIdealConn: IdealNodeContextKey.Value(ctx) != "",
peerGoneLim: rate.NewLimiter(rate.Every(time.Second), 3),
}
@@ -881,6 +974,9 @@ func (c *sclient) run(ctx context.Context) error {
if errors.Is(err, context.Canceled) {
c.debugLogf("sender canceled by reader exiting")
} else {
if errors.Is(err, os.ErrDeadlineExceeded) {
c.s.sclientWriteTimeouts.Add(1)
}
c.logf("sender failed: %v", err)
}
}
@@ -1116,31 +1212,37 @@ func (c *sclient) debugLogf(format string, v ...any) {
}
}
// dropReason is why we dropped a DERP frame.
type dropReason int
type dropReasonKindLabels struct {
Reason string // metric label corresponding to a given dropReason
Kind string // either `disco` or `other`
}
//go:generate go run tailscale.com/cmd/addlicense -file dropreason_string.go go run golang.org/x/tools/cmd/stringer -type=dropReason -trimprefix=dropReason
// dropReason is why we dropped a DERP frame.
type dropReason string
const (
dropReasonUnknownDest dropReason = iota // unknown destination pubkey
dropReasonUnknownDestOnFwd // unknown destination pubkey on a derp-forwarded packet
dropReasonGoneDisconnected // 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
dropReasonDupClient // the public key is connected 2+ times (active/active, fighting)
numDropReasons // unused; keep last
dropReasonUnknownDest dropReason = "unknown_dest" // unknown destination pubkey
dropReasonUnknownDestOnFwd dropReason = "unknown_dest_on_fwd" // unknown destination pubkey on a derp-forwarded packet
dropReasonGoneDisconnected dropReason = "gone_disconnected" // destination tailscaled disconnected before we could send
dropReasonQueueHead dropReason = "queue_head" // destination queue is full, dropped packet at queue head
dropReasonQueueTail dropReason = "queue_tail" // destination queue is full, dropped packet at queue tail
dropReasonWriteError dropReason = "write_error" // OS write() failed
dropReasonDupClient dropReason = "dup_client" // the public key is connected 2+ times (active/active, fighting)
)
func (s *Server) recordDrop(packetBytes []byte, srcKey, dstKey key.NodePublic, reason dropReason) {
s.packetsDropped.Add(1)
s.packetsDroppedReasonCounters[reason].Add(1)
labels := dropReasonKindLabels{
Reason: string(reason),
}
looksDisco := disco.LooksLikeDiscoWrapper(packetBytes)
if looksDisco {
s.packetsDroppedTypeDisco.Add(1)
labels.Kind = string(packetKindDisco)
} else {
s.packetsDroppedTypeOther.Add(1)
labels.Kind = string(packetKindOther)
}
packetsDropped.Add(labels, 1)
bytesDropped.Add(labels, int64(len(packetBytes)))
if verboseDropKeys[dstKey] {
// Preformat the log string prior to calling limitedLogf. The
// limiter acts based on the format string, and we want to
@@ -1229,8 +1331,6 @@ func (c *sclient) requestMeshUpdate() {
}
}
var localClient tailscale.LocalClient
// isMeshPeer reports whether the client is a trusted mesh peer
// node in the DERP region.
func (s *Server) isMeshPeer(info *clientInfo) bool {
@@ -1249,7 +1349,7 @@ func (s *Server) verifyClient(ctx context.Context, clientKey key.NodePublic, inf
// tailscaled-based verification:
if s.verifyClientsLocalTailscaled {
_, err := localClient.WhoIsNodeKey(ctx, clientKey)
_, err := s.localClient.WhoIsNodeKey(ctx, clientKey)
if err == tailscale.ErrPeerNotFound {
return fmt.Errorf("peer %v not authorized (not found in local tailscaled)", clientKey)
}
@@ -1505,6 +1605,7 @@ type sclient struct {
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
isNotIdealConn bool // client indicated it is not its ideal node in the region
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
@@ -1540,6 +1641,9 @@ func (c *sclient) presentFlags() PeerPresentFlags {
if c.canMesh {
f |= PeerPresentIsMeshPeer
}
if c.isNotIdealConn {
f |= PeerPresentNotIdeal
}
if f == 0 {
return PeerPresentIsRegular
}
@@ -1721,7 +1825,30 @@ func (c *sclient) sendLoop(ctx context.Context) error {
}
func (c *sclient) setWriteDeadline() {
c.nc.SetWriteDeadline(time.Now().Add(writeTimeout))
d := c.s.tcpWriteTimeout
if c.canMesh {
// Trusted peers get more tolerance.
//
// The "canMesh" is a bit of a misnomer; mesh peers typically run over a
// different interface for a per-region private VPC and are not
// throttled. But monitoring software elsewhere over the internet also
// use the private mesh key to subscribe to connect/disconnect events
// and might hit throttling and need more time to get the initial dump
// of connected peers.
d = privilegedWriteTimeout
}
if d == 0 {
// A zero value should disable the write deadline per
// --tcp-write-timeout docs. The flag should only be applicable for
// non-mesh connections, again per its docs. If mesh happened to use a
// zero value constant above it would be a bug, so we don't bother
// with a condition on c.canMesh.
return
}
// Ignore the error from setting the write deadline. In practice,
// setting the deadline will only fail if the connection is closed
// or closing, so the subsequent Write() will fail anyway.
_ = c.nc.SetWriteDeadline(time.Now().Add(d))
}
// sendKeepAlive sends a keep-alive frame, without flushing.
@@ -2033,6 +2160,7 @@ func (s *Server) ExpVar() expvar.Var {
m.Set("gauge_current_file_descriptors", expvar.Func(func() any { return metrics.CurrentFDs() }))
m.Set("gauge_current_connections", &s.curClients)
m.Set("gauge_current_home_connections", &s.curHomeClients)
m.Set("gauge_current_notideal_connections", &s.curClientsNotIdeal)
m.Set("gauge_clients_total", expvar.Func(func() any { return len(s.clientsMesh) }))
m.Set("gauge_clients_local", expvar.Func(func() any { return len(s.clients) }))
m.Set("gauge_clients_remote", expvar.Func(func() any { return len(s.clientsMesh) - len(s.clients) }))
@@ -2042,9 +2170,6 @@ func (s *Server) ExpVar() expvar.Var {
m.Set("accepts", &s.accepts)
m.Set("bytes_received", &s.bytesRecv)
m.Set("bytes_sent", &s.bytesSent)
m.Set("packets_dropped", &s.packetsDropped)
m.Set("counter_packets_dropped_reason", &s.packetsDroppedReason)
m.Set("counter_packets_dropped_type", &s.packetsDroppedType)
m.Set("counter_packets_received_kind", &s.packetsRecvByKind)
m.Set("packets_sent", &s.packetsSent)
m.Set("packets_received", &s.packetsRecv)
@@ -2060,6 +2185,7 @@ func (s *Server) ExpVar() expvar.Var {
m.Set("multiforwarder_created", &s.multiForwarderCreated)
m.Set("multiforwarder_deleted", &s.multiForwarderDeleted)
m.Set("packet_forwarder_delete_other_value", &s.removePktForwardOther)
m.Set("sclient_write_timeouts", &s.sclientWriteTimeouts)
m.Set("average_queue_duration_ms", expvar.Func(func() any {
return math.Float64frombits(atomic.LoadUint64(s.avgQueueDuration))
}))
@@ -2123,7 +2249,7 @@ func (s *Server) ConsistencyCheck() error {
func (s *Server) checkVerifyClientsLocalTailscaled() error {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
status, err := localClient.StatusWithoutPeers(ctx)
status, err := s.localClient.StatusWithoutPeers(ctx)
if err != nil {
return fmt.Errorf("localClient.Status: %w", err)
}

View File

@@ -313,6 +313,9 @@ func (c *Client) preferIPv6() bool {
var dialWebsocketFunc func(ctx context.Context, urlStr string) (net.Conn, error)
func useWebsockets() bool {
if !canWebsockets {
return false
}
if runtime.GOOS == "js" {
return true
}
@@ -383,7 +386,7 @@ func (c *Client) connect(ctx context.Context, caller string) (client *derp.Clien
var node *tailcfg.DERPNode // nil when using c.url to dial
var idealNodeInRegion bool
switch {
case useWebsockets():
case canWebsockets && useWebsockets():
var urlStr string
if c.url != nil {
urlStr = c.url.String()
@@ -498,7 +501,7 @@ func (c *Client) connect(ctx context.Context, caller string) (client *derp.Clien
req.Header.Set("Connection", "Upgrade")
if !idealNodeInRegion && reg != nil {
// This is purely informative for now (2024-07-06) for stats:
req.Header.Set("Ideal-Node", reg.Nodes[0].Name)
req.Header.Set(derp.IdealNodeHeader, reg.Nodes[0].Name)
// TODO(bradfitz,raggi): start a time.AfterFunc for 30m-1h or so to
// dialNode(reg.Nodes[0]) and see if we can even TCP connect to it. If
// so, TLS handshake it as well (which is mixed up in this massive
@@ -649,7 +652,11 @@ func (c *Client) tlsClient(nc net.Conn, node *tailcfg.DERPNode) *tls.Conn {
tlsConf.VerifyConnection = nil
}
if node.CertName != "" {
tlsdial.SetConfigExpectedCert(tlsConf, node.CertName)
if suf, ok := strings.CutPrefix(node.CertName, "sha256-raw:"); ok {
tlsdial.SetConfigExpectedCertHash(tlsConf, suf)
} else {
tlsdial.SetConfigExpectedCert(tlsConf, node.CertName)
}
}
}
return tls.Client(nc, tlsConf)
@@ -663,7 +670,7 @@ func (c *Client) tlsClient(nc net.Conn, node *tailcfg.DERPNode) *tls.Conn {
func (c *Client) DialRegionTLS(ctx context.Context, reg *tailcfg.DERPRegion) (tlsConn *tls.Conn, connClose io.Closer, node *tailcfg.DERPNode, err error) {
tcpConn, node, err := c.dialRegion(ctx, reg)
if err != nil {
return nil, nil, nil, err
return nil, nil, nil, fmt.Errorf("dialRegion(%d): %w", reg.RegionID, err)
}
done := make(chan bool) // unbuffered
defer close(done)
@@ -738,6 +745,17 @@ func (c *Client) dialNode(ctx context.Context, n *tailcfg.DERPNode) (net.Conn, e
nwait := 0
startDial := func(dstPrimary, proto string) {
dst := cmp.Or(dstPrimary, n.HostName)
// If dialing an IP address directly, check its address family
// and bail out before incrementing nwait.
if ip, err := netip.ParseAddr(dst); err == nil {
if proto == "tcp4" && ip.Is6() ||
proto == "tcp6" && ip.Is4() {
return
}
}
nwait++
go func() {
if proto == "tcp4" && c.preferIPv6() {
@@ -752,8 +770,10 @@ func (c *Client) dialNode(ctx context.Context, n *tailcfg.DERPNode) (net.Conn, e
// Start v4 dial
}
}
dst := cmp.Or(dstPrimary, n.HostName)
port := "443"
if !c.useHTTPS() {
port = "3340"
}
if n.DERPPort != 0 {
port = fmt.Sprint(n.DERPPort)
}

View File

@@ -21,6 +21,8 @@ const fastStartHeader = "Derp-Fast-Start"
// Handler returns an http.Handler to be mounted at /derp, serving s.
func Handler(s *derp.Server) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
// These are installed both here and in cmd/derper. The check here
// catches both cmd/derper run with DERP disabled (STUN only mode) as
// well as DERP being run in tests with derphttp.Handler directly,
@@ -66,7 +68,11 @@ func Handler(s *derp.Server) http.Handler {
pubKey.UntypedHexString())
}
s.Accept(r.Context(), netConn, conn, netConn.RemoteAddr().String())
if v := r.Header.Get(derp.IdealNodeHeader); v != "" {
ctx = derp.IdealNodeContextKey.WithValue(ctx, v)
}
s.Accept(ctx, netConn, conn, netConn.RemoteAddr().String())
})
}
@@ -92,6 +98,7 @@ func ServeNoContent(w http.ResponseWriter, r *http.Request) {
w.Header().Set(NoContentResponseHeader, "response "+challenge)
}
}
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate, no-transform, max-age=0")
w.WriteHeader(http.StatusNoContent)
}
@@ -99,7 +106,7 @@ func isChallengeChar(c rune) bool {
// Semi-randomly chosen as a limited set of valid characters
return ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z') ||
('0' <= c && c <= '9') ||
c == '.' || c == '-' || c == '_'
c == '.' || c == '-' || c == '_' || c == ':'
}
const (

View File

@@ -1,7 +1,7 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build linux || js
//go:build js || ((linux || darwin) && ts_debug_websockets)
package derphttp
@@ -14,6 +14,8 @@ import (
"tailscale.com/net/wsconn"
)
const canWebsockets = true
func init() {
dialWebsocketFunc = dialWebsocket
}

8
vendor/tailscale.com/derp/derphttp/websocket_stub.go generated vendored Normal file
View File

@@ -0,0 +1,8 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build !(js || ((linux || darwin) && ts_debug_websockets))
package derphttp
const canWebsockets = false

View File

@@ -1,33 +0,0 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// Code generated by "stringer -type=dropReason -trimprefix=dropReason"; DO NOT EDIT.
package derp
import "strconv"
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[dropReasonUnknownDest-0]
_ = x[dropReasonUnknownDestOnFwd-1]
_ = x[dropReasonGoneDisconnected-2]
_ = x[dropReasonQueueHead-3]
_ = x[dropReasonQueueTail-4]
_ = x[dropReasonWriteError-5]
_ = x[dropReasonDupClient-6]
_ = x[numDropReasons-7]
}
const _dropReason_name = "UnknownDestUnknownDestOnFwdGoneDisconnectedQueueHeadQueueTailWriteErrorDupClientnumDropReasons"
var _dropReason_index = [...]uint8{0, 11, 27, 43, 52, 61, 71, 80, 94}
func (i dropReason) String() string {
if i < 0 || i >= dropReason(len(_dropReason_index)-1) {
return "dropReason(" + strconv.FormatInt(int64(i), 10) + ")"
}
return _dropReason_name[_dropReason_index[i]:_dropReason_index[i+1]]
}

View File

@@ -14,7 +14,7 @@ import (
//go:generate go run tailscale.com/cmd/cloner -clonefunc=true -type=Share
// View returns a readonly view of Share.
// View returns a read-only view of Share.
func (p *Share) View() ShareView {
return ShareView{ж: p}
}
@@ -30,7 +30,7 @@ type ShareView struct {
ж *Share
}
// Valid reports whether underlying value is non-nil.
// Valid reports whether v's underlying value is non-nil.
func (v ShareView) Valid() bool { return v.ж != nil }
// AsStruct returns a clone of the underlying value which aliases no memory with

View File

@@ -411,12 +411,35 @@ func TKASkipSignatureCheck() bool { return Bool("TS_UNSAFE_SKIP_NKS_VERIFICATION
// Kubernetes Operator components.
func App() string {
a := os.Getenv("TS_INTERNAL_APP")
if a == kubetypes.AppConnector || a == kubetypes.AppEgressProxy || a == kubetypes.AppIngressProxy || a == kubetypes.AppIngressResource {
if a == kubetypes.AppConnector || a == kubetypes.AppEgressProxy || a == kubetypes.AppIngressProxy || a == kubetypes.AppIngressResource || a == kubetypes.AppProxyGroupEgress || a == kubetypes.AppProxyGroupIngress {
return a
}
return ""
}
// IsCertShareReadOnlyMode returns true if this replica should never attempt to
// issue or renew TLS credentials for any of the HTTPS endpoints that it is
// serving. It should only return certs found in its cert store. Currently,
// this is used by the Kubernetes Operator's HA Ingress via VIPServices, where
// multiple Ingress proxy instances serve the same HTTPS endpoint with a shared
// TLS credentials. The TLS credentials should only be issued by one of the
// replicas.
// For HTTPS Ingress the operator and containerboot ensure
// that read-only replicas will not be serving the HTTPS endpoints before there
// is a shared cert available.
func IsCertShareReadOnlyMode() bool {
m := String("TS_CERT_SHARE_MODE")
return m == "ro"
}
// IsCertShareReadWriteMode returns true if this instance is the replica
// responsible for issuing and renewing TLS certs in an HA setup with certs
// shared between multiple replicas.
func IsCertShareReadWriteMode() bool {
m := String("TS_CERT_SHARE_MODE")
return m == "rw"
}
// CrashOnUnexpected reports whether the Tailscale client should panic
// on unexpected conditions. If TS_DEBUG_CRASH_ON_UNEXPECTED is set, that's
// used. Otherwise the default value is true for unstable builds.

View File

@@ -0,0 +1,67 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// Package featureknob provides a facility to control whether features
// can run based on either an envknob or running OS / distro.
package featureknob
import (
"errors"
"runtime"
"tailscale.com/envknob"
"tailscale.com/hostinfo"
"tailscale.com/version"
"tailscale.com/version/distro"
)
// CanRunTailscaleSSH reports whether serving a Tailscale SSH server is
// supported for the current os/distro.
func CanRunTailscaleSSH() error {
switch runtime.GOOS {
case "linux":
if distro.Get() == distro.Synology && !envknob.UseWIPCode() {
return errors.New("The Tailscale SSH server does not run on Synology.")
}
if distro.Get() == distro.QNAP && !envknob.UseWIPCode() {
return errors.New("The Tailscale SSH server does not run on QNAP.")
}
// Setting SSH on Home Assistant causes trouble on startup
// (since the flag is not being passed to `tailscale up`).
// Although Tailscale SSH does work here,
// it's not terribly useful since it's running in a separate container.
if hostinfo.GetEnvType() == hostinfo.HomeAssistantAddOn {
return errors.New("The Tailscale SSH server does not run on HomeAssistant.")
}
// otherwise okay
case "darwin":
// okay only in tailscaled mode for now.
if version.IsSandboxedMacOS() {
return errors.New("The Tailscale SSH server does not run in sandboxed Tailscale GUI builds.")
}
case "freebsd", "openbsd":
default:
return errors.New("The Tailscale SSH server is not supported on " + runtime.GOOS)
}
if !envknob.CanSSHD() {
return errors.New("The Tailscale SSH server has been administratively disabled.")
}
return nil
}
// CanUseExitNode reports whether using an exit node is supported for the
// current os/distro.
func CanUseExitNode() error {
switch dist := distro.Get(); dist {
case distro.Synology, // see https://github.com/tailscale/tailscale/issues/1995
distro.QNAP:
return errors.New("Tailscale exit nodes cannot be used on " + string(dist))
}
if hostinfo.GetEnvType() == hostinfo.HomeAssistantAddOn {
return errors.New("Tailscale exit nodes cannot be used on HomeAssistant.")
}
return nil
}

View File

@@ -1,39 +0,0 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package envknob
import (
"errors"
"runtime"
"tailscale.com/version"
"tailscale.com/version/distro"
)
// CanRunTailscaleSSH reports whether serving a Tailscale SSH server is
// supported for the current os/distro.
func CanRunTailscaleSSH() error {
switch runtime.GOOS {
case "linux":
if distro.Get() == distro.Synology && !UseWIPCode() {
return errors.New("The Tailscale SSH server does not run on Synology.")
}
if distro.Get() == distro.QNAP && !UseWIPCode() {
return errors.New("The Tailscale SSH server does not run on QNAP.")
}
// otherwise okay
case "darwin":
// okay only in tailscaled mode for now.
if version.IsSandboxedMacOS() {
return errors.New("The Tailscale SSH server does not run in sandboxed Tailscale GUI builds.")
}
case "freebsd", "openbsd":
default:
return errors.New("The Tailscale SSH server is not supported on " + runtime.GOOS)
}
if !CanSSHD() {
return errors.New("The Tailscale SSH server has been administratively disabled.")
}
return nil
}

View File

@@ -13,21 +13,44 @@ import (
"sync"
"time"
_ "embed"
"tailscale.com/feature"
"tailscale.com/ipn/localapi"
"tailscale.com/net/packet"
"tailscale.com/util/set"
)
//go:embed ts-dissector.lua
var DissectorLua string
func init() {
feature.Register("capture")
localapi.Register("debug-capture", serveLocalAPIDebugCapture)
}
// Callback describes a function which is called to
// record packets when debugging packet-capture.
// Such callbacks must not take ownership of the
// provided data slice: it may only copy out of it
// within the lifetime of the function.
type Callback func(Path, time.Time, []byte, packet.CaptureMeta)
func serveLocalAPIDebugCapture(h *localapi.Handler, w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
if !h.PermitWrite {
http.Error(w, "debug access denied", http.StatusForbidden)
return
}
if r.Method != "POST" {
http.Error(w, "POST required", http.StatusMethodNotAllowed)
return
}
w.WriteHeader(http.StatusOK)
w.(http.Flusher).Flush()
b := h.LocalBackend()
s := b.GetOrSetCaptureSink(newSink)
unregister := s.RegisterOutput(w)
select {
case <-ctx.Done():
case <-s.WaitCh():
}
unregister()
b.ClearCaptureSink()
}
var bufferPool = sync.Pool{
New: func() any {
@@ -57,29 +80,8 @@ func writePktHeader(w *bytes.Buffer, when time.Time, length int) {
binary.Write(w, binary.LittleEndian, uint32(length)) // total length
}
// Path describes where in the data path the packet was captured.
type Path uint8
// Valid Path values.
const (
// FromLocal indicates the packet was logged as it traversed the FromLocal path:
// i.e.: A packet from the local system into the TUN.
FromLocal Path = 0
// FromPeer indicates the packet was logged upon reception from a remote peer.
FromPeer Path = 1
// SynthesizedToLocal indicates the packet was generated from within tailscaled,
// and is being routed to the local machine's network stack.
SynthesizedToLocal Path = 2
// SynthesizedToPeer indicates the packet was generated from within tailscaled,
// and is being routed to a remote Wireguard peer.
SynthesizedToPeer Path = 3
// PathDisco indicates the packet is information about a disco frame.
PathDisco Path = 254
)
// New creates a new capture sink.
func New() *Sink {
// newSink creates a new capture sink.
func newSink() packet.CaptureSink {
ctx, c := context.WithCancel(context.Background())
return &Sink{
ctx: ctx,
@@ -126,6 +128,10 @@ func (s *Sink) RegisterOutput(w io.Writer) (unregister func()) {
}
}
func (s *Sink) CaptureCallback() packet.CaptureCallback {
return s.LogPacket
}
// NumOutputs returns the number of outputs registered with the sink.
func (s *Sink) NumOutputs() int {
s.mu.Lock()
@@ -174,7 +180,7 @@ func customDataLen(meta packet.CaptureMeta) int {
// LogPacket is called to insert a packet into the capture.
//
// This function does not take ownership of the provided data slice.
func (s *Sink) LogPacket(path Path, when time.Time, data []byte, meta packet.CaptureMeta) {
func (s *Sink) LogPacket(path packet.CapturePath, when time.Time, data []byte, meta packet.CaptureMeta) {
select {
case <-s.ctx.Done():
return

View File

@@ -0,0 +1,7 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// The condregister package registers all conditional features guarded
// by build tags. It is one central package that callers can empty import
// to ensure all conditional features are registered.
package condregister

View File

@@ -0,0 +1,8 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build !ios && !ts_omit_capture
package condregister
import _ "tailscale.com/feature/capture"

View File

@@ -0,0 +1,8 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build linux && !ts_omit_tap
package condregister
import _ "tailscale.com/feature/tap"

View File

@@ -0,0 +1,8 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build !ts_omit_wakeonlan
package condregister
import _ "tailscale.com/feature/wakeonlan"

54
vendor/tailscale.com/feature/feature.go generated vendored Normal file
View File

@@ -0,0 +1,54 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// Package feature tracks which features are linked into the binary.
package feature
import "reflect"
var in = map[string]bool{}
// Register notes that the named feature is linked into the binary.
func Register(name string) {
if _, ok := in[name]; ok {
panic("duplicate feature registration for " + name)
}
in[name] = true
}
// Hook is a func that can only be set once.
//
// It is not safe for concurrent use.
type Hook[Func any] struct {
f Func
ok bool
}
// IsSet reports whether the hook has been set.
func (h *Hook[Func]) IsSet() bool {
return h.ok
}
// Set sets the hook function, panicking if it's already been set
// or f is the zero value.
//
// It's meant to be called in init.
func (h *Hook[Func]) Set(f Func) {
if h.ok {
panic("Set on already-set feature hook")
}
if reflect.ValueOf(f).IsZero() {
panic("Set with zero value")
}
h.f = f
h.ok = true
}
// Get returns the hook function, or panics if it hasn't been set.
// Use IsSet to check if it's been set.
func (h *Hook[Func]) Get() Func {
if !h.ok {
panic("Get on unset feature hook, without IsSet")
}
return h.f
}

View File

@@ -1,11 +1,11 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build !ts_omit_tap
package tstun
// Package tap registers Tailscale's experimental (demo) Linux TAP (Layer 2) support.
package tap
import (
"bytes"
"fmt"
"net"
"net/netip"
@@ -20,10 +20,15 @@ import (
"gvisor.dev/gvisor/pkg/tcpip/checksum"
"gvisor.dev/gvisor/pkg/tcpip/header"
"gvisor.dev/gvisor/pkg/tcpip/network/ipv4"
"gvisor.dev/gvisor/pkg/tcpip/network/ipv6"
"gvisor.dev/gvisor/pkg/tcpip/transport/udp"
"tailscale.com/net/netaddr"
"tailscale.com/net/packet"
"tailscale.com/net/tsaddr"
"tailscale.com/net/tstun"
"tailscale.com/syncs"
"tailscale.com/types/ipproto"
"tailscale.com/types/logger"
"tailscale.com/util/multierr"
)
@@ -33,15 +38,19 @@ import (
// For now just hard code it.
var ourMAC = net.HardwareAddr{0x30, 0x2D, 0x66, 0xEC, 0x7A, 0x93}
func init() { createTAP = createTAPLinux }
const tapDebug = tstun.TAPDebug
func createTAPLinux(tapName, bridgeName string) (tun.Device, error) {
func init() {
tstun.CreateTAP.Set(createTAPLinux)
}
func createTAPLinux(logf logger.Logf, tapName, bridgeName string) (tun.Device, error) {
fd, err := unix.Open("/dev/net/tun", unix.O_RDWR, 0)
if err != nil {
return nil, err
}
dev, err := openDevice(fd, tapName, bridgeName)
dev, err := openDevice(logf, fd, tapName, bridgeName)
if err != nil {
unix.Close(fd)
return nil, err
@@ -50,7 +59,7 @@ func createTAPLinux(tapName, bridgeName string) (tun.Device, error) {
return dev, nil
}
func openDevice(fd int, tapName, bridgeName string) (tun.Device, error) {
func openDevice(logf logger.Logf, fd int, tapName, bridgeName string) (tun.Device, error) {
ifr, err := unix.NewIfreq(tapName)
if err != nil {
return nil, err
@@ -71,7 +80,7 @@ func openDevice(fd int, tapName, bridgeName string) (tun.Device, error) {
}
}
return newTAPDevice(fd, tapName)
return newTAPDevice(logf, fd, tapName)
}
type etherType [2]byte
@@ -82,7 +91,10 @@ var (
etherTypeIPv6 = etherType{0x86, 0xDD}
)
const ipv4HeaderLen = 20
const (
ipv4HeaderLen = 20
ethernetFrameSize = 14 // 2 six byte MACs, 2 bytes ethertype
)
const (
consumePacket = true
@@ -91,7 +103,7 @@ const (
// handleTAPFrame handles receiving a raw TAP ethernet frame and reports whether
// it's been handled (that is, whether it should NOT be passed to wireguard).
func (t *Wrapper) handleTAPFrame(ethBuf []byte) bool {
func (t *tapDevice) handleTAPFrame(ethBuf []byte) bool {
if len(ethBuf) < ethernetFrameSize {
// Corrupt. Ignore.
@@ -154,7 +166,7 @@ func (t *Wrapper) handleTAPFrame(ethBuf []byte) bool {
// If the client's asking about their own IP, tell them it's
// their own MAC. TODO(bradfitz): remove String allocs.
if net.IP(req.ProtocolAddressTarget()).String() == theClientIP {
if net.IP(req.ProtocolAddressTarget()).String() == t.clientIPv4.Load() {
copy(res.HardwareAddressSender(), ethSrcMAC)
} else {
copy(res.HardwareAddressSender(), ourMAC[:])
@@ -164,8 +176,7 @@ func (t *Wrapper) handleTAPFrame(ethBuf []byte) bool {
copy(res.HardwareAddressTarget(), req.HardwareAddressSender())
copy(res.ProtocolAddressTarget(), req.ProtocolAddressSender())
// TODO(raggi): reduce allocs!
n, err := t.tdev.Write([][]byte{buf}, 0)
n, err := t.WriteEthernet(buf)
if tapDebug {
t.logf("tap: wrote ARP reply %v, %v", n, err)
}
@@ -175,14 +186,22 @@ func (t *Wrapper) handleTAPFrame(ethBuf []byte) bool {
}
}
// TODO(bradfitz): remove these hard-coded values and move from a /24 to a /10 CGNAT as the range.
const theClientIP = "100.70.145.3" // TODO: make dynamic from netmap
const routerIP = "100.70.145.1" // must be in same netmask (currently hack at /24) as theClientIP
var (
// routerIP is the IP address of the DHCP server.
routerIP = net.ParseIP(tsaddr.TailscaleServiceIPString)
// cgnatNetMask is the netmask of the 100.64.0.0/10 CGNAT range.
cgnatNetMask = net.IPMask(net.ParseIP("255.192.0.0").To4())
)
// parsedPacketPool holds a pool of Parsed structs for use in filtering.
// This is needed because escape analysis cannot see that parsed packets
// do not escape through {Pre,Post}Filter{In,Out}.
var parsedPacketPool = sync.Pool{New: func() any { return new(packet.Parsed) }}
// handleDHCPRequest handles receiving a raw TAP ethernet frame and reports whether
// it's been handled as a DHCP request. That is, it reports whether the frame should
// be ignored by the caller and not passed on.
func (t *Wrapper) handleDHCPRequest(ethBuf []byte) bool {
func (t *tapDevice) handleDHCPRequest(ethBuf []byte) bool {
const udpHeader = 8
if len(ethBuf) < ethernetFrameSize+ipv4HeaderLen+udpHeader {
if tapDebug {
@@ -207,7 +226,7 @@ func (t *Wrapper) handleDHCPRequest(ethBuf []byte) bool {
if p.IPProto != ipproto.UDP || p.Src.Port() != 68 || p.Dst.Port() != 67 {
// Not a DHCP request.
if tapDebug {
t.logf("tap: DHCP wrong meta")
t.logf("tap: DHCP wrong meta: %+v", p)
}
return passOnPacket
}
@@ -225,17 +244,22 @@ func (t *Wrapper) handleDHCPRequest(ethBuf []byte) bool {
}
switch dp.MessageType() {
case dhcpv4.MessageTypeDiscover:
ips := t.clientIPv4.Load()
if ips == "" {
t.logf("tap: DHCP no client IP")
return consumePacket
}
offer, err := dhcpv4.New(
dhcpv4.WithReply(dp),
dhcpv4.WithMessageType(dhcpv4.MessageTypeOffer),
dhcpv4.WithRouter(net.ParseIP(routerIP)), // the default route
dhcpv4.WithDNS(net.ParseIP("100.100.100.100")),
dhcpv4.WithServerIP(net.ParseIP("100.100.100.100")), // TODO: what is this?
dhcpv4.WithOption(dhcpv4.OptServerIdentifier(net.ParseIP("100.100.100.100"))),
dhcpv4.WithYourIP(net.ParseIP(theClientIP)),
dhcpv4.WithRouter(routerIP), // the default route
dhcpv4.WithDNS(routerIP),
dhcpv4.WithServerIP(routerIP), // TODO: what is this?
dhcpv4.WithOption(dhcpv4.OptServerIdentifier(routerIP)),
dhcpv4.WithYourIP(net.ParseIP(ips)),
dhcpv4.WithLeaseTime(3600), // hour works
//dhcpv4.WithHwAddr(ethSrcMAC),
dhcpv4.WithNetmask(net.IPMask(net.ParseIP("255.255.255.0").To4())), // TODO: wrong
dhcpv4.WithNetmask(cgnatNetMask),
//dhcpv4.WithTransactionID(dp.TransactionID),
)
if err != nil {
@@ -250,22 +274,26 @@ func (t *Wrapper) handleDHCPRequest(ethBuf []byte) bool {
netip.AddrPortFrom(netaddr.IPv4(255, 255, 255, 255), 68), // dst
)
// TODO(raggi): reduce allocs!
n, err := t.tdev.Write([][]byte{pkt}, 0)
n, err := t.WriteEthernet(pkt)
if tapDebug {
t.logf("tap: wrote DHCP OFFER %v, %v", n, err)
}
case dhcpv4.MessageTypeRequest:
ips := t.clientIPv4.Load()
if ips == "" {
t.logf("tap: DHCP no client IP")
return consumePacket
}
ack, err := dhcpv4.New(
dhcpv4.WithReply(dp),
dhcpv4.WithMessageType(dhcpv4.MessageTypeAck),
dhcpv4.WithDNS(net.ParseIP("100.100.100.100")),
dhcpv4.WithRouter(net.ParseIP(routerIP)), // the default route
dhcpv4.WithServerIP(net.ParseIP("100.100.100.100")), // TODO: what is this?
dhcpv4.WithOption(dhcpv4.OptServerIdentifier(net.ParseIP("100.100.100.100"))),
dhcpv4.WithYourIP(net.ParseIP(theClientIP)), // Hello world
dhcpv4.WithLeaseTime(3600), // hour works
dhcpv4.WithNetmask(net.IPMask(net.ParseIP("255.255.255.0").To4())),
dhcpv4.WithDNS(routerIP),
dhcpv4.WithRouter(routerIP), // the default route
dhcpv4.WithServerIP(routerIP), // TODO: what is this?
dhcpv4.WithOption(dhcpv4.OptServerIdentifier(routerIP)),
dhcpv4.WithYourIP(net.ParseIP(ips)), // Hello world
dhcpv4.WithLeaseTime(3600), // hour works
dhcpv4.WithNetmask(cgnatNetMask),
)
if err != nil {
t.logf("error building DHCP ack: %v", err)
@@ -278,8 +306,7 @@ func (t *Wrapper) handleDHCPRequest(ethBuf []byte) bool {
netip.AddrPortFrom(netaddr.IPv4(100, 100, 100, 100), 67), // src
netip.AddrPortFrom(netaddr.IPv4(255, 255, 255, 255), 68), // dst
)
// TODO(raggi): reduce allocs!
n, err := t.tdev.Write([][]byte{pkt}, 0)
n, err := t.WriteEthernet(pkt)
if tapDebug {
t.logf("tap: wrote DHCP ACK %v, %v", n, err)
}
@@ -291,6 +318,16 @@ func (t *Wrapper) handleDHCPRequest(ethBuf []byte) bool {
return consumePacket
}
func writeEthernetFrame(buf []byte, srcMAC, dstMAC net.HardwareAddr, proto tcpip.NetworkProtocolNumber) {
// Ethernet header
eth := header.Ethernet(buf)
eth.Encode(&header.EthernetFields{
SrcAddr: tcpip.LinkAddress(srcMAC),
DstAddr: tcpip.LinkAddress(dstMAC),
Type: proto,
})
}
func packLayer2UDP(payload []byte, srcMAC, dstMAC net.HardwareAddr, src, dst netip.AddrPort) []byte {
buf := make([]byte, header.EthernetMinimumSize+header.UDPMinimumSize+header.IPv4MinimumSize+len(payload))
payloadStart := len(buf) - len(payload)
@@ -300,12 +337,7 @@ func packLayer2UDP(payload []byte, srcMAC, dstMAC net.HardwareAddr, src, dst net
dstB := dst.Addr().As4()
dstIP := tcpip.AddrFromSlice(dstB[:])
// Ethernet header
eth := header.Ethernet(buf)
eth.Encode(&header.EthernetFields{
SrcAddr: tcpip.LinkAddress(srcMAC),
DstAddr: tcpip.LinkAddress(dstMAC),
Type: ipv4.ProtocolNumber,
})
writeEthernetFrame(buf, srcMAC, dstMAC, ipv4.ProtocolNumber)
// IP header
ipbuf := buf[header.EthernetMinimumSize:]
ip := header.IPv4(ipbuf)
@@ -342,17 +374,18 @@ func run(prog string, args ...string) error {
return nil
}
func (t *Wrapper) destMAC() [6]byte {
func (t *tapDevice) destMAC() [6]byte {
return t.destMACAtomic.Load()
}
func newTAPDevice(fd int, tapName string) (tun.Device, error) {
func newTAPDevice(logf logger.Logf, fd int, tapName string) (tun.Device, error) {
err := unix.SetNonblock(fd, true)
if err != nil {
return nil, err
}
file := os.NewFile(uintptr(fd), "/dev/tap")
d := &tapDevice{
logf: logf,
file: file,
events: make(chan tun.Event),
name: tapName,
@@ -360,20 +393,22 @@ func newTAPDevice(fd int, tapName string) (tun.Device, error) {
return d, nil
}
var (
_ setWrapperer = &tapDevice{}
)
type tapDevice struct {
file *os.File
events chan tun.Event
name string
wrapper *Wrapper
closeOnce sync.Once
file *os.File
logf func(format string, args ...any)
events chan tun.Event
name string
closeOnce sync.Once
clientIPv4 syncs.AtomicValue[string]
destMACAtomic syncs.AtomicValue[[6]byte]
}
func (t *tapDevice) setWrapper(wrapper *Wrapper) {
t.wrapper = wrapper
var _ tstun.SetIPer = (*tapDevice)(nil)
func (t *tapDevice) SetIP(ipV4, ipV6TODO netip.Addr) error {
t.clientIPv4.Store(ipV4.String())
return nil
}
func (t *tapDevice) File() *os.File {
@@ -384,36 +419,63 @@ func (t *tapDevice) Name() (string, error) {
return t.name, nil
}
// Read reads an IP packet from the TAP device. It strips the ethernet frame header.
func (t *tapDevice) Read(buffs [][]byte, sizes []int, offset int) (int, error) {
n, err := t.ReadEthernet(buffs, sizes, offset)
if err != nil || n == 0 {
return n, err
}
// Strip the ethernet frame header.
copy(buffs[0][offset:], buffs[0][offset+ethernetFrameSize:offset+sizes[0]])
sizes[0] -= ethernetFrameSize
return 1, nil
}
// ReadEthernet reads a raw ethernet frame from the TAP device.
func (t *tapDevice) ReadEthernet(buffs [][]byte, sizes []int, offset int) (int, error) {
n, err := t.file.Read(buffs[0][offset:])
if err != nil {
return 0, err
}
if t.handleTAPFrame(buffs[0][offset : offset+n]) {
return 0, nil
}
sizes[0] = n
return 1, nil
}
// WriteEthernet writes a raw ethernet frame to the TAP device.
func (t *tapDevice) WriteEthernet(buf []byte) (int, error) {
return t.file.Write(buf)
}
// ethBufPool holds a pool of bytes.Buffers for use in [tapDevice.Write].
var ethBufPool = syncs.Pool[*bytes.Buffer]{New: func() *bytes.Buffer { return new(bytes.Buffer) }}
// Write writes a raw IP packet to the TAP device. It adds the ethernet frame header.
func (t *tapDevice) Write(buffs [][]byte, offset int) (int, error) {
errs := make([]error, 0)
wrote := 0
m := t.destMAC()
dstMac := net.HardwareAddr(m[:])
buf := ethBufPool.Get()
defer ethBufPool.Put(buf)
for _, buff := range buffs {
if offset < ethernetFrameSize {
errs = append(errs, fmt.Errorf("[unexpected] weird offset %d for TAP write", offset))
return 0, multierr.New(errs...)
buf.Reset()
buf.Grow(header.EthernetMinimumSize + len(buff) - offset)
var ebuf [14]byte
switch buff[offset] >> 4 {
case 4:
writeEthernetFrame(ebuf[:], ourMAC, dstMac, ipv4.ProtocolNumber)
case 6:
writeEthernetFrame(ebuf[:], ourMAC, dstMac, ipv6.ProtocolNumber)
default:
continue
}
eth := buff[offset-ethernetFrameSize:]
dst := t.wrapper.destMAC()
copy(eth[:6], dst[:])
copy(eth[6:12], ourMAC[:])
et := etherTypeIPv4
if buff[offset]>>4 == 6 {
et = etherTypeIPv6
}
eth[12], eth[13] = et[0], et[1]
if tapDebug {
t.wrapper.logf("tap: tapWrite off=%v % x", offset, buff)
}
_, err := t.file.Write(buff[offset-ethernetFrameSize:])
buf.Write(ebuf[:])
buf.Write(buff[offset:])
_, err := t.WriteEthernet(buf.Bytes())
if err != nil {
errs = append(errs, err)
} else {
@@ -428,8 +490,7 @@ func (t *tapDevice) MTU() (int, error) {
if err != nil {
return 0, err
}
err = unix.IoctlIfreq(int(t.file.Fd()), unix.SIOCGIFMTU, ifr)
if err != nil {
if err := unix.IoctlIfreq(int(t.file.Fd()), unix.SIOCGIFMTU, ifr); err != nil {
return 0, err
}
return int(ifr.Uint32()), nil

243
vendor/tailscale.com/feature/wakeonlan/wakeonlan.go generated vendored Normal file
View File

@@ -0,0 +1,243 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// Package wakeonlan registers the Wake-on-LAN feature.
package wakeonlan
import (
"encoding/json"
"log"
"net"
"net/http"
"runtime"
"sort"
"strings"
"unicode"
"github.com/kortschak/wol"
"tailscale.com/envknob"
"tailscale.com/feature"
"tailscale.com/hostinfo"
"tailscale.com/ipn/ipnlocal"
"tailscale.com/tailcfg"
"tailscale.com/util/clientmetric"
)
func init() {
feature.Register("wakeonlan")
ipnlocal.RegisterC2N("POST /wol", handleC2NWoL)
ipnlocal.RegisterPeerAPIHandler("/v0/wol", handlePeerAPIWakeOnLAN)
hostinfo.RegisterHostinfoNewHook(func(h *tailcfg.Hostinfo) {
h.WoLMACs = getWoLMACs()
})
}
func handleC2NWoL(b *ipnlocal.LocalBackend, w http.ResponseWriter, r *http.Request) {
r.ParseForm()
var macs []net.HardwareAddr
for _, macStr := range r.Form["mac"] {
mac, err := net.ParseMAC(macStr)
if err != nil {
http.Error(w, "bad 'mac' param", http.StatusBadRequest)
return
}
macs = append(macs, mac)
}
var res struct {
SentTo []string
Errors []string
}
st := b.NetMon().InterfaceState()
if st == nil {
res.Errors = append(res.Errors, "no interface state")
writeJSON(w, &res)
return
}
var password []byte // TODO(bradfitz): support? does anything use WoL passwords?
for _, mac := range macs {
for ifName, ips := range st.InterfaceIPs {
for _, ip := range ips {
if ip.Addr().IsLoopback() || ip.Addr().Is6() {
continue
}
local := &net.UDPAddr{
IP: ip.Addr().AsSlice(),
Port: 0,
}
remote := &net.UDPAddr{
IP: net.IPv4bcast,
Port: 0,
}
if err := wol.Wake(mac, password, local, remote); err != nil {
res.Errors = append(res.Errors, err.Error())
} else {
res.SentTo = append(res.SentTo, ifName)
}
break // one per interface is enough
}
}
}
sort.Strings(res.SentTo)
writeJSON(w, &res)
}
func writeJSON(w http.ResponseWriter, v any) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(v)
}
func canWakeOnLAN(h ipnlocal.PeerAPIHandler) bool {
if h.Peer().UnsignedPeerAPIOnly() {
return false
}
return h.IsSelfUntagged() || h.PeerCaps().HasCapability(tailcfg.PeerCapabilityWakeOnLAN)
}
var metricWakeOnLANCalls = clientmetric.NewCounter("peerapi_wol")
func handlePeerAPIWakeOnLAN(h ipnlocal.PeerAPIHandler, w http.ResponseWriter, r *http.Request) {
metricWakeOnLANCalls.Add(1)
if !canWakeOnLAN(h) {
http.Error(w, "no WoL access", http.StatusForbidden)
return
}
if r.Method != "POST" {
http.Error(w, "bad method", http.StatusMethodNotAllowed)
return
}
macStr := r.FormValue("mac")
if macStr == "" {
http.Error(w, "missing 'mac' param", http.StatusBadRequest)
return
}
mac, err := net.ParseMAC(macStr)
if err != nil {
http.Error(w, "bad 'mac' param", http.StatusBadRequest)
return
}
var password []byte // TODO(bradfitz): support? does anything use WoL passwords?
st := h.LocalBackend().NetMon().InterfaceState()
if st == nil {
http.Error(w, "failed to get interfaces state", http.StatusInternalServerError)
return
}
var res struct {
SentTo []string
Errors []string
}
for ifName, ips := range st.InterfaceIPs {
for _, ip := range ips {
if ip.Addr().IsLoopback() || ip.Addr().Is6() {
continue
}
local := &net.UDPAddr{
IP: ip.Addr().AsSlice(),
Port: 0,
}
remote := &net.UDPAddr{
IP: net.IPv4bcast,
Port: 0,
}
if err := wol.Wake(mac, password, local, remote); err != nil {
res.Errors = append(res.Errors, err.Error())
} else {
res.SentTo = append(res.SentTo, ifName)
}
break // one per interface is enough
}
}
sort.Strings(res.SentTo)
writeJSON(w, res)
}
// TODO(bradfitz): this is all too simplistic and static. It needs to run
// continuously in response to netmon events (USB ethernet adapters might get
// plugged in) and look for the media type/status/etc. Right now on macOS it
// still detects a half dozen "up" en0, en1, en2, en3 etc interfaces that don't
// have any media. We should only report the one that's actually connected.
// But it works for now (2023-10-05) for fleshing out the rest.
var wakeMAC = envknob.RegisterString("TS_WAKE_MAC") // mac address, "false" or "auto". for https://github.com/tailscale/tailscale/issues/306
// getWoLMACs returns up to 10 MAC address of the local machine to send
// wake-on-LAN packets to in order to wake it up. The returned MACs are in
// lowercase hex colon-separated form ("xx:xx:xx:xx:xx:xx").
//
// If TS_WAKE_MAC=auto, it tries to automatically find the MACs based on the OS
// type and interface properties. (TODO(bradfitz): incomplete) If TS_WAKE_MAC is
// set to a MAC address, that sole MAC address is returned.
func getWoLMACs() (macs []string) {
switch runtime.GOOS {
case "ios", "android":
return nil
}
if s := wakeMAC(); s != "" {
switch s {
case "auto":
ifs, _ := net.Interfaces()
for _, iface := range ifs {
if iface.Flags&net.FlagLoopback != 0 {
continue
}
if iface.Flags&net.FlagBroadcast == 0 ||
iface.Flags&net.FlagRunning == 0 ||
iface.Flags&net.FlagUp == 0 {
continue
}
if keepMAC(iface.Name, iface.HardwareAddr) {
macs = append(macs, iface.HardwareAddr.String())
}
if len(macs) == 10 {
break
}
}
return macs
case "false", "off": // fast path before ParseMAC error
return nil
}
mac, err := net.ParseMAC(s)
if err != nil {
log.Printf("invalid MAC %q", s)
return nil
}
return []string{mac.String()}
}
return nil
}
var ignoreWakeOUI = map[[3]byte]bool{
{0x00, 0x15, 0x5d}: true, // Hyper-V
{0x00, 0x50, 0x56}: true, // VMware
{0x00, 0x1c, 0x14}: true, // VMware
{0x00, 0x05, 0x69}: true, // VMware
{0x00, 0x0c, 0x29}: true, // VMware
{0x00, 0x1c, 0x42}: true, // Parallels
{0x08, 0x00, 0x27}: true, // VirtualBox
{0x00, 0x21, 0xf6}: true, // VirtualBox
{0x00, 0x14, 0x4f}: true, // VirtualBox
{0x00, 0x0f, 0x4b}: true, // VirtualBox
{0x52, 0x54, 0x00}: true, // VirtualBox/Vagrant
}
func keepMAC(ifName string, mac []byte) bool {
if len(mac) != 6 {
return false
}
base := strings.TrimRightFunc(ifName, unicode.IsNumber)
switch runtime.GOOS {
case "darwin":
switch base {
case "llw", "awdl", "utun", "bridge", "lo", "gif", "stf", "anpi", "ap":
return false
}
}
if mac[0] == 0x02 && mac[1] == 0x42 {
// Docker container.
return false
}
oui := [3]byte{mac[0], mac[1], mac[2]}
if ignoreWakeOUI[oui] {
return false
}
return true
}

View File

@@ -1 +1 @@
tailscale.go1.23
tailscale.go1.24

View File

@@ -1 +1 @@
bf15628b759344c6fc7763795a405ba65b8be5d7
4fdaeeb8fe43bcdb4e8cc736433b9cd9c0ddd221

View File

@@ -22,6 +22,7 @@ import (
"tailscale.com/envknob"
"tailscale.com/metrics"
"tailscale.com/tailcfg"
"tailscale.com/tstime"
"tailscale.com/types/opt"
"tailscale.com/util/cibuild"
"tailscale.com/util/mak"
@@ -73,6 +74,8 @@ type Tracker struct {
// mu should not be held during init.
initOnce sync.Once
testClock tstime.Clock // nil means use time.Now / tstime.StdClock{}
// mu guards everything that follows.
mu sync.Mutex
@@ -80,13 +83,13 @@ type Tracker struct {
warnableVal map[*Warnable]*warningState
// pendingVisibleTimers contains timers for Warnables that are unhealthy, but are
// not visible to the user yet, because they haven't been unhealthy for TimeToVisible
pendingVisibleTimers map[*Warnable]*time.Timer
pendingVisibleTimers map[*Warnable]tstime.TimerController
// sysErr maps subsystems to their current error (or nil if the subsystem is healthy)
// Deprecated: using Warnables should be preferred
sysErr map[Subsystem]error
watchers set.HandleSet[func(*Warnable, *UnhealthyState)] // opt func to run if error state changes
timer *time.Timer
timer tstime.TimerController
latestVersion *tailcfg.ClientVersion // or nil
checkForUpdates bool
@@ -115,6 +118,20 @@ type Tracker struct {
metricHealthMessage *metrics.MultiLabelMap[metricHealthMessageLabel]
}
func (t *Tracker) now() time.Time {
if t.testClock != nil {
return t.testClock.Now()
}
return time.Now()
}
func (t *Tracker) clock() tstime.Clock {
if t.testClock != nil {
return t.testClock
}
return tstime.StdClock{}
}
// Subsystem is the name of a subsystem whose health can be monitored.
//
// Deprecated: Registering a Warnable using Register() and updating its health state
@@ -128,9 +145,6 @@ const (
// SysDNS is the name of the net/dns subsystem.
SysDNS = Subsystem("dns")
// SysDNSOS is the name of the net/dns OSConfigurator subsystem.
SysDNSOS = Subsystem("dns-os")
// SysDNSManager is the name of the net/dns manager subsystem.
SysDNSManager = Subsystem("dns-manager")
@@ -141,7 +155,7 @@ const (
var subsystemsWarnables = map[Subsystem]*Warnable{}
func init() {
for _, s := range []Subsystem{SysRouter, SysDNS, SysDNSOS, SysDNSManager, SysTKA} {
for _, s := range []Subsystem{SysRouter, SysDNS, SysDNSManager, SysTKA} {
w := Register(&Warnable{
Code: WarnableCode(s),
Severity: SeverityMedium,
@@ -217,9 +231,11 @@ type Warnable struct {
// TODO(angott): turn this into a SeverityFunc, which allows the Warnable to change its severity based on
// the Args of the unhappy state, just like we do in the Text function.
Severity Severity
// DependsOn is a set of Warnables that this Warnable depends, on and need to be healthy
// before this Warnable can also be healthy again. The GUI can use this information to ignore
// DependsOn is a set of Warnables that this Warnable depends on and need to be healthy
// before this Warnable is relevant. The GUI can use this information to ignore
// this Warnable if one of its dependencies is unhealthy.
// That is, if any of these Warnables are unhealthy, then this Warnable is not relevant
// and should be considered healthy to bother the user about.
DependsOn []*Warnable
// MapDebugFlag is a MapRequest.DebugFlag that is sent to control when this Warnable is unhealthy
@@ -312,11 +328,11 @@ func (ws *warningState) Equal(other *warningState) bool {
// IsVisible returns whether the Warnable should be visible to the user, based on the TimeToVisible
// field of the Warnable and the BrokenSince time when the Warnable became unhealthy.
func (w *Warnable) IsVisible(ws *warningState) bool {
func (w *Warnable) IsVisible(ws *warningState, clockNow func() time.Time) bool {
if ws == nil || w.TimeToVisible == 0 {
return true
}
return time.Since(ws.BrokenSince) >= w.TimeToVisible
return clockNow().Sub(ws.BrokenSince) >= w.TimeToVisible
}
// SetMetricsRegistry sets up the metrics for the Tracker. It takes
@@ -334,7 +350,7 @@ func (t *Tracker) SetMetricsRegistry(reg *usermetric.Registry) {
)
t.metricHealthMessage.Set(metricHealthMessageLabel{
Type: "warning",
Type: MetricLabelWarning,
}, expvar.Func(func() any {
if t.nil() {
return 0
@@ -366,7 +382,7 @@ func (t *Tracker) setUnhealthyLocked(w *Warnable, args Args) {
// If we already have a warningState for this Warnable with an earlier BrokenSince time, keep that
// BrokenSince time.
brokenSince := time.Now()
brokenSince := t.now()
if existingWS := t.warnableVal[w]; existingWS != nil {
brokenSince = existingWS.BrokenSince
}
@@ -385,15 +401,15 @@ func (t *Tracker) setUnhealthyLocked(w *Warnable, args Args) {
// If the Warnable has been unhealthy for more than its TimeToVisible, the callback should be
// executed immediately. Otherwise, the callback should be enqueued to run once the Warnable
// becomes visible.
if w.IsVisible(ws) {
if w.IsVisible(ws, t.now) {
go cb(w, w.unhealthyState(ws))
continue
}
// The time remaining until the Warnable will be visible to the user is the TimeToVisible
// minus the time that has already passed since the Warnable became unhealthy.
visibleIn := w.TimeToVisible - time.Since(brokenSince)
mak.Set(&t.pendingVisibleTimers, w, time.AfterFunc(visibleIn, func() {
visibleIn := w.TimeToVisible - t.now().Sub(brokenSince)
var tc tstime.TimerController = t.clock().AfterFunc(visibleIn, func() {
t.mu.Lock()
defer t.mu.Unlock()
// Check if the Warnable is still unhealthy, as it could have become healthy between the time
@@ -402,7 +418,8 @@ func (t *Tracker) setUnhealthyLocked(w *Warnable, args Args) {
go cb(w, w.unhealthyState(ws))
delete(t.pendingVisibleTimers, w)
}
}))
})
mak.Set(&t.pendingVisibleTimers, w, tc)
}
}
}
@@ -477,7 +494,7 @@ func (t *Tracker) RegisterWatcher(cb func(w *Warnable, r *UnhealthyState)) (unre
}
handle := t.watchers.Add(cb)
if t.timer == nil {
t.timer = time.AfterFunc(time.Minute, t.timerSelfCheck)
t.timer = t.clock().AfterFunc(time.Minute, t.timerSelfCheck)
}
return func() {
t.mu.Lock()
@@ -510,22 +527,12 @@ func (t *Tracker) SetDNSHealth(err error) { t.setErr(SysDNS, err) }
// Deprecated: Warnables should be preferred over Subsystem errors.
func (t *Tracker) DNSHealth() error { return t.get(SysDNS) }
// SetDNSOSHealth sets the state of the net/dns.OSConfigurator
//
// Deprecated: Warnables should be preferred over Subsystem errors.
func (t *Tracker) SetDNSOSHealth(err error) { t.setErr(SysDNSOS, err) }
// SetDNSManagerHealth sets the state of the Linux net/dns manager's
// discovery of the /etc/resolv.conf situation.
//
// Deprecated: Warnables should be preferred over Subsystem errors.
func (t *Tracker) SetDNSManagerHealth(err error) { t.setErr(SysDNSManager, err) }
// DNSOSHealth returns the net/dns.OSConfigurator error state.
//
// Deprecated: Warnables should be preferred over Subsystem errors.
func (t *Tracker) DNSOSHealth() error { return t.get(SysDNSOS) }
// SetTKAHealth sets the health of the tailnet key authority.
//
// Deprecated: Warnables should be preferred over Subsystem errors.
@@ -651,10 +658,10 @@ func (t *Tracker) GotStreamedMapResponse() {
}
t.mu.Lock()
defer t.mu.Unlock()
t.lastStreamedMapResponse = time.Now()
t.lastStreamedMapResponse = t.now()
if !t.inMapPoll {
t.inMapPoll = true
t.inMapPollSince = time.Now()
t.inMapPollSince = t.now()
}
t.selfCheckLocked()
}
@@ -671,7 +678,7 @@ func (t *Tracker) SetOutOfPollNetMap() {
return
}
t.inMapPoll = false
t.lastMapPollEndedAt = time.Now()
t.lastMapPollEndedAt = t.now()
t.selfCheckLocked()
}
@@ -713,7 +720,7 @@ func (t *Tracker) NoteMapRequestHeard(mr *tailcfg.MapRequest) {
// against SetMagicSockDERPHome and
// SetDERPRegionConnectedState
t.lastMapRequestHeard = time.Now()
t.lastMapRequestHeard = t.now()
t.selfCheckLocked()
}
@@ -751,7 +758,7 @@ func (t *Tracker) NoteDERPRegionReceivedFrame(region int) {
}
t.mu.Lock()
defer t.mu.Unlock()
mak.Set(&t.derpRegionLastFrame, region, time.Now())
mak.Set(&t.derpRegionLastFrame, region, t.now())
t.selfCheckLocked()
}
@@ -810,9 +817,9 @@ func (t *Tracker) SetIPNState(state string, wantRunning bool) {
// The first time we see wantRunning=true and it used to be false, it means the user requested
// the backend to start. We store this timestamp and use it to silence some warnings that are
// expected during startup.
t.ipnWantRunningLastTrue = time.Now()
t.ipnWantRunningLastTrue = t.now()
t.setUnhealthyLocked(warmingUpWarnable, nil)
time.AfterFunc(warmingUpWarnableDuration, func() {
t.clock().AfterFunc(warmingUpWarnableDuration, func() {
t.mu.Lock()
t.updateWarmingUpWarnableLocked()
t.mu.Unlock()
@@ -949,10 +956,13 @@ func (t *Tracker) Strings() []string {
func (t *Tracker) stringsLocked() []string {
result := []string{}
for w, ws := range t.warnableVal {
if !w.IsVisible(ws) {
if !w.IsVisible(ws, t.now) {
// Do not append invisible warnings.
continue
}
if t.isEffectivelyHealthyLocked(w) {
continue
}
if ws.Args == nil {
result = append(result, w.Text(Args{}))
} else {
@@ -1018,7 +1028,7 @@ func (t *Tracker) updateBuiltinWarnablesLocked() {
t.setHealthyLocked(localLogWarnable)
}
now := time.Now()
now := t.now()
// How long we assume we'll have heard a DERP frame or a MapResponse
// KeepAlive by.
@@ -1028,8 +1038,10 @@ func (t *Tracker) updateBuiltinWarnablesLocked() {
recentlyOn := now.Sub(t.ipnWantRunningLastTrue) < 5*time.Second
homeDERP := t.derpHomeRegion
if recentlyOn {
if recentlyOn || !t.inMapPoll {
// If user just turned Tailscale on, don't warn for a bit.
// Also, if we're not in a map poll, that means we don't yet
// have a DERPMap or aren't in a state where we even want
t.setHealthyLocked(noDERPHomeWarnable)
t.setHealthyLocked(noDERPConnectionWarnable)
t.setHealthyLocked(derpTimeoutWarnable)
@@ -1051,11 +1063,15 @@ func (t *Tracker) updateBuiltinWarnablesLocked() {
ArgDuration: d.Round(time.Second).String(),
})
}
} else {
} else if homeDERP != 0 {
t.setUnhealthyLocked(noDERPConnectionWarnable, Args{
ArgDERPRegionID: fmt.Sprint(homeDERP),
ArgDERPRegionName: t.derpRegionNameLocked(homeDERP),
})
} else {
// No DERP home yet determined yet. There's probably some
// other problem or things are just starting up.
t.setHealthyLocked(noDERPConnectionWarnable)
}
if !t.ipnWantRunning {
@@ -1174,7 +1190,7 @@ func (t *Tracker) updateBuiltinWarnablesLocked() {
// updateWarmingUpWarnableLocked ensures the warmingUpWarnable is healthy if wantRunning has been set to true
// for more than warmingUpWarnableDuration.
func (t *Tracker) updateWarmingUpWarnableLocked() {
if !t.ipnWantRunningLastTrue.IsZero() && time.Now().After(t.ipnWantRunningLastTrue.Add(warmingUpWarnableDuration)) {
if !t.ipnWantRunningLastTrue.IsZero() && t.now().After(t.ipnWantRunningLastTrue.Add(warmingUpWarnableDuration)) {
t.setHealthyLocked(warmingUpWarnable)
}
}
@@ -1286,12 +1302,14 @@ func (t *Tracker) LastNoiseDialWasRecent() bool {
t.mu.Lock()
defer t.mu.Unlock()
now := time.Now()
now := t.now()
dur := now.Sub(t.lastNoiseDial)
t.lastNoiseDial = now
return dur < 2*time.Minute
}
const MetricLabelWarning = "warning"
type metricHealthMessageLabel struct {
// TODO: break down by warnable.severity as well?
Type string

27
vendor/tailscale.com/health/state.go generated vendored
View File

@@ -86,10 +86,15 @@ func (t *Tracker) CurrentState() *State {
wm := map[WarnableCode]UnhealthyState{}
for w, ws := range t.warnableVal {
if !w.IsVisible(ws) {
if !w.IsVisible(ws, t.now) {
// Skip invisible Warnables.
continue
}
if t.isEffectivelyHealthyLocked(w) {
// Skip Warnables that are unhealthy if they have dependencies
// that are unhealthy.
continue
}
wm[w.Code] = *w.unhealthyState(ws)
}
@@ -97,3 +102,23 @@ func (t *Tracker) CurrentState() *State {
Warnings: wm,
}
}
// isEffectivelyHealthyLocked reports whether w is effectively healthy.
// That means it's either actually healthy or it has a dependency that
// that's unhealthy, so we should treat w as healthy to not spam users
// with multiple warnings when only the root cause is relevant.
func (t *Tracker) isEffectivelyHealthyLocked(w *Warnable) bool {
if _, ok := t.warnableVal[w]; !ok {
// Warnable not found in the tracker. So healthy.
return true
}
for _, d := range w.DependsOn {
if !t.isEffectivelyHealthyLocked(d) {
// If one of our deps is unhealthy, we're healthy.
return true
}
}
// If we have no unhealthy deps and had warnableVal set,
// we're unhealthy.
return false
}

View File

@@ -25,18 +25,26 @@ import (
"tailscale.com/types/ptr"
"tailscale.com/util/cloudenv"
"tailscale.com/util/dnsname"
"tailscale.com/util/lineread"
"tailscale.com/util/lineiter"
"tailscale.com/version"
"tailscale.com/version/distro"
)
var started = time.Now()
var newHooks []func(*tailcfg.Hostinfo)
// RegisterHostinfoNewHook registers a callback to be called on a non-nil
// [tailcfg.Hostinfo] before it is returned by [New].
func RegisterHostinfoNewHook(f func(*tailcfg.Hostinfo)) {
newHooks = append(newHooks, f)
}
// New returns a partially populated Hostinfo for the current host.
func New() *tailcfg.Hostinfo {
hostname, _ := os.Hostname()
hostname = dnsname.FirstLabel(hostname)
return &tailcfg.Hostinfo{
hi := &tailcfg.Hostinfo{
IPNVersion: version.Long(),
Hostname: hostname,
App: appTypeCached(),
@@ -57,8 +65,11 @@ func New() *tailcfg.Hostinfo {
Cloud: string(cloudenv.Get()),
NoLogsNoSupport: envknob.NoLogsNoSupport(),
AllowsUpdate: envknob.AllowsRemoteUpdate(),
WoLMACs: getWoLMACs(),
}
for _, f := range newHooks {
f(hi)
}
return hi
}
// non-nil on some platforms
@@ -231,12 +242,11 @@ func desktop() (ret opt.Bool) {
}
seenDesktop := false
lineread.File("/proc/net/unix", func(line []byte) error {
seenDesktop = seenDesktop || mem.Contains(mem.B(line), mem.S(" @/tmp/dbus-"))
for lr := range lineiter.File("/proc/net/unix") {
line, _ := lr.Value()
seenDesktop = seenDesktop || mem.Contains(mem.B(line), mem.S(".X11-unix"))
seenDesktop = seenDesktop || mem.Contains(mem.B(line), mem.S("/wayland-1"))
return nil
})
}
ret.Set(seenDesktop)
// Only cache after a minute - compositors might not have started yet.
@@ -280,13 +290,22 @@ func getEnvType() EnvType {
return ""
}
// inContainer reports whether we're running in a container.
// inContainer reports whether we're running in a container. Best-effort only,
// there's no foolproof way to detect this, but the build tag should catch all
// official builds from 1.78.0.
func inContainer() opt.Bool {
if runtime.GOOS != "linux" {
return ""
}
var ret opt.Bool
ret.Set(false)
if packageType != nil && packageType() == "container" {
// Go build tag ts_package_container was set during build.
ret.Set(true)
return ret
}
// Only set if using docker's container runtime. Not guaranteed by
// documentation, but it's been in place for a long time.
if _, err := os.Stat("/.dockerenv"); err == nil {
ret.Set(true)
return ret
@@ -296,21 +315,21 @@ func inContainer() opt.Bool {
ret.Set(true)
return ret
}
lineread.File("/proc/1/cgroup", func(line []byte) error {
for lr := range lineiter.File("/proc/1/cgroup") {
line, _ := lr.Value()
if mem.Contains(mem.B(line), mem.S("/docker/")) ||
mem.Contains(mem.B(line), mem.S("/lxc/")) {
ret.Set(true)
return io.EOF // arbitrary non-nil error to stop loop
break
}
return nil
})
lineread.File("/proc/mounts", func(line []byte) error {
}
for lr := range lineiter.File("/proc/mounts") {
line, _ := lr.Value()
if mem.Contains(mem.B(line), mem.S("lxcfs /proc/cpuinfo fuse.lxcfs")) {
ret.Set(true)
return io.EOF
break
}
return nil
})
}
return ret
}
@@ -362,7 +381,7 @@ func inFlyDotIo() bool {
}
func inReplit() bool {
// https://docs.replit.com/programming-ide/getting-repl-metadata
// https://docs.replit.com/replit-workspace/configuring-repl#environment-variables
if os.Getenv("REPL_OWNER") != "" && os.Getenv("REPL_SLUG") != "" {
return true
}

View File

@@ -12,7 +12,7 @@ import (
"golang.org/x/sys/unix"
"tailscale.com/types/ptr"
"tailscale.com/util/lineread"
"tailscale.com/util/lineiter"
"tailscale.com/version/distro"
)
@@ -106,15 +106,18 @@ func linuxVersionMeta() (meta versionMeta) {
}
m := map[string]string{}
lineread.File(propFile, func(line []byte) error {
for lr := range lineiter.File(propFile) {
line, err := lr.Value()
if err != nil {
break
}
eq := bytes.IndexByte(line, '=')
if eq == -1 {
return nil
continue
}
k, v := string(line[:eq]), strings.Trim(string(line[eq+1:]), `"'`)
m[k] = v
return nil
})
}
if v := m["VERSION_CODENAME"]; v != "" {
meta.DistroCodeName = v

106
vendor/tailscale.com/hostinfo/wol.go generated vendored
View File

@@ -1,106 +0,0 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package hostinfo
import (
"log"
"net"
"runtime"
"strings"
"unicode"
"tailscale.com/envknob"
)
// TODO(bradfitz): this is all too simplistic and static. It needs to run
// continuously in response to netmon events (USB ethernet adapaters might get
// plugged in) and look for the media type/status/etc. Right now on macOS it
// still detects a half dozen "up" en0, en1, en2, en3 etc interfaces that don't
// have any media. We should only report the one that's actually connected.
// But it works for now (2023-10-05) for fleshing out the rest.
var wakeMAC = envknob.RegisterString("TS_WAKE_MAC") // mac address, "false" or "auto". for https://github.com/tailscale/tailscale/issues/306
// getWoLMACs returns up to 10 MAC address of the local machine to send
// wake-on-LAN packets to in order to wake it up. The returned MACs are in
// lowercase hex colon-separated form ("xx:xx:xx:xx:xx:xx").
//
// If TS_WAKE_MAC=auto, it tries to automatically find the MACs based on the OS
// type and interface properties. (TODO(bradfitz): incomplete) If TS_WAKE_MAC is
// set to a MAC address, that sole MAC address is returned.
func getWoLMACs() (macs []string) {
switch runtime.GOOS {
case "ios", "android":
return nil
}
if s := wakeMAC(); s != "" {
switch s {
case "auto":
ifs, _ := net.Interfaces()
for _, iface := range ifs {
if iface.Flags&net.FlagLoopback != 0 {
continue
}
if iface.Flags&net.FlagBroadcast == 0 ||
iface.Flags&net.FlagRunning == 0 ||
iface.Flags&net.FlagUp == 0 {
continue
}
if keepMAC(iface.Name, iface.HardwareAddr) {
macs = append(macs, iface.HardwareAddr.String())
}
if len(macs) == 10 {
break
}
}
return macs
case "false", "off": // fast path before ParseMAC error
return nil
}
mac, err := net.ParseMAC(s)
if err != nil {
log.Printf("invalid MAC %q", s)
return nil
}
return []string{mac.String()}
}
return nil
}
var ignoreWakeOUI = map[[3]byte]bool{
{0x00, 0x15, 0x5d}: true, // Hyper-V
{0x00, 0x50, 0x56}: true, // VMware
{0x00, 0x1c, 0x14}: true, // VMware
{0x00, 0x05, 0x69}: true, // VMware
{0x00, 0x0c, 0x29}: true, // VMware
{0x00, 0x1c, 0x42}: true, // Parallels
{0x08, 0x00, 0x27}: true, // VirtualBox
{0x00, 0x21, 0xf6}: true, // VirtualBox
{0x00, 0x14, 0x4f}: true, // VirtualBox
{0x00, 0x0f, 0x4b}: true, // VirtualBox
{0x52, 0x54, 0x00}: true, // VirtualBox/Vagrant
}
func keepMAC(ifName string, mac []byte) bool {
if len(mac) != 6 {
return false
}
base := strings.TrimRightFunc(ifName, unicode.IsNumber)
switch runtime.GOOS {
case "darwin":
switch base {
case "llw", "awdl", "utun", "bridge", "lo", "gif", "stf", "anpi", "ap":
return false
}
}
if mac[0] == 0x02 && mac[1] == 0x42 {
// Docker container.
return false
}
oui := [3]byte{mac[0], mac[1], mac[2]}
if ignoreWakeOUI[oui] {
return false
}
return true
}

466
vendor/tailscale.com/ipn/auditlog/auditlog.go generated vendored Normal file
View File

@@ -0,0 +1,466 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// Package auditlog provides a mechanism for logging audit events.
package auditlog
import (
"context"
"encoding/json"
"errors"
"fmt"
"sort"
"sync"
"time"
"tailscale.com/ipn"
"tailscale.com/tailcfg"
"tailscale.com/types/logger"
"tailscale.com/util/rands"
"tailscale.com/util/set"
)
// transaction represents an audit log that has not yet been sent to the control plane.
type transaction struct {
// EventID is the unique identifier for the event being logged.
// This is used on the client side only and is not sent to control.
EventID string `json:",omitempty"`
// Retries is the number of times the logger has attempted to send this log.
// This is used on the client side only and is not sent to control.
Retries int `json:",omitempty"`
// Action is the action to be logged. It must correspond to a known action in the control plane.
Action tailcfg.ClientAuditAction `json:",omitempty"`
// Details is an opaque string specific to the action being logged. Empty strings may not
// be valid depending on the action being logged.
Details string `json:",omitempty"`
// TimeStamp is the time at which the audit log was generated on the node.
TimeStamp time.Time `json:",omitzero"`
}
// Transport provides a means for a client to send audit logs to a consumer (typically the control plane).
type Transport interface {
// SendAuditLog sends an audit log to a consumer of audit logs.
// Errors should be checked with [IsRetryableError] for retryability.
SendAuditLog(context.Context, tailcfg.AuditLogRequest) error
}
// LogStore provides a means for a [Logger] to persist logs to disk or memory.
type LogStore interface {
// Save saves the given data to a persistent store. Save will overwrite existing data
// for the given key.
save(key ipn.ProfileID, txns []*transaction) error
// Load retrieves the data from a persistent store. Returns a nil slice and
// no error if no data exists for the given key.
load(key ipn.ProfileID) ([]*transaction, error)
}
// Opts contains the configuration options for a [Logger].
type Opts struct {
// RetryLimit is the maximum number of attempts the logger will make to send a log before giving up.
RetryLimit int
// Store is the persistent store used to save logs to disk. Must be non-nil.
Store LogStore
// Logf is the logger used to log messages from the audit logger. Must be non-nil.
Logf logger.Logf
}
// IsRetryableError returns true if the given error is retryable
// See [controlclient.apiResponseError]. Potentially retryable errors implement the Retryable() method.
func IsRetryableError(err error) bool {
var retryable interface{ Retryable() bool }
return errors.As(err, &retryable) && retryable.Retryable()
}
type backoffOpts struct {
min, max time.Duration
multiplier float64
}
// .5, 1, 2, 4, 8, 10, 10, 10, 10, 10...
var defaultBackoffOpts = backoffOpts{
min: time.Millisecond * 500,
max: 10 * time.Second,
multiplier: 2,
}
// Logger provides a queue-based mechanism for submitting audit logs to the control plane - or
// another suitable consumer. Logs are stored to disk and retried until they are successfully sent,
// or until they permanently fail.
//
// Each individual profile/controlclient tuple should construct and manage a unique [Logger] instance.
type Logger struct {
logf logger.Logf
retryLimit int // the maximum number of attempts to send a log before giving up.
flusher chan struct{} // channel used to signal a flush operation.
done chan struct{} // closed when the flush worker exits.
ctx context.Context // canceled when the logger is stopped.
ctxCancel context.CancelFunc // cancels ctx.
backoffOpts // backoff settings for retry operations.
// mu protects the fields below.
mu sync.Mutex
store LogStore // persistent storage for unsent logs.
profileID ipn.ProfileID // empty if [Logger.SetProfileID] has not been called.
transport Transport // nil until [Logger.Start] is called.
}
// NewLogger creates a new [Logger] with the given options.
func NewLogger(opts Opts) *Logger {
ctx, cancel := context.WithCancel(context.Background())
al := &Logger{
retryLimit: opts.RetryLimit,
logf: logger.WithPrefix(opts.Logf, "auditlog: "),
store: opts.Store,
flusher: make(chan struct{}, 1),
done: make(chan struct{}),
ctx: ctx,
ctxCancel: cancel,
backoffOpts: defaultBackoffOpts,
}
al.logf("created")
return al
}
// FlushAndStop synchronously flushes all pending logs and stops the audit logger.
// This will block until a final flush operation completes or context is done.
// If the logger is already stopped, this will return immediately. All unsent
// logs will be persisted to the store.
func (al *Logger) FlushAndStop(ctx context.Context) {
al.stop()
al.flush(ctx)
}
// SetProfileID sets the profileID for the logger. This must be called before any logs can be enqueued.
// The profileID of a logger cannot be changed once set.
func (al *Logger) SetProfileID(profileID ipn.ProfileID) error {
al.mu.Lock()
defer al.mu.Unlock()
if al.profileID != "" {
return errors.New("profileID already set")
}
al.profileID = profileID
return nil
}
// Start starts the audit logger with the given transport.
// It returns an error if the logger is already started.
func (al *Logger) Start(t Transport) error {
al.mu.Lock()
defer al.mu.Unlock()
if al.transport != nil {
return errors.New("already started")
}
al.transport = t
pending, err := al.storedCountLocked()
if err != nil {
al.logf("[unexpected] failed to restore logs: %v", err)
}
go al.flushWorker()
if pending > 0 {
al.flushAsync()
}
return nil
}
// ErrAuditLogStorageFailure is returned when the logger fails to persist logs to the store.
var ErrAuditLogStorageFailure = errors.New("audit log storage failure")
// Enqueue queues an audit log to be sent to the control plane (or another suitable consumer/transport).
// This will return an error if the underlying store fails to save the log or we fail to generate a unique
// eventID for the log.
func (al *Logger) Enqueue(action tailcfg.ClientAuditAction, details string) error {
txn := &transaction{
Action: action,
Details: details,
TimeStamp: time.Now(),
}
// Generate a suitably random eventID for the transaction.
txn.EventID = fmt.Sprint(txn.TimeStamp, rands.HexString(16))
return al.enqueue(txn)
}
// flushAsync requests an asynchronous flush.
// It is a no-op if a flush is already pending.
func (al *Logger) flushAsync() {
select {
case al.flusher <- struct{}{}:
default:
}
}
func (al *Logger) flushWorker() {
defer close(al.done)
var retryDelay time.Duration
retry := time.NewTimer(0)
retry.Stop()
for {
select {
case <-al.ctx.Done():
return
case <-al.flusher:
err := al.flush(al.ctx)
switch {
case errors.Is(err, context.Canceled):
// The logger was stopped, no need to retry.
return
case err != nil:
retryDelay = max(al.backoffOpts.min, min(retryDelay*time.Duration(al.backoffOpts.multiplier), al.backoffOpts.max))
al.logf("retrying after %v, %v", retryDelay, err)
retry.Reset(retryDelay)
default:
retryDelay = 0
retry.Stop()
}
case <-retry.C:
al.flushAsync()
}
}
}
// flush attempts to send all pending logs to the control plane.
// l.mu must not be held.
func (al *Logger) flush(ctx context.Context) error {
al.mu.Lock()
pending, err := al.store.load(al.profileID)
t := al.transport
al.mu.Unlock()
if err != nil {
// This will catch nil profileIDs
return fmt.Errorf("failed to restore pending logs: %w", err)
}
if len(pending) == 0 {
return nil
}
if t == nil {
return errors.New("no transport")
}
complete, unsent := al.sendToTransport(ctx, pending, t)
al.markTransactionsDone(complete)
al.mu.Lock()
defer al.mu.Unlock()
if err = al.appendToStoreLocked(unsent); err != nil {
al.logf("[unexpected] failed to persist logs: %v", err)
}
if len(unsent) != 0 {
return fmt.Errorf("failed to send %d logs", len(unsent))
}
if len(complete) != 0 {
al.logf("complete %d audit log transactions", len(complete))
}
return nil
}
// sendToTransport sends all pending logs to the control plane. Returns a pair of slices
// containing the logs that were successfully sent (or failed permanently) and those that were not.
//
// This may require multiple round trips to the control plane and can be a long running transaction.
func (al *Logger) sendToTransport(ctx context.Context, pending []*transaction, t Transport) (complete []*transaction, unsent []*transaction) {
for i, txn := range pending {
req := tailcfg.AuditLogRequest{
Action: tailcfg.ClientAuditAction(txn.Action),
Details: txn.Details,
Timestamp: txn.TimeStamp,
}
if err := t.SendAuditLog(ctx, req); err != nil {
switch {
case errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded):
// The contex is done. All further attempts will fail.
unsent = append(unsent, pending[i:]...)
return complete, unsent
case IsRetryableError(err) && txn.Retries+1 < al.retryLimit:
// We permit a maximum number of retries for each log. All retriable
// errors should be transient and we should be able to send the log eventually, but
// we don't want logs to be persisted indefinitely.
txn.Retries++
unsent = append(unsent, txn)
default:
complete = append(complete, txn)
al.logf("failed permanently: %v", err)
}
} else {
// No error - we're done.
complete = append(complete, txn)
}
}
return complete, unsent
}
func (al *Logger) stop() {
al.mu.Lock()
t := al.transport
al.mu.Unlock()
if t == nil {
// No transport means no worker goroutine and done will not be
// closed if we cancel the context.
return
}
al.ctxCancel()
<-al.done
al.logf("stopped for profileID: %v", al.profileID)
}
// appendToStoreLocked persists logs to the store. This will deduplicate
// logs so it is safe to call this with the same logs multiple time, to
// requeue failed transactions for example.
//
// l.mu must be held.
func (al *Logger) appendToStoreLocked(txns []*transaction) error {
if len(txns) == 0 {
return nil
}
if al.profileID == "" {
return errors.New("no logId set")
}
persisted, err := al.store.load(al.profileID)
if err != nil {
al.logf("[unexpected] append failed to restore logs: %v", err)
}
// The order is important here. We want the latest transactions first, which will
// ensure when we dedup, the new transactions are seen and the older transactions
// are discarded.
txnsOut := append(txns, persisted...)
txnsOut = deduplicateAndSort(txnsOut)
return al.store.save(al.profileID, txnsOut)
}
// storedCountLocked returns the number of logs persisted to the store.
// al.mu must be held.
func (al *Logger) storedCountLocked() (int, error) {
persisted, err := al.store.load(al.profileID)
return len(persisted), err
}
// markTransactionsDone removes logs from the store that are complete (sent or failed permanently).
// al.mu must not be held.
func (al *Logger) markTransactionsDone(sent []*transaction) {
al.mu.Lock()
defer al.mu.Unlock()
ids := set.Set[string]{}
for _, txn := range sent {
ids.Add(txn.EventID)
}
persisted, err := al.store.load(al.profileID)
if err != nil {
al.logf("[unexpected] markTransactionsDone failed to restore logs: %v", err)
}
var unsent []*transaction
for _, txn := range persisted {
if !ids.Contains(txn.EventID) {
unsent = append(unsent, txn)
}
}
al.store.save(al.profileID, unsent)
}
// deduplicateAndSort removes duplicate logs from the given slice and sorts them by timestamp.
// The first log entry in the slice will be retained, subsequent logs with the same EventID will be discarded.
func deduplicateAndSort(txns []*transaction) []*transaction {
seen := set.Set[string]{}
deduped := make([]*transaction, 0, len(txns))
for _, txn := range txns {
if !seen.Contains(txn.EventID) {
deduped = append(deduped, txn)
seen.Add(txn.EventID)
}
}
// Sort logs by timestamp - oldest to newest. This will put the oldest logs at
// the front of the queue.
sort.Slice(deduped, func(i, j int) bool {
return deduped[i].TimeStamp.Before(deduped[j].TimeStamp)
})
return deduped
}
func (al *Logger) enqueue(txn *transaction) error {
al.mu.Lock()
defer al.mu.Unlock()
if err := al.appendToStoreLocked([]*transaction{txn}); err != nil {
return fmt.Errorf("%w: %w", ErrAuditLogStorageFailure, err)
}
// If a.transport is nil if the logger is stopped.
if al.transport != nil {
al.flushAsync()
}
return nil
}
var _ LogStore = (*logStateStore)(nil)
// logStateStore is a concrete implementation of [LogStore]
// using [ipn.StateStore] as the underlying storage.
type logStateStore struct {
store ipn.StateStore
}
// NewLogStore creates a new LogStateStore with the given [ipn.StateStore].
func NewLogStore(store ipn.StateStore) LogStore {
return &logStateStore{
store: store,
}
}
func (s *logStateStore) generateKey(key ipn.ProfileID) string {
return "auditlog-" + string(key)
}
// Save saves the given logs to an [ipn.StateStore]. This overwrites
// any existing entries for the given key.
func (s *logStateStore) save(key ipn.ProfileID, txns []*transaction) error {
if key == "" {
return errors.New("empty key")
}
data, err := json.Marshal(txns)
if err != nil {
return err
}
k := ipn.StateKey(s.generateKey(key))
return s.store.WriteState(k, data)
}
// Load retrieves the logs from an [ipn.StateStore].
func (s *logStateStore) load(key ipn.ProfileID) ([]*transaction, error) {
if key == "" {
return nil, errors.New("empty key")
}
k := ipn.StateKey(s.generateKey(key))
data, err := s.store.ReadState(k)
switch {
case errors.Is(err, ipn.ErrStateNotExist):
return nil, nil
case err != nil:
return nil, err
}
var txns []*transaction
err = json.Unmarshal(data, &txns)
return txns, err
}

30
vendor/tailscale.com/ipn/backend.go generated vendored
View File

@@ -58,21 +58,29 @@ type EngineStatus struct {
// to subscribe to.
type NotifyWatchOpt uint64
// NotifyWatchOpt values.
//
// These aren't declared using Go's iota because they're not purely internal to
// the process and iota should not be used for values that are serialized to
// disk or network. In this case, these values come over the network via the
// LocalAPI, a mostly stable API.
const (
// NotifyWatchEngineUpdates, if set, causes Engine updates to be sent to the
// client either regularly or when they change, without having to ask for
// each one via Engine.RequestStatus.
NotifyWatchEngineUpdates NotifyWatchOpt = 1 << iota
NotifyWatchEngineUpdates NotifyWatchOpt = 1 << 0
NotifyInitialState // if set, the first Notify message (sent immediately) will contain the current State + BrowseToURL + SessionID
NotifyInitialPrefs // if set, the first Notify message (sent immediately) will contain the current Prefs
NotifyInitialNetMap // if set, the first Notify message (sent immediately) will contain the current NetMap
NotifyInitialState NotifyWatchOpt = 1 << 1 // if set, the first Notify message (sent immediately) will contain the current State + BrowseToURL + SessionID
NotifyInitialPrefs NotifyWatchOpt = 1 << 2 // if set, the first Notify message (sent immediately) will contain the current Prefs
NotifyInitialNetMap NotifyWatchOpt = 1 << 3 // if set, the first Notify message (sent immediately) will contain the current NetMap
NotifyNoPrivateKeys // if set, private keys that would normally be sent in updates are zeroed out
NotifyInitialDriveShares // if set, the first Notify message (sent immediately) will contain the current Taildrive Shares
NotifyInitialOutgoingFiles // if set, the first Notify message (sent immediately) will contain the current Taildrop OutgoingFiles
NotifyNoPrivateKeys NotifyWatchOpt = 1 << 4 // if set, private keys that would normally be sent in updates are zeroed out
NotifyInitialDriveShares NotifyWatchOpt = 1 << 5 // if set, the first Notify message (sent immediately) will contain the current Taildrive Shares
NotifyInitialOutgoingFiles NotifyWatchOpt = 1 << 6 // if set, the first Notify message (sent immediately) will contain the current Taildrop OutgoingFiles
NotifyInitialHealthState // if set, the first Notify message (sent immediately) will contain the current health.State of the client
NotifyInitialHealthState NotifyWatchOpt = 1 << 7 // if set, the first Notify message (sent immediately) will contain the current health.State of the client
NotifyRateLimit NotifyWatchOpt = 1 << 8 // if set, rate limit spammy netmap updates to every few seconds
)
// Notify is a communication from a backend (e.g. tailscaled) to a frontend
@@ -100,7 +108,6 @@ type Notify struct {
NetMap *netmap.NetworkMap // if non-nil, the new or current netmap
Engine *EngineStatus // if non-nil, the new or current wireguard stats
BrowseToURL *string // if non-nil, UI should open a browser right now
BackendLogID *string // if non-nil, the public logtail ID used by backend
// FilesWaiting if non-nil means that files are buffered in
// the Tailscale daemon and ready for local transfer to the
@@ -146,7 +153,7 @@ type Notify struct {
// any changes to the user in the UI.
Health *health.State `json:",omitempty"`
// type is mirrored in xcode/Shared/IPN.swift
// type is mirrored in xcode/IPN/Core/LocalAPI/Model/LocalAPIModel.swift
}
func (n Notify) String() string {
@@ -173,9 +180,6 @@ func (n Notify) String() string {
if n.BrowseToURL != nil {
sb.WriteString("URL=<...> ")
}
if n.BackendLogID != nil {
sb.WriteString("BackendLogID ")
}
if n.FilesWaiting != nil {
sb.WriteString("FilesWaiting ")
}

18
vendor/tailscale.com/ipn/conf.go generated vendored
View File

@@ -32,6 +32,10 @@ type ConfigVAlpha struct {
AdvertiseRoutes []netip.Prefix `json:",omitempty"`
DisableSNAT opt.Bool `json:",omitempty"`
AdvertiseServices []string `json:",omitempty"`
AppConnector *AppConnectorPrefs `json:",omitempty"` // advertise app connector; defaults to false (if nil or explicitly set to false)
NetfilterMode *string `json:",omitempty"` // "on", "off", "nodivert"
NoStatefulFiltering opt.Bool `json:",omitempty"`
@@ -137,5 +141,19 @@ func (c *ConfigVAlpha) ToPrefs() (MaskedPrefs, error) {
mp.AutoUpdate = *c.AutoUpdate
mp.AutoUpdateSet = AutoUpdatePrefsMask{ApplySet: true, CheckSet: true}
}
if c.AppConnector != nil {
mp.AppConnector = *c.AppConnector
mp.AppConnectorSet = true
}
// Configfile should be the source of truth for whether this node
// advertises any services. We need to ensure that each reload updates
// currently advertised services as else the transition from 'some
// services are advertised' to 'advertised services are empty/unset in
// conffile' would have no effect (especially given that an empty
// service slice would be omitted from the JSON config).
mp.AdvertiseServicesSet = true
if c.AdvertiseServices != nil {
mp.AdvertiseServices = c.AdvertiseServices
}
return mp, nil
}

6
vendor/tailscale.com/ipn/desktop/doc.go generated vendored Normal file
View File

@@ -0,0 +1,6 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// Package desktop facilitates interaction with the desktop environment
// and user sessions. As of 2025-02-06, it is only implemented for Windows.
package desktop

24
vendor/tailscale.com/ipn/desktop/mksyscall.go generated vendored Normal file
View File

@@ -0,0 +1,24 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package desktop
//go:generate go run golang.org/x/sys/windows/mkwinsyscall -output zsyscall_windows.go mksyscall.go
//go:generate go run golang.org/x/tools/cmd/goimports -w zsyscall_windows.go
//sys setLastError(dwErrorCode uint32) = kernel32.SetLastError
//sys registerClassEx(windowClass *_WNDCLASSEX) (atom uint16, err error) [atom==0] = user32.RegisterClassExW
//sys createWindowEx(dwExStyle uint32, lpClassName *uint16, lpWindowName *uint16, dwStyle uint32, x int32, y int32, nWidth int32, nHeight int32, hWndParent windows.HWND, hMenu windows.Handle, hInstance windows.Handle, lpParam unsafe.Pointer) (hWnd windows.HWND, err error) [hWnd==0] = user32.CreateWindowExW
//sys defWindowProc(hwnd windows.HWND, msg uint32, wparam uintptr, lparam uintptr) (res uintptr) = user32.DefWindowProcW
//sys setWindowLongPtr(hwnd windows.HWND, index int32, newLong uintptr) (res uintptr, err error) [res==0 && e1!=0] = user32.SetWindowLongPtrW
//sys getWindowLongPtr(hwnd windows.HWND, index int32) (res uintptr, err error) [res==0 && e1!=0] = user32.GetWindowLongPtrW
//sys sendMessage(hwnd windows.HWND, msg uint32, wparam uintptr, lparam uintptr) (res uintptr) = user32.SendMessageW
//sys getMessage(lpMsg *_MSG, hwnd windows.HWND, msgMin uint32, msgMax uint32) (ret int32) = user32.GetMessageW
//sys translateMessage(lpMsg *_MSG) (res bool) = user32.TranslateMessage
//sys dispatchMessage(lpMsg *_MSG) (res uintptr) = user32.DispatchMessageW
//sys destroyWindow(hwnd windows.HWND) (err error) [int32(failretval)==0] = user32.DestroyWindow
//sys postQuitMessage(exitCode int32) = user32.PostQuitMessage
//sys registerSessionNotification(hServer windows.Handle, hwnd windows.HWND, flags uint32) (err error) [int32(failretval)==0] = wtsapi32.WTSRegisterSessionNotificationEx
//sys unregisterSessionNotification(hServer windows.Handle, hwnd windows.HWND) (err error) [int32(failretval)==0] = wtsapi32.WTSUnRegisterSessionNotificationEx

58
vendor/tailscale.com/ipn/desktop/session.go generated vendored Normal file
View File

@@ -0,0 +1,58 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package desktop
import (
"fmt"
"tailscale.com/ipn/ipnauth"
)
// SessionID is a unique identifier of a desktop session.
type SessionID uint
// SessionStatus is the status of a desktop session.
type SessionStatus int
const (
// ClosedSession is a session that does not exist, is not yet initialized by the OS,
// or has been terminated.
ClosedSession SessionStatus = iota
// ForegroundSession is a session that a user can interact with,
// such as when attached to a physical console or an active,
// unlocked RDP connection.
ForegroundSession
// BackgroundSession indicates that the session is locked, disconnected,
// or otherwise running without user presence or interaction.
BackgroundSession
)
// String implements [fmt.Stringer].
func (s SessionStatus) String() string {
switch s {
case ClosedSession:
return "Closed"
case ForegroundSession:
return "Foreground"
case BackgroundSession:
return "Background"
default:
panic("unreachable")
}
}
// Session is a state of a desktop session at a given point in time.
type Session struct {
ID SessionID // Identifier of the session; can be reused after the session is closed.
Status SessionStatus // The status of the session, such as foreground or background.
User ipnauth.Actor // User logged into the session.
}
// Description returns a human-readable description of the session.
func (s *Session) Description() string {
if maybeUsername, _ := s.User.Username(); maybeUsername != "" { // best effort
return fmt.Sprintf("Session %d - %q (%s)", s.ID, maybeUsername, s.Status)
}
return fmt.Sprintf("Session %d (%s)", s.ID, s.Status)
}

60
vendor/tailscale.com/ipn/desktop/sessions.go generated vendored Normal file
View File

@@ -0,0 +1,60 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package desktop
import (
"errors"
"runtime"
)
// ErrNotImplemented is returned by [NewSessionManager] when it is not
// implemented for the current GOOS.
var ErrNotImplemented = errors.New("not implemented for GOOS=" + runtime.GOOS)
// SessionInitCallback is a function that is called once per [Session].
// It returns an optional cleanup function that is called when the session
// is about to be destroyed, or nil if no cleanup is needed.
// It is not safe to call SessionManager methods from within the callback.
type SessionInitCallback func(session *Session) (cleanup func())
// SessionStateCallback is a function that reports the initial or updated
// state of a [Session], such as when it transitions between foreground and background.
// It is guaranteed to be called after all registered [SessionInitCallback] functions
// have completed, and before any cleanup functions are called for the same session.
// It is not safe to call SessionManager methods from within the callback.
type SessionStateCallback func(session *Session)
// SessionManager is an interface that provides access to desktop sessions on the current platform.
// It is safe for concurrent use.
type SessionManager interface {
// Init explicitly initializes the receiver.
// Unless the receiver is explicitly initialized, it will be lazily initialized
// on the first call to any other method.
// It is safe to call Init multiple times.
Init() error
// Sessions returns a session snapshot taken at the time of the call.
// Since sessions can be created or destroyed at any time, it may become
// outdated as soon as it is returned.
//
// It is primarily intended for logging and debugging.
// Prefer registering a [SessionInitCallback] or [SessionStateCallback]
// in contexts requiring stronger guarantees.
Sessions() (map[SessionID]*Session, error)
// RegisterInitCallback registers a [SessionInitCallback] that is called for each existing session
// and for each new session that is created, until the returned unregister function is called.
// If the specified [SessionInitCallback] returns a cleanup function, it is called when the session
// is about to be destroyed. The callback function is guaranteed to be called once and only once
// for each existing and new session.
RegisterInitCallback(cb SessionInitCallback) (unregister func(), err error)
// RegisterStateCallback registers a [SessionStateCallback] that is called for each existing session
// and every time the state of a session changes, until the returned unregister function is called.
RegisterStateCallback(cb SessionStateCallback) (unregister func(), err error)
// Close waits for all registered callbacks to complete
// and releases resources associated with the receiver.
Close() error
}

View File

@@ -0,0 +1,15 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build !windows
package desktop
import "tailscale.com/types/logger"
// NewSessionManager returns a new [SessionManager] for the current platform,
// [ErrNotImplemented] if the platform is not supported, or an error if the
// session manager could not be created.
func NewSessionManager(logger.Logf) (SessionManager, error) {
return nil, ErrNotImplemented
}

672
vendor/tailscale.com/ipn/desktop/sessions_windows.go generated vendored Normal file
View File

@@ -0,0 +1,672 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package desktop
import (
"context"
"errors"
"fmt"
"runtime"
"sync"
"syscall"
"unsafe"
"golang.org/x/sys/windows"
"tailscale.com/ipn/ipnauth"
"tailscale.com/types/logger"
"tailscale.com/util/must"
"tailscale.com/util/set"
)
// wtsManager is a [SessionManager] implementation for Windows.
type wtsManager struct {
logf logger.Logf
ctx context.Context // cancelled when the manager is closed
ctxCancel context.CancelFunc
initOnce func() error
watcher *sessionWatcher
mu sync.Mutex
sessions map[SessionID]*wtsSession
initCbs set.HandleSet[SessionInitCallback]
stateCbs set.HandleSet[SessionStateCallback]
}
// NewSessionManager returns a new [SessionManager] for the current platform,
func NewSessionManager(logf logger.Logf) (SessionManager, error) {
ctx, ctxCancel := context.WithCancel(context.Background())
m := &wtsManager{
logf: logf,
ctx: ctx,
ctxCancel: ctxCancel,
sessions: make(map[SessionID]*wtsSession),
}
m.watcher = newSessionWatcher(m.ctx, m.logf, m.sessionEventHandler)
m.initOnce = sync.OnceValue(func() error {
if err := waitUntilWTSReady(m.ctx); err != nil {
return fmt.Errorf("WTS is not ready: %w", err)
}
m.mu.Lock()
defer m.mu.Unlock()
if err := m.watcher.Start(); err != nil {
return fmt.Errorf("failed to start session watcher: %w", err)
}
var err error
m.sessions, err = enumerateSessions()
return err // may be nil or non-nil
})
return m, nil
}
// Init implements [SessionManager].
func (m *wtsManager) Init() error {
return m.initOnce()
}
// Sessions implements [SessionManager].
func (m *wtsManager) Sessions() (map[SessionID]*Session, error) {
if err := m.initOnce(); err != nil {
return nil, err
}
m.mu.Lock()
defer m.mu.Unlock()
sessions := make(map[SessionID]*Session, len(m.sessions))
for _, s := range m.sessions {
sessions[s.id] = s.AsSession()
}
return sessions, nil
}
// RegisterInitCallback implements [SessionManager].
func (m *wtsManager) RegisterInitCallback(cb SessionInitCallback) (unregister func(), err error) {
if err := m.initOnce(); err != nil {
return nil, err
}
if cb == nil {
return nil, errors.New("nil callback")
}
m.mu.Lock()
defer m.mu.Unlock()
handle := m.initCbs.Add(cb)
// TODO(nickkhyl): enqueue callbacks in a separate goroutine?
for _, s := range m.sessions {
if cleanup := cb(s.AsSession()); cleanup != nil {
s.cleanup = append(s.cleanup, cleanup)
}
}
return func() {
m.mu.Lock()
defer m.mu.Unlock()
delete(m.initCbs, handle)
}, nil
}
// RegisterStateCallback implements [SessionManager].
func (m *wtsManager) RegisterStateCallback(cb SessionStateCallback) (unregister func(), err error) {
if err := m.initOnce(); err != nil {
return nil, err
}
if cb == nil {
return nil, errors.New("nil callback")
}
m.mu.Lock()
defer m.mu.Unlock()
handle := m.stateCbs.Add(cb)
// TODO(nickkhyl): enqueue callbacks in a separate goroutine?
for _, s := range m.sessions {
cb(s.AsSession())
}
return func() {
m.mu.Lock()
defer m.mu.Unlock()
delete(m.stateCbs, handle)
}, nil
}
func (m *wtsManager) sessionEventHandler(id SessionID, event uint32) {
m.mu.Lock()
defer m.mu.Unlock()
switch event {
case windows.WTS_SESSION_LOGON:
// The session may have been created after we started watching,
// but before the initial enumeration was performed.
// Do not create a new session if it already exists.
if _, _, err := m.getOrCreateSessionLocked(id); err != nil {
m.logf("[unexpected] getOrCreateSessionLocked(%d): %v", id, err)
}
case windows.WTS_SESSION_LOCK:
if err := m.setSessionStatusLocked(id, BackgroundSession); err != nil {
m.logf("[unexpected] setSessionStatusLocked(%d, BackgroundSession): %v", id, err)
}
case windows.WTS_SESSION_UNLOCK:
if err := m.setSessionStatusLocked(id, ForegroundSession); err != nil {
m.logf("[unexpected] setSessionStatusLocked(%d, ForegroundSession): %v", id, err)
}
case windows.WTS_SESSION_LOGOFF:
if err := m.deleteSessionLocked(id); err != nil {
m.logf("[unexpected] deleteSessionLocked(%d): %v", id, err)
}
}
}
func (m *wtsManager) getOrCreateSessionLocked(id SessionID) (_ *wtsSession, created bool, err error) {
if s, ok := m.sessions[id]; ok {
return s, false, nil
}
s, err := newWTSSession(id, ForegroundSession)
if err != nil {
return nil, false, err
}
m.sessions[id] = s
session := s.AsSession()
// TODO(nickkhyl): enqueue callbacks in a separate goroutine?
for _, cb := range m.initCbs {
if cleanup := cb(session); cleanup != nil {
s.cleanup = append(s.cleanup, cleanup)
}
}
for _, cb := range m.stateCbs {
cb(session)
}
return s, true, err
}
func (m *wtsManager) setSessionStatusLocked(id SessionID, status SessionStatus) error {
s, _, err := m.getOrCreateSessionLocked(id)
if err != nil {
return err
}
if s.status == status {
return nil
}
s.status = status
session := s.AsSession()
// TODO(nickkhyl): enqueue callbacks in a separate goroutine?
for _, cb := range m.stateCbs {
cb(session)
}
return nil
}
func (m *wtsManager) deleteSessionLocked(id SessionID) error {
s, ok := m.sessions[id]
if !ok {
return nil
}
s.status = ClosedSession
session := s.AsSession()
// TODO(nickkhyl): enqueue callbacks (and [wtsSession.close]!) in a separate goroutine?
for _, cb := range m.stateCbs {
cb(session)
}
delete(m.sessions, id)
return s.close()
}
func (m *wtsManager) Close() error {
m.ctxCancel()
if m.watcher != nil {
err := m.watcher.Stop()
if err != nil {
return err
}
m.watcher = nil
}
m.mu.Lock()
defer m.mu.Unlock()
m.initCbs = nil
m.stateCbs = nil
errs := make([]error, 0, len(m.sessions))
for _, s := range m.sessions {
errs = append(errs, s.close())
}
m.sessions = nil
return errors.Join(errs...)
}
type wtsSession struct {
id SessionID
user *ipnauth.WindowsActor
status SessionStatus
cleanup []func()
}
func newWTSSession(id SessionID, status SessionStatus) (*wtsSession, error) {
var token windows.Token
if err := windows.WTSQueryUserToken(uint32(id), &token); err != nil {
return nil, err
}
user, err := ipnauth.NewWindowsActorWithToken(token)
if err != nil {
return nil, err
}
return &wtsSession{id, user, status, nil}, nil
}
// enumerateSessions returns a map of all active WTS sessions.
func enumerateSessions() (map[SessionID]*wtsSession, error) {
const reserved, version uint32 = 0, 1
var numSessions uint32
var sessionInfos *windows.WTS_SESSION_INFO
if err := windows.WTSEnumerateSessions(_WTS_CURRENT_SERVER_HANDLE, reserved, version, &sessionInfos, &numSessions); err != nil {
return nil, fmt.Errorf("WTSEnumerateSessions failed: %w", err)
}
defer windows.WTSFreeMemory(uintptr(unsafe.Pointer(sessionInfos)))
sessions := make(map[SessionID]*wtsSession, numSessions)
for _, si := range unsafe.Slice(sessionInfos, numSessions) {
status := _WTS_CONNECTSTATE_CLASS(si.State).ToSessionStatus()
if status == ClosedSession {
// The session does not exist as far as we're concerned.
// It may be in the process of being created or destroyed,
// or be a special "listener" session, etc.
continue
}
id := SessionID(si.SessionID)
session, err := newWTSSession(id, status)
if err != nil {
continue
}
sessions[id] = session
}
return sessions, nil
}
func (s *wtsSession) AsSession() *Session {
return &Session{
ID: s.id,
Status: s.status,
// wtsSession owns the user; don't let the caller close it
User: ipnauth.WithoutClose(s.user),
}
}
func (m *wtsSession) close() error {
for _, cleanup := range m.cleanup {
cleanup()
}
m.cleanup = nil
if m.user != nil {
if err := m.user.Close(); err != nil {
return err
}
m.user = nil
}
return nil
}
type sessionEventHandler func(id SessionID, event uint32)
// TODO(nickkhyl): implement a sessionWatcher that does not use the message queue.
// One possible approach is to have the tailscaled service register a HandlerEx function
// and stream SERVICE_CONTROL_SESSIONCHANGE events to the tailscaled subprocess
// (the actual tailscaled backend), exposing these events via [sessionWatcher]/[wtsManager].
//
// See tailscale/corp#26477 for details and tracking.
type sessionWatcher struct {
logf logger.Logf
ctx context.Context // canceled to stop the watcher
ctxCancel context.CancelFunc // cancels the watcher
hWnd windows.HWND // window handle for receiving session change notifications
handler sessionEventHandler // called on session events
mu sync.Mutex
doneCh chan error // written to when the watcher exits; nil if not started
}
func newSessionWatcher(ctx context.Context, logf logger.Logf, handler sessionEventHandler) *sessionWatcher {
ctx, cancel := context.WithCancel(ctx)
return &sessionWatcher{logf: logf, ctx: ctx, ctxCancel: cancel, handler: handler}
}
func (sw *sessionWatcher) Start() error {
sw.mu.Lock()
defer sw.mu.Unlock()
select {
case <-sw.ctx.Done():
return fmt.Errorf("sessionWatcher already stopped: %w", sw.ctx.Err())
default:
}
if sw.doneCh != nil {
// Already started.
return nil
}
sw.doneCh = make(chan error, 1)
startedCh := make(chan error, 1)
go sw.run(startedCh, sw.doneCh)
if err := <-startedCh; err != nil {
return err
}
// Signal the window to unsubscribe from session notifications
// and shut down gracefully when the sessionWatcher is stopped.
context.AfterFunc(sw.ctx, func() {
sendMessage(sw.hWnd, _WM_CLOSE, 0, 0)
})
return nil
}
func (sw *sessionWatcher) run(started, done chan<- error) {
runtime.LockOSThread()
defer func() {
runtime.UnlockOSThread()
close(done)
}()
err := sw.createMessageWindow()
started <- err
if err != nil {
return
}
pumpThreadMessages()
}
// Stop stops the session watcher and waits for it to exit.
func (sw *sessionWatcher) Stop() error {
sw.ctxCancel()
sw.mu.Lock()
doneCh := sw.doneCh
sw.doneCh = nil
sw.mu.Unlock()
if doneCh != nil {
return <-doneCh
}
return nil
}
const watcherWindowClassName = "Tailscale-SessionManager"
var watcherWindowClassName16 = sync.OnceValue(func() *uint16 {
return must.Get(syscall.UTF16PtrFromString(watcherWindowClassName))
})
var registerSessionManagerWindowClass = sync.OnceValue(func() error {
var hInst windows.Handle
if err := windows.GetModuleHandleEx(0, nil, &hInst); err != nil {
return fmt.Errorf("GetModuleHandle: %w", err)
}
wc := _WNDCLASSEX{
CbSize: uint32(unsafe.Sizeof(_WNDCLASSEX{})),
HInstance: hInst,
LpfnWndProc: syscall.NewCallback(sessionWatcherWndProc),
LpszClassName: watcherWindowClassName16(),
}
if _, err := registerClassEx(&wc); err != nil {
return fmt.Errorf("RegisterClassEx(%q): %w", watcherWindowClassName, err)
}
return nil
})
func (sw *sessionWatcher) createMessageWindow() error {
if err := registerSessionManagerWindowClass(); err != nil {
return err
}
_, err := createWindowEx(
0, // dwExStyle
watcherWindowClassName16(), // lpClassName
nil, // lpWindowName
0, // dwStyle
0, // x
0, // y
0, // nWidth
0, // nHeight
_HWND_MESSAGE, // hWndParent; message-only window
0, // hMenu
0, // hInstance
unsafe.Pointer(sw), // lpParam
)
if err != nil {
return fmt.Errorf("CreateWindowEx: %w", err)
}
return nil
}
func (sw *sessionWatcher) wndProc(hWnd windows.HWND, msg uint32, wParam, lParam uintptr) (result uintptr) {
switch msg {
case _WM_CREATE:
err := registerSessionNotification(_WTS_CURRENT_SERVER_HANDLE, hWnd, _NOTIFY_FOR_ALL_SESSIONS)
if err != nil {
sw.logf("[unexpected] failed to register for session notifications: %v", err)
return ^uintptr(0)
}
sw.logf("registered for session notifications")
case _WM_WTSSESSION_CHANGE:
sw.handler(SessionID(lParam), uint32(wParam))
return 0
case _WM_CLOSE:
if err := destroyWindow(hWnd); err != nil {
sw.logf("[unexpected] failed to destroy window: %v", err)
}
return 0
case _WM_DESTROY:
err := unregisterSessionNotification(_WTS_CURRENT_SERVER_HANDLE, hWnd)
if err != nil {
sw.logf("[unexpected] failed to unregister session notifications callback: %v", err)
}
sw.logf("unregistered from session notifications")
return 0
case _WM_NCDESTROY:
sw.hWnd = 0
postQuitMessage(0) // quit the message loop for this thread
}
return defWindowProc(hWnd, msg, wParam, lParam)
}
func (sw *sessionWatcher) setHandle(hwnd windows.HWND) error {
sw.hWnd = hwnd
setLastError(0)
_, err := setWindowLongPtr(sw.hWnd, _GWLP_USERDATA, uintptr(unsafe.Pointer(sw)))
return err // may be nil or non-nil
}
func sessionWatcherByHandle(hwnd windows.HWND) *sessionWatcher {
val, _ := getWindowLongPtr(hwnd, _GWLP_USERDATA)
return (*sessionWatcher)(unsafe.Pointer(val))
}
func sessionWatcherWndProc(hWnd windows.HWND, msg uint32, wParam, lParam uintptr) (result uintptr) {
if msg == _WM_NCCREATE {
cs := (*_CREATESTRUCT)(unsafe.Pointer(lParam))
sw := (*sessionWatcher)(unsafe.Pointer(cs.CreateParams))
if sw == nil {
return 0
}
if err := sw.setHandle(hWnd); err != nil {
return 0
}
return defWindowProc(hWnd, msg, wParam, lParam)
}
if sw := sessionWatcherByHandle(hWnd); sw != nil {
return sw.wndProc(hWnd, msg, wParam, lParam)
}
return defWindowProc(hWnd, msg, wParam, lParam)
}
func pumpThreadMessages() {
var msg _MSG
for getMessage(&msg, 0, 0, 0) != 0 {
translateMessage(&msg)
dispatchMessage(&msg)
}
}
// waitUntilWTSReady waits until the Windows Terminal Services (WTS) is ready.
// This is necessary because the WTS API functions may fail if called before
// the WTS is ready.
//
// https://web.archive.org/web/20250207011738/https://learn.microsoft.com/en-us/windows/win32/api/wtsapi32/nf-wtsapi32-wtsregistersessionnotificationex
func waitUntilWTSReady(ctx context.Context) error {
eventName16, err := windows.UTF16PtrFromString(`Global\TermSrvReadyEvent`)
if err != nil {
return err
}
event, err := windows.OpenEvent(windows.SYNCHRONIZE, false, eventName16)
if err != nil {
return err
}
defer windows.CloseHandle(event)
return waitForContextOrHandle(ctx, event)
}
// waitForContextOrHandle waits for either the context to be done or a handle to be signaled.
func waitForContextOrHandle(ctx context.Context, handle windows.Handle) error {
contextDoneEvent, cleanup, err := channelToEvent(ctx.Done())
if err != nil {
return err
}
defer cleanup()
handles := []windows.Handle{contextDoneEvent, handle}
waitCode, err := windows.WaitForMultipleObjects(handles, false, windows.INFINITE)
if err != nil {
return err
}
waitCode -= windows.WAIT_OBJECT_0
if waitCode == 0 { // contextDoneEvent
return ctx.Err()
}
return nil
}
// channelToEvent returns an auto-reset event that is set when the channel
// becomes receivable, including when the channel is closed.
func channelToEvent[T any](c <-chan T) (evt windows.Handle, cleanup func(), err error) {
evt, err = windows.CreateEvent(nil, 0, 0, nil)
if err != nil {
return 0, nil, err
}
cancel := make(chan struct{})
go func() {
select {
case <-cancel:
return
case <-c:
}
windows.SetEvent(evt)
}()
cleanup = func() {
close(cancel)
windows.CloseHandle(evt)
}
return evt, cleanup, nil
}
type _WNDCLASSEX struct {
CbSize uint32
Style uint32
LpfnWndProc uintptr
CbClsExtra int32
CbWndExtra int32
HInstance windows.Handle
HIcon windows.Handle
HCursor windows.Handle
HbrBackground windows.Handle
LpszMenuName *uint16
LpszClassName *uint16
HIconSm windows.Handle
}
type _CREATESTRUCT struct {
CreateParams uintptr
Instance windows.Handle
Menu windows.Handle
Parent windows.HWND
Cy int32
Cx int32
Y int32
X int32
Style int32
Name *uint16
ClassName *uint16
ExStyle uint32
}
type _POINT struct {
X, Y int32
}
type _MSG struct {
HWnd windows.HWND
Message uint32
WParam uintptr
LParam uintptr
Time uint32
Pt _POINT
}
const (
_WM_CREATE = 1
_WM_DESTROY = 2
_WM_CLOSE = 16
_WM_NCCREATE = 129
_WM_QUIT = 18
_WM_NCDESTROY = 130
// _WM_WTSSESSION_CHANGE is a message sent to windows that have registered
// for session change notifications, informing them of changes in session state.
//
// https://web.archive.org/web/20250207012421/https://learn.microsoft.com/en-us/windows/win32/termserv/wm-wtssession-change
_WM_WTSSESSION_CHANGE = 0x02B1
)
const _GWLP_USERDATA = -21
const _HWND_MESSAGE = ^windows.HWND(2)
// _NOTIFY_FOR_ALL_SESSIONS indicates that the window should receive
// session change notifications for all sessions on the specified server.
const _NOTIFY_FOR_ALL_SESSIONS = 1
// _WTS_CURRENT_SERVER_HANDLE indicates that the window should receive
// session change notifications for the host itself rather than a remote server.
const _WTS_CURRENT_SERVER_HANDLE = windows.Handle(0)
// _WTS_CONNECTSTATE_CLASS represents the connection state of a session.
//
// https://web.archive.org/web/20250206082427/https://learn.microsoft.com/en-us/windows/win32/api/wtsapi32/ne-wtsapi32-wts_connectstate_class
type _WTS_CONNECTSTATE_CLASS int32
// ToSessionStatus converts cs to a [SessionStatus].
func (cs _WTS_CONNECTSTATE_CLASS) ToSessionStatus() SessionStatus {
switch cs {
case windows.WTSActive:
return ForegroundSession
case windows.WTSDisconnected:
return BackgroundSession
default:
// The session does not exist as far as we're concerned.
return ClosedSession
}
}

159
vendor/tailscale.com/ipn/desktop/zsyscall_windows.go generated vendored Normal file
View File

@@ -0,0 +1,159 @@
// Code generated by 'go generate'; DO NOT EDIT.
package desktop
import (
"syscall"
"unsafe"
"golang.org/x/sys/windows"
)
var _ unsafe.Pointer
// Do the interface allocations only once for common
// Errno values.
const (
errnoERROR_IO_PENDING = 997
)
var (
errERROR_IO_PENDING error = syscall.Errno(errnoERROR_IO_PENDING)
errERROR_EINVAL error = syscall.EINVAL
)
// errnoErr returns common boxed Errno values, to prevent
// allocations at runtime.
func errnoErr(e syscall.Errno) error {
switch e {
case 0:
return errERROR_EINVAL
case errnoERROR_IO_PENDING:
return errERROR_IO_PENDING
}
// TODO: add more here, after collecting data on the common
// error values see on Windows. (perhaps when running
// all.bat?)
return e
}
var (
modkernel32 = windows.NewLazySystemDLL("kernel32.dll")
moduser32 = windows.NewLazySystemDLL("user32.dll")
modwtsapi32 = windows.NewLazySystemDLL("wtsapi32.dll")
procSetLastError = modkernel32.NewProc("SetLastError")
procCreateWindowExW = moduser32.NewProc("CreateWindowExW")
procDefWindowProcW = moduser32.NewProc("DefWindowProcW")
procDestroyWindow = moduser32.NewProc("DestroyWindow")
procDispatchMessageW = moduser32.NewProc("DispatchMessageW")
procGetMessageW = moduser32.NewProc("GetMessageW")
procGetWindowLongPtrW = moduser32.NewProc("GetWindowLongPtrW")
procPostQuitMessage = moduser32.NewProc("PostQuitMessage")
procRegisterClassExW = moduser32.NewProc("RegisterClassExW")
procSendMessageW = moduser32.NewProc("SendMessageW")
procSetWindowLongPtrW = moduser32.NewProc("SetWindowLongPtrW")
procTranslateMessage = moduser32.NewProc("TranslateMessage")
procWTSRegisterSessionNotificationEx = modwtsapi32.NewProc("WTSRegisterSessionNotificationEx")
procWTSUnRegisterSessionNotificationEx = modwtsapi32.NewProc("WTSUnRegisterSessionNotificationEx")
)
func setLastError(dwErrorCode uint32) {
syscall.Syscall(procSetLastError.Addr(), 1, uintptr(dwErrorCode), 0, 0)
return
}
func createWindowEx(dwExStyle uint32, lpClassName *uint16, lpWindowName *uint16, dwStyle uint32, x int32, y int32, nWidth int32, nHeight int32, hWndParent windows.HWND, hMenu windows.Handle, hInstance windows.Handle, lpParam unsafe.Pointer) (hWnd windows.HWND, err error) {
r0, _, e1 := syscall.Syscall12(procCreateWindowExW.Addr(), 12, uintptr(dwExStyle), uintptr(unsafe.Pointer(lpClassName)), uintptr(unsafe.Pointer(lpWindowName)), uintptr(dwStyle), uintptr(x), uintptr(y), uintptr(nWidth), uintptr(nHeight), uintptr(hWndParent), uintptr(hMenu), uintptr(hInstance), uintptr(lpParam))
hWnd = windows.HWND(r0)
if hWnd == 0 {
err = errnoErr(e1)
}
return
}
func defWindowProc(hwnd windows.HWND, msg uint32, wparam uintptr, lparam uintptr) (res uintptr) {
r0, _, _ := syscall.Syscall6(procDefWindowProcW.Addr(), 4, uintptr(hwnd), uintptr(msg), uintptr(wparam), uintptr(lparam), 0, 0)
res = uintptr(r0)
return
}
func destroyWindow(hwnd windows.HWND) (err error) {
r1, _, e1 := syscall.Syscall(procDestroyWindow.Addr(), 1, uintptr(hwnd), 0, 0)
if int32(r1) == 0 {
err = errnoErr(e1)
}
return
}
func dispatchMessage(lpMsg *_MSG) (res uintptr) {
r0, _, _ := syscall.Syscall(procDispatchMessageW.Addr(), 1, uintptr(unsafe.Pointer(lpMsg)), 0, 0)
res = uintptr(r0)
return
}
func getMessage(lpMsg *_MSG, hwnd windows.HWND, msgMin uint32, msgMax uint32) (ret int32) {
r0, _, _ := syscall.Syscall6(procGetMessageW.Addr(), 4, uintptr(unsafe.Pointer(lpMsg)), uintptr(hwnd), uintptr(msgMin), uintptr(msgMax), 0, 0)
ret = int32(r0)
return
}
func getWindowLongPtr(hwnd windows.HWND, index int32) (res uintptr, err error) {
r0, _, e1 := syscall.Syscall(procGetWindowLongPtrW.Addr(), 2, uintptr(hwnd), uintptr(index), 0)
res = uintptr(r0)
if res == 0 && e1 != 0 {
err = errnoErr(e1)
}
return
}
func postQuitMessage(exitCode int32) {
syscall.Syscall(procPostQuitMessage.Addr(), 1, uintptr(exitCode), 0, 0)
return
}
func registerClassEx(windowClass *_WNDCLASSEX) (atom uint16, err error) {
r0, _, e1 := syscall.Syscall(procRegisterClassExW.Addr(), 1, uintptr(unsafe.Pointer(windowClass)), 0, 0)
atom = uint16(r0)
if atom == 0 {
err = errnoErr(e1)
}
return
}
func sendMessage(hwnd windows.HWND, msg uint32, wparam uintptr, lparam uintptr) (res uintptr) {
r0, _, _ := syscall.Syscall6(procSendMessageW.Addr(), 4, uintptr(hwnd), uintptr(msg), uintptr(wparam), uintptr(lparam), 0, 0)
res = uintptr(r0)
return
}
func setWindowLongPtr(hwnd windows.HWND, index int32, newLong uintptr) (res uintptr, err error) {
r0, _, e1 := syscall.Syscall(procSetWindowLongPtrW.Addr(), 3, uintptr(hwnd), uintptr(index), uintptr(newLong))
res = uintptr(r0)
if res == 0 && e1 != 0 {
err = errnoErr(e1)
}
return
}
func translateMessage(lpMsg *_MSG) (res bool) {
r0, _, _ := syscall.Syscall(procTranslateMessage.Addr(), 1, uintptr(unsafe.Pointer(lpMsg)), 0, 0)
res = r0 != 0
return
}
func registerSessionNotification(hServer windows.Handle, hwnd windows.HWND, flags uint32) (err error) {
r1, _, e1 := syscall.Syscall(procWTSRegisterSessionNotificationEx.Addr(), 3, uintptr(hServer), uintptr(hwnd), uintptr(flags))
if int32(r1) == 0 {
err = errnoErr(e1)
}
return
}
func unregisterSessionNotification(hServer windows.Handle, hwnd windows.HWND) (err error) {
r1, _, e1 := syscall.Syscall(procWTSUnRegisterSessionNotificationEx.Addr(), 2, uintptr(hServer), uintptr(hwnd), 0)
if int32(r1) == 0 {
err = errnoErr(e1)
}
return
}

2
vendor/tailscale.com/ipn/doc.go generated vendored
View File

@@ -1,7 +1,7 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:generate go run tailscale.com/cmd/viewer -type=Prefs,ServeConfig,TCPPortHandler,HTTPHandler,WebServerConfig
//go:generate go run tailscale.com/cmd/viewer -type=LoginProfile,Prefs,ServeConfig,ServiceConfig,TCPPortHandler,HTTPHandler,WebServerConfig
// Package ipn implements the interactions between the Tailscale cloud
// control plane and the local network stack.

View File

@@ -17,6 +17,29 @@ import (
"tailscale.com/types/ptr"
)
// Clone makes a deep copy of LoginProfile.
// The result aliases no memory with the original.
func (src *LoginProfile) Clone() *LoginProfile {
if src == nil {
return nil
}
dst := new(LoginProfile)
*dst = *src
return dst
}
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _LoginProfileCloneNeedsRegeneration = LoginProfile(struct {
ID ProfileID
Name string
NetworkProfile NetworkProfile
Key StateKey
UserProfile tailcfg.UserProfile
NodeID tailcfg.StableNodeID
LocalUserID WindowsUserID
ControlURL string
}{})
// Clone makes a deep copy of Prefs.
// The result aliases no memory with the original.
func (src *Prefs) Clone() *Prefs {
@@ -27,6 +50,7 @@ func (src *Prefs) Clone() *Prefs {
*dst = *src
dst.AdvertiseTags = append(src.AdvertiseTags[:0:0], src.AdvertiseTags...)
dst.AdvertiseRoutes = append(src.AdvertiseRoutes[:0:0], src.AdvertiseRoutes...)
dst.AdvertiseServices = append(src.AdvertiseServices[:0:0], src.AdvertiseServices...)
if src.DriveShares != nil {
dst.DriveShares = make([]*drive.Share, len(src.DriveShares))
for i := range dst.DriveShares {
@@ -61,6 +85,7 @@ var _PrefsCloneNeedsRegeneration = Prefs(struct {
ForceDaemon bool
Egg bool
AdvertiseRoutes []netip.Prefix
AdvertiseServices []string
NoSNAT bool
NoStatefulFiltering opt.Bool
NetfilterMode preftype.NetfilterMode
@@ -103,6 +128,16 @@ func (src *ServeConfig) Clone() *ServeConfig {
}
}
}
if dst.Services != nil {
dst.Services = map[tailcfg.ServiceName]*ServiceConfig{}
for k, v := range src.Services {
if v == nil {
dst.Services[k] = nil
} else {
dst.Services[k] = v.Clone()
}
}
}
dst.AllowFunnel = maps.Clone(src.AllowFunnel)
if dst.Foreground != nil {
dst.Foreground = map[string]*ServeConfig{}
@@ -121,11 +156,50 @@ func (src *ServeConfig) Clone() *ServeConfig {
var _ServeConfigCloneNeedsRegeneration = ServeConfig(struct {
TCP map[uint16]*TCPPortHandler
Web map[HostPort]*WebServerConfig
Services map[tailcfg.ServiceName]*ServiceConfig
AllowFunnel map[HostPort]bool
Foreground map[string]*ServeConfig
ETag string
}{})
// Clone makes a deep copy of ServiceConfig.
// The result aliases no memory with the original.
func (src *ServiceConfig) Clone() *ServiceConfig {
if src == nil {
return nil
}
dst := new(ServiceConfig)
*dst = *src
if dst.TCP != nil {
dst.TCP = map[uint16]*TCPPortHandler{}
for k, v := range src.TCP {
if v == nil {
dst.TCP[k] = nil
} else {
dst.TCP[k] = ptr.To(*v)
}
}
}
if dst.Web != nil {
dst.Web = map[HostPort]*WebServerConfig{}
for k, v := range src.Web {
if v == nil {
dst.Web[k] = nil
} else {
dst.Web[k] = v.Clone()
}
}
}
return dst
}
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _ServiceConfigCloneNeedsRegeneration = ServiceConfig(struct {
TCP map[uint16]*TCPPortHandler
Web map[HostPort]*WebServerConfig
Tun bool
}{})
// Clone makes a deep copy of TCPPortHandler.
// The result aliases no memory with the original.
func (src *TCPPortHandler) Clone() *TCPPortHandler {

164
vendor/tailscale.com/ipn/ipn_view.go generated vendored
View File

@@ -18,9 +18,75 @@ import (
"tailscale.com/types/views"
)
//go:generate go run tailscale.com/cmd/cloner -clonefunc=false -type=Prefs,ServeConfig,TCPPortHandler,HTTPHandler,WebServerConfig
//go:generate go run tailscale.com/cmd/cloner -clonefunc=false -type=LoginProfile,Prefs,ServeConfig,ServiceConfig,TCPPortHandler,HTTPHandler,WebServerConfig
// View returns a readonly view of Prefs.
// View returns a read-only view of LoginProfile.
func (p *LoginProfile) View() LoginProfileView {
return LoginProfileView{ж: p}
}
// LoginProfileView provides a read-only view over LoginProfile.
//
// Its methods should only be called if `Valid()` returns true.
type LoginProfileView 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.
ж *LoginProfile
}
// Valid reports whether v's underlying value is non-nil.
func (v LoginProfileView) Valid() bool { return v.ж != nil }
// AsStruct returns a clone of the underlying value which aliases no memory with
// the original.
func (v LoginProfileView) AsStruct() *LoginProfile {
if v.ж == nil {
return nil
}
return v.ж.Clone()
}
func (v LoginProfileView) MarshalJSON() ([]byte, error) { return json.Marshal(v.ж) }
func (v *LoginProfileView) UnmarshalJSON(b []byte) error {
if v.ж != nil {
return errors.New("already initialized")
}
if len(b) == 0 {
return nil
}
var x LoginProfile
if err := json.Unmarshal(b, &x); err != nil {
return err
}
v.ж = &x
return nil
}
func (v LoginProfileView) ID() ProfileID { return v.ж.ID }
func (v LoginProfileView) Name() string { return v.ж.Name }
func (v LoginProfileView) NetworkProfile() NetworkProfile { return v.ж.NetworkProfile }
func (v LoginProfileView) Key() StateKey { return v.ж.Key }
func (v LoginProfileView) UserProfile() tailcfg.UserProfile { return v.ж.UserProfile }
func (v LoginProfileView) NodeID() tailcfg.StableNodeID { return v.ж.NodeID }
func (v LoginProfileView) LocalUserID() WindowsUserID { return v.ж.LocalUserID }
func (v LoginProfileView) ControlURL() string { return v.ж.ControlURL }
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _LoginProfileViewNeedsRegeneration = LoginProfile(struct {
ID ProfileID
Name string
NetworkProfile NetworkProfile
Key StateKey
UserProfile tailcfg.UserProfile
NodeID tailcfg.StableNodeID
LocalUserID WindowsUserID
ControlURL string
}{})
// View returns a read-only view of Prefs.
func (p *Prefs) View() PrefsView {
return PrefsView{ж: p}
}
@@ -36,7 +102,7 @@ type PrefsView struct {
ж *Prefs
}
// Valid reports whether underlying value is non-nil.
// Valid reports whether v's underlying value is non-nil.
func (v PrefsView) Valid() bool { return v.ж != nil }
// AsStruct returns a clone of the underlying value which aliases no memory with
@@ -85,6 +151,9 @@ func (v PrefsView) Egg() bool { return v.ж.Eg
func (v PrefsView) AdvertiseRoutes() views.Slice[netip.Prefix] {
return views.SliceOf(v.ж.AdvertiseRoutes)
}
func (v PrefsView) AdvertiseServices() views.Slice[string] {
return views.SliceOf(v.ж.AdvertiseServices)
}
func (v PrefsView) NoSNAT() bool { return v.ж.NoSNAT }
func (v PrefsView) NoStatefulFiltering() opt.Bool { return v.ж.NoStatefulFiltering }
func (v PrefsView) NetfilterMode() preftype.NetfilterMode { return v.ж.NetfilterMode }
@@ -120,6 +189,7 @@ var _PrefsViewNeedsRegeneration = Prefs(struct {
ForceDaemon bool
Egg bool
AdvertiseRoutes []netip.Prefix
AdvertiseServices []string
NoSNAT bool
NoStatefulFiltering opt.Bool
NetfilterMode preftype.NetfilterMode
@@ -134,7 +204,7 @@ var _PrefsViewNeedsRegeneration = Prefs(struct {
Persist *persist.Persist
}{})
// View returns a readonly view of ServeConfig.
// View returns a read-only view of ServeConfig.
func (p *ServeConfig) View() ServeConfigView {
return ServeConfigView{ж: p}
}
@@ -150,7 +220,7 @@ type ServeConfigView struct {
ж *ServeConfig
}
// Valid reports whether underlying value is non-nil.
// Valid reports whether v's underlying value is non-nil.
func (v ServeConfigView) Valid() bool { return v.ж != nil }
// AsStruct returns a clone of the underlying value which aliases no memory with
@@ -191,6 +261,12 @@ func (v ServeConfigView) Web() views.MapFn[HostPort, *WebServerConfig, WebServer
})
}
func (v ServeConfigView) Services() views.MapFn[tailcfg.ServiceName, *ServiceConfig, ServiceConfigView] {
return views.MapFnOf(v.ж.Services, func(t *ServiceConfig) ServiceConfigView {
return t.View()
})
}
func (v ServeConfigView) AllowFunnel() views.Map[HostPort, bool] {
return views.MapOf(v.ж.AllowFunnel)
}
@@ -206,12 +282,78 @@ func (v ServeConfigView) ETag() string { return v.ж.ETag }
var _ServeConfigViewNeedsRegeneration = ServeConfig(struct {
TCP map[uint16]*TCPPortHandler
Web map[HostPort]*WebServerConfig
Services map[tailcfg.ServiceName]*ServiceConfig
AllowFunnel map[HostPort]bool
Foreground map[string]*ServeConfig
ETag string
}{})
// View returns a readonly view of TCPPortHandler.
// View returns a read-only view of ServiceConfig.
func (p *ServiceConfig) View() ServiceConfigView {
return ServiceConfigView{ж: p}
}
// ServiceConfigView provides a read-only view over ServiceConfig.
//
// Its methods should only be called if `Valid()` returns true.
type ServiceConfigView 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.
ж *ServiceConfig
}
// Valid reports whether v's underlying value is non-nil.
func (v ServiceConfigView) Valid() bool { return v.ж != nil }
// AsStruct returns a clone of the underlying value which aliases no memory with
// the original.
func (v ServiceConfigView) AsStruct() *ServiceConfig {
if v.ж == nil {
return nil
}
return v.ж.Clone()
}
func (v ServiceConfigView) MarshalJSON() ([]byte, error) { return json.Marshal(v.ж) }
func (v *ServiceConfigView) UnmarshalJSON(b []byte) error {
if v.ж != nil {
return errors.New("already initialized")
}
if len(b) == 0 {
return nil
}
var x ServiceConfig
if err := json.Unmarshal(b, &x); err != nil {
return err
}
v.ж = &x
return nil
}
func (v ServiceConfigView) TCP() views.MapFn[uint16, *TCPPortHandler, TCPPortHandlerView] {
return views.MapFnOf(v.ж.TCP, func(t *TCPPortHandler) TCPPortHandlerView {
return t.View()
})
}
func (v ServiceConfigView) Web() views.MapFn[HostPort, *WebServerConfig, WebServerConfigView] {
return views.MapFnOf(v.ж.Web, func(t *WebServerConfig) WebServerConfigView {
return t.View()
})
}
func (v ServiceConfigView) Tun() bool { return v.ж.Tun }
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _ServiceConfigViewNeedsRegeneration = ServiceConfig(struct {
TCP map[uint16]*TCPPortHandler
Web map[HostPort]*WebServerConfig
Tun bool
}{})
// View returns a read-only view of TCPPortHandler.
func (p *TCPPortHandler) View() TCPPortHandlerView {
return TCPPortHandlerView{ж: p}
}
@@ -227,7 +369,7 @@ type TCPPortHandlerView struct {
ж *TCPPortHandler
}
// Valid reports whether underlying value is non-nil.
// Valid reports whether v's underlying value is non-nil.
func (v TCPPortHandlerView) Valid() bool { return v.ж != nil }
// AsStruct returns a clone of the underlying value which aliases no memory with
@@ -269,7 +411,7 @@ var _TCPPortHandlerViewNeedsRegeneration = TCPPortHandler(struct {
TerminateTLS string
}{})
// View returns a readonly view of HTTPHandler.
// View returns a read-only view of HTTPHandler.
func (p *HTTPHandler) View() HTTPHandlerView {
return HTTPHandlerView{ж: p}
}
@@ -285,7 +427,7 @@ type HTTPHandlerView struct {
ж *HTTPHandler
}
// Valid reports whether underlying value is non-nil.
// Valid reports whether v's underlying value is non-nil.
func (v HTTPHandlerView) Valid() bool { return v.ж != nil }
// AsStruct returns a clone of the underlying value which aliases no memory with
@@ -325,7 +467,7 @@ var _HTTPHandlerViewNeedsRegeneration = HTTPHandler(struct {
Text string
}{})
// View returns a readonly view of WebServerConfig.
// View returns a read-only view of WebServerConfig.
func (p *WebServerConfig) View() WebServerConfigView {
return WebServerConfigView{ж: p}
}
@@ -341,7 +483,7 @@ type WebServerConfigView struct {
ж *WebServerConfig
}
// Valid reports whether underlying value is non-nil.
// Valid reports whether v's underlying value is non-nil.
func (v WebServerConfigView) Valid() bool { return v.ж != nil }
// AsStruct returns a clone of the underlying value which aliases no memory with

17
vendor/tailscale.com/ipn/ipnauth/access.go generated vendored Normal file
View File

@@ -0,0 +1,17 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package ipnauth
// ProfileAccess is a bitmask representing the requested, required, or granted
// access rights to an [ipn.LoginProfile].
//
// It is not to be written to disk or transmitted over the network in its integer form,
// but rather serialized to a string or other format if ever needed.
type ProfileAccess uint
// Define access rights that might be granted or denied on a per-profile basis.
const (
// Disconnect is required to disconnect (or switch from) a Tailscale profile.
Disconnect = ProfileAccess(1 << iota)
)

View File

@@ -4,9 +4,18 @@
package ipnauth
import (
"context"
"encoding/json"
"fmt"
"tailscale.com/client/tailscale/apitype"
"tailscale.com/ipn"
"tailscale.com/tailcfg"
)
// AuditLogFunc is any function that can be used to log audit actions performed by an [Actor].
type AuditLogFunc func(action tailcfg.ClientAuditAction, details string) error
// Actor is any actor using the [ipnlocal.LocalBackend].
//
// It typically represents a specific OS user, indicating that an operation
@@ -20,6 +29,22 @@ type Actor interface {
// Username returns the user name associated with the receiver,
// or "" if the actor does not represent a specific user.
Username() (string, error)
// ClientID returns a non-zero ClientID and true if the actor represents
// a connected LocalAPI client. Otherwise, it returns a zero value and false.
ClientID() (_ ClientID, ok bool)
// Context returns the context associated with the actor.
// It carries additional information about the actor
// and is canceled when the actor is done.
Context() context.Context
// CheckProfileAccess checks whether the actor has the necessary access rights
// to perform a given action on the specified Tailscale profile.
// It returns an error if access is denied.
//
// If the auditLogger is non-nil, it is used to write details about the action
// to the audit log when required by the policy.
CheckProfileAccess(profile ipn.LoginProfileView, requestedAccess ProfileAccess, auditLogFn AuditLogFunc) error
// IsLocalSystem reports whether the actor is the Windows' Local System account.
//
@@ -45,3 +70,65 @@ type ActorCloser interface {
// Close releases resources associated with the receiver.
Close() error
}
// ClientID is an opaque, comparable value used to identify a connected LocalAPI
// client, such as a connected Tailscale GUI or CLI. It does not necessarily
// correspond to the same [net.Conn] or any physical session.
//
// Its zero value is valid, but does not represent a specific connected client.
type ClientID struct {
v any
}
// NoClientID is the zero value of [ClientID].
var NoClientID ClientID
// ClientIDFrom returns a new [ClientID] derived from the specified value.
// ClientIDs derived from equal values are equal.
func ClientIDFrom[T comparable](v T) ClientID {
return ClientID{v}
}
// String implements [fmt.Stringer].
func (id ClientID) String() string {
if id.v == nil {
return "(none)"
}
return fmt.Sprint(id.v)
}
// MarshalJSON implements [json.Marshaler].
// It is primarily used for testing.
func (id ClientID) MarshalJSON() ([]byte, error) {
return json.Marshal(id.v)
}
// UnmarshalJSON implements [json.Unmarshaler].
// It is primarily used for testing.
func (id *ClientID) UnmarshalJSON(b []byte) error {
return json.Unmarshal(b, &id.v)
}
type actorWithRequestReason struct {
Actor
ctx context.Context
}
// WithRequestReason returns an [Actor] that wraps the given actor and
// carries the specified request reason in its context.
func WithRequestReason(actor Actor, requestReason string) Actor {
ctx := apitype.RequestReasonKey.WithValue(actor.Context(), requestReason)
return &actorWithRequestReason{Actor: actor, ctx: ctx}
}
// Context implements [Actor].
func (a *actorWithRequestReason) Context() context.Context { return a.ctx }
type withoutCloseActor struct{ Actor }
// WithoutClose returns an [Actor] that does not expose the [ActorCloser] interface.
// In other words, _, ok := WithoutClose(actor).(ActorCloser) will always be false,
// even if the original actor implements [ActorCloser].
func WithoutClose(actor Actor) Actor {
return withoutCloseActor{actor}
}

102
vendor/tailscale.com/ipn/ipnauth/actor_windows.go generated vendored Normal file
View File

@@ -0,0 +1,102 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package ipnauth
import (
"context"
"errors"
"golang.org/x/sys/windows"
"tailscale.com/ipn"
"tailscale.com/types/lazy"
)
// WindowsActor implements [Actor].
var _ Actor = (*WindowsActor)(nil)
// WindowsActor represents a logged in Windows user.
type WindowsActor struct {
ctx context.Context
cancelCtx context.CancelFunc
token WindowsToken
uid ipn.WindowsUserID
username lazy.SyncValue[string]
}
// NewWindowsActorWithToken returns a new [WindowsActor] for the user
// represented by the given [windows.Token].
// It takes ownership of the token.
func NewWindowsActorWithToken(t windows.Token) (_ *WindowsActor, err error) {
tok := newToken(t)
uid, err := tok.UID()
if err != nil {
t.Close()
return nil, err
}
ctx, cancelCtx := context.WithCancel(context.Background())
return &WindowsActor{ctx: ctx, cancelCtx: cancelCtx, token: tok, uid: uid}, nil
}
// UserID implements [Actor].
func (a *WindowsActor) UserID() ipn.WindowsUserID {
return a.uid
}
// Username implements [Actor].
func (a *WindowsActor) Username() (string, error) {
return a.username.GetErr(a.token.Username)
}
// ClientID implements [Actor].
func (a *WindowsActor) ClientID() (_ ClientID, ok bool) {
// TODO(nickkhyl): assign and return a client ID when the actor
// represents a connected LocalAPI client.
return NoClientID, false
}
// Context implements [Actor].
func (a *WindowsActor) Context() context.Context {
return a.ctx
}
// CheckProfileAccess implements [Actor].
func (a *WindowsActor) CheckProfileAccess(profile ipn.LoginProfileView, _ ProfileAccess, _ AuditLogFunc) error {
if profile.LocalUserID() != a.UserID() {
// TODO(nickkhyl): return errors of more specific types and have them
// translated to the appropriate HTTP status codes in the API handler.
return errors.New("the target profile does not belong to the user")
}
return nil
}
// IsLocalSystem implements [Actor].
//
// Deprecated: this method exists for compatibility with the current (as of 2025-02-06)
// permission model and will be removed as we progress on tailscale/corp#18342.
func (a *WindowsActor) IsLocalSystem() bool {
// https://web.archive.org/web/2024/https://learn.microsoft.com/en-us/windows-server/identity/ad-ds/manage/understand-security-identifiers
const systemUID = ipn.WindowsUserID("S-1-5-18")
return a.uid == systemUID
}
// IsLocalAdmin implements [Actor].
//
// Deprecated: this method exists for compatibility with the current (as of 2025-02-06)
// permission model and will be removed as we progress on tailscale/corp#18342.
func (a *WindowsActor) IsLocalAdmin(operatorUID string) bool {
return a.token.IsElevated()
}
// Close releases resources associated with the actor
// and cancels its context.
func (a *WindowsActor) Close() error {
if a.token != nil {
if err := a.token.Close(); err != nil {
return err
}
a.token = nil
}
a.cancelCtx()
return nil
}

View File

@@ -18,7 +18,9 @@ import (
func GetConnIdentity(_ logger.Logf, c net.Conn) (ci *ConnIdentity, err error) {
ci = &ConnIdentity{conn: c, notWindows: true}
_, ci.isUnixSock = c.(*net.UnixConn)
ci.creds, _ = peercred.Get(c)
if ci.creds, _ = peercred.Get(c); ci.creds != nil {
ci.pid, _ = ci.creds.PID()
}
return ci, nil
}

View File

@@ -36,6 +36,12 @@ type token struct {
t windows.Token
}
func newToken(t windows.Token) *token {
tok := &token{t: t}
runtime.SetFinalizer(tok, func(t *token) { t.Close() })
return tok
}
func (t *token) UID() (ipn.WindowsUserID, error) {
sid, err := t.uid()
if err != nil {
@@ -184,7 +190,5 @@ func (ci *ConnIdentity) WindowsToken() (WindowsToken, error) {
return nil, err
}
result := &token{t: windows.Token(h)}
runtime.SetFinalizer(result, func(t *token) { t.Close() })
return result, nil
return newToken(windows.Token(h)), nil
}

74
vendor/tailscale.com/ipn/ipnauth/policy.go generated vendored Normal file
View File

@@ -0,0 +1,74 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package ipnauth
import (
"errors"
"fmt"
"tailscale.com/client/tailscale/apitype"
"tailscale.com/ipn"
"tailscale.com/tailcfg"
"tailscale.com/util/syspolicy"
)
type actorWithPolicyChecks struct{ Actor }
// WithPolicyChecks returns an [Actor] that wraps the given actor and
// performs additional policy checks on top of the access checks
// implemented by the wrapped actor.
func WithPolicyChecks(actor Actor) Actor {
// TODO(nickkhyl): We should probably exclude the Windows Local System
// account from policy checks as well.
switch actor.(type) {
case unrestricted:
return actor
default:
return &actorWithPolicyChecks{Actor: actor}
}
}
// CheckProfileAccess implements [Actor].
func (a actorWithPolicyChecks) CheckProfileAccess(profile ipn.LoginProfileView, requestedAccess ProfileAccess, auditLogger AuditLogFunc) error {
if err := a.Actor.CheckProfileAccess(profile, requestedAccess, auditLogger); err != nil {
return err
}
requestReason := apitype.RequestReasonKey.Value(a.Context())
return CheckDisconnectPolicy(a.Actor, profile, requestReason, auditLogger)
}
// CheckDisconnectPolicy checks if the policy allows the specified actor to disconnect
// Tailscale with the given optional reason. It returns nil if the operation is allowed,
// or an error if it is not. If auditLogger is non-nil, it is called to log the action
// when required by the policy.
//
// Note: this function only checks the policy and does not check whether the actor has
// the necessary access rights to the device or profile. It is intended to be used by
// [Actor] implementations on platforms where [syspolicy] is supported.
//
// TODO(nickkhyl): unexport it when we move [ipn.Actor] implementations from [ipnserver]
// and corp to this package.
func CheckDisconnectPolicy(actor Actor, profile ipn.LoginProfileView, reason string, auditFn AuditLogFunc) error {
if alwaysOn, _ := syspolicy.GetBoolean(syspolicy.AlwaysOn, false); !alwaysOn {
return nil
}
if allowWithReason, _ := syspolicy.GetBoolean(syspolicy.AlwaysOnOverrideWithReason, false); !allowWithReason {
return errors.New("disconnect not allowed: always-on mode is enabled")
}
if reason == "" {
return errors.New("disconnect not allowed: reason required")
}
if auditFn != nil {
var details string
if username, _ := actor.Username(); username != "" { // best-effort; we don't have it on all platforms
details = fmt.Sprintf("%q is being disconnected by %q: %v", profile.Name(), username, reason)
} else {
details = fmt.Sprintf("%q is being disconnected: %v", profile.Name(), reason)
}
if err := auditFn(tailcfg.AuditNodeDisconnect, details); err != nil {
return err
}
}
return nil
}

51
vendor/tailscale.com/ipn/ipnauth/self.go generated vendored Normal file
View File

@@ -0,0 +1,51 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package ipnauth
import (
"context"
"tailscale.com/ipn"
)
// Self is a caller identity that represents the tailscaled itself and therefore
// has unlimited access.
var Self Actor = unrestricted{}
// unrestricted is an [Actor] that has unlimited access to the currently running
// tailscaled instance. It's typically used for operations performed by tailscaled
// on its own, or upon a request from the control plane, rather on behalf of a user.
type unrestricted struct{}
// UserID implements [Actor].
func (unrestricted) UserID() ipn.WindowsUserID { return "" }
// Username implements [Actor].
func (unrestricted) Username() (string, error) { return "", nil }
// Context implements [Actor].
func (unrestricted) Context() context.Context { return context.Background() }
// ClientID implements [Actor].
// It always returns (NoClientID, false) because the tailscaled itself
// is not a connected LocalAPI client.
func (unrestricted) ClientID() (_ ClientID, ok bool) { return NoClientID, false }
// CheckProfileAccess implements [Actor].
func (unrestricted) CheckProfileAccess(_ ipn.LoginProfileView, _ ProfileAccess, _ AuditLogFunc) error {
// Unrestricted access to all profiles.
return nil
}
// IsLocalSystem implements [Actor].
//
// Deprecated: this method exists for compatibility with the current (as of 2025-01-28)
// permission model and will be removed as we progress on tailscale/corp#18342.
func (unrestricted) IsLocalSystem() bool { return false }
// IsLocalAdmin implements [Actor].
//
// Deprecated: this method exists for compatibility with the current (as of 2025-01-28)
// permission model and will be removed as we progress on tailscale/corp#18342.
func (unrestricted) IsLocalAdmin(operatorUID string) bool { return false }

48
vendor/tailscale.com/ipn/ipnauth/test_actor.go generated vendored Normal file
View File

@@ -0,0 +1,48 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package ipnauth
import (
"cmp"
"context"
"errors"
"tailscale.com/ipn"
)
var _ Actor = (*TestActor)(nil)
// TestActor is an [Actor] used exclusively for testing purposes.
type TestActor struct {
UID ipn.WindowsUserID // OS-specific UID of the user, if the actor represents a local Windows user
Name string // username associated with the actor, or ""
NameErr error // error to be returned by [TestActor.Username]
CID ClientID // non-zero if the actor represents a connected LocalAPI client
Ctx context.Context // context associated with the actor
LocalSystem bool // whether the actor represents the special Local System account on Windows
LocalAdmin bool // whether the actor has local admin access
}
// UserID implements [Actor].
func (a *TestActor) UserID() ipn.WindowsUserID { return a.UID }
// Username implements [Actor].
func (a *TestActor) Username() (string, error) { return a.Name, a.NameErr }
// ClientID implements [Actor].
func (a *TestActor) ClientID() (_ ClientID, ok bool) { return a.CID, a.CID != NoClientID }
// Context implements [Actor].
func (a *TestActor) Context() context.Context { return cmp.Or(a.Ctx, context.Background()) }
// CheckProfileAccess implements [Actor].
func (a *TestActor) CheckProfileAccess(profile ipn.LoginProfileView, _ ProfileAccess, _ AuditLogFunc) error {
return errors.New("profile access denied")
}
// IsLocalSystem implements [Actor].
func (a *TestActor) IsLocalSystem() bool { return a.LocalSystem }
// IsLocalAdmin implements [Actor].
func (a *TestActor) IsLocalAdmin(operatorUID string) bool { return a.LocalAdmin }

160
vendor/tailscale.com/ipn/ipnlocal/bus.go generated vendored Normal file
View File

@@ -0,0 +1,160 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package ipnlocal
import (
"context"
"time"
"tailscale.com/ipn"
"tailscale.com/tstime"
)
type rateLimitingBusSender struct {
fn func(*ipn.Notify) (keepGoing bool)
lastFlush time.Time // last call to fn, or zero value if none
interval time.Duration // 0 to flush immediately; non-zero to rate limit sends
clock tstime.DefaultClock // non-nil for testing
didSendTestHook func() // non-nil for testing
// pending, if non-nil, is the pending notification that we
// haven't sent yet. We own this memory to mutate.
pending *ipn.Notify
// flushTimer is non-nil if the timer is armed.
flushTimer tstime.TimerController // effectively a *time.Timer
flushTimerC <-chan time.Time // ... said ~Timer's C chan
}
func (s *rateLimitingBusSender) close() {
if s.flushTimer != nil {
s.flushTimer.Stop()
}
}
func (s *rateLimitingBusSender) flushChan() <-chan time.Time {
return s.flushTimerC
}
func (s *rateLimitingBusSender) flush() (keepGoing bool) {
if n := s.pending; n != nil {
s.pending = nil
return s.flushNotify(n)
}
return true
}
func (s *rateLimitingBusSender) flushNotify(n *ipn.Notify) (keepGoing bool) {
s.lastFlush = s.clock.Now()
return s.fn(n)
}
// send conditionally sends n to the underlying fn, possibly rate
// limiting it, depending on whether s.interval is set, and whether
// n is a notable notification that the client (typically a GUI) would
// want to act on (render) immediately.
//
// It returns whether the caller should keep looping.
//
// The passed-in memory 'n' is owned by the caller and should
// not be mutated.
func (s *rateLimitingBusSender) send(n *ipn.Notify) (keepGoing bool) {
if s.interval <= 0 {
// No rate limiting case.
return s.fn(n)
}
if isNotableNotify(n) {
// Notable notifications are always sent immediately.
// But first send any boring one that was pending.
// TODO(bradfitz): there might be a boring one pending
// with a NetMap or Engine field that is redundant
// with the new one (n) with NetMap or Engine populated.
// We should clear the pending one's NetMap/Engine in
// that case. Or really, merge the two, but mergeBoringNotifies
// only handles the case of both sides being boring.
// So for now, flush both.
if !s.flush() {
return false
}
return s.flushNotify(n)
}
s.pending = mergeBoringNotifies(s.pending, n)
d := s.clock.Now().Sub(s.lastFlush)
if d > s.interval {
return s.flush()
}
nextFlushIn := s.interval - d
if s.flushTimer == nil {
s.flushTimer, s.flushTimerC = s.clock.NewTimer(nextFlushIn)
} else {
s.flushTimer.Reset(nextFlushIn)
}
return true
}
func (s *rateLimitingBusSender) Run(ctx context.Context, ch <-chan *ipn.Notify) {
for {
select {
case <-ctx.Done():
return
case n, ok := <-ch:
if !ok {
return
}
if !s.send(n) {
return
}
if f := s.didSendTestHook; f != nil {
f()
}
case <-s.flushChan():
if !s.flush() {
return
}
}
}
}
// mergeBoringNotify merges new notify 'src' into possibly-nil 'dst',
// either mutating 'dst' or allocating a new one if 'dst' is nil,
// returning the merged result.
//
// dst and src must both be "boring" (i.e. not notable per isNotifiableNotify).
func mergeBoringNotifies(dst, src *ipn.Notify) *ipn.Notify {
if dst == nil {
dst = &ipn.Notify{Version: src.Version}
}
if src.NetMap != nil {
dst.NetMap = src.NetMap
}
if src.Engine != nil {
dst.Engine = src.Engine
}
return dst
}
// isNotableNotify reports whether n is a "notable" notification that
// should be sent on the IPN bus immediately (e.g. to GUIs) without
// rate limiting it for a few seconds.
//
// It effectively reports whether n contains any field set that's
// not NetMap or Engine.
func isNotableNotify(n *ipn.Notify) bool {
if n == nil {
return false
}
return n.State != nil ||
n.SessionID != "" ||
n.BrowseToURL != nil ||
n.LocalTCPPort != nil ||
n.ClientVersion != nil ||
n.Prefs != nil ||
n.ErrMessage != nil ||
n.LoginFinished != nil ||
!n.DriveShares.IsNil() ||
n.Health != nil ||
len(n.IncomingFiles) > 0 ||
len(n.OutgoingFiles) > 0 ||
n.FilesWaiting != nil
}

View File

@@ -10,19 +10,16 @@ import (
"errors"
"fmt"
"io"
"net"
"net/http"
"os"
"os/exec"
"path"
"path/filepath"
"runtime"
"sort"
"strconv"
"strings"
"time"
"github.com/kortschak/wol"
"tailscale.com/clientupdate"
"tailscale.com/envknob"
"tailscale.com/ipn"
@@ -66,9 +63,6 @@ var c2nHandlers = map[methodAndPath]c2nHandler{
req("GET /update"): handleC2NUpdateGet,
req("POST /update"): handleC2NUpdatePost,
// Wake-on-LAN.
req("POST /wol"): handleC2NWoL,
// Device posture.
req("GET /posture/identity"): handleC2NPostureIdentityGet,
@@ -77,6 +71,21 @@ var c2nHandlers = map[methodAndPath]c2nHandler{
// Linux netfilter.
req("POST /netfilter-kind"): handleC2NSetNetfilterKind,
// VIP services.
req("GET /vip-services"): handleC2NVIPServicesGet,
}
// RegisterC2N registers a new c2n handler for the given pattern.
//
// A pattern is like "GET /foo" (specific to an HTTP method) or "/foo" (all
// methods). It panics if the pattern is already registered.
func RegisterC2N(pattern string, h func(*LocalBackend, http.ResponseWriter, *http.Request)) {
k := req(pattern)
if _, ok := c2nHandlers[k]; ok {
panic(fmt.Sprintf("c2n: duplicate handler for %q", pattern))
}
c2nHandlers[k] = h
}
type c2nHandler func(*LocalBackend, http.ResponseWriter, *http.Request)
@@ -269,6 +278,16 @@ func handleC2NSetNetfilterKind(b *LocalBackend, w http.ResponseWriter, r *http.R
w.WriteHeader(http.StatusNoContent)
}
func handleC2NVIPServicesGet(b *LocalBackend, w http.ResponseWriter, r *http.Request) {
b.logf("c2n: GET /vip-services received")
var res tailcfg.C2NVIPServicesResponse
res.VIPServices = b.VIPServices()
res.ServicesHash = b.vipServiceHash(res.VIPServices)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(res)
}
func handleC2NUpdateGet(b *LocalBackend, w http.ResponseWriter, r *http.Request) {
b.logf("c2n: GET /update received")
@@ -332,12 +351,10 @@ func handleC2NPostureIdentityGet(b *LocalBackend, w http.ResponseWriter, r *http
}
if choice.ShouldEnable(b.Prefs().PostureChecking()) {
sns, err := posture.GetSerialNumbers(b.logf)
res.SerialNumbers, err = posture.GetSerialNumbers(b.logf)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
b.logf("c2n: GetSerialNumbers returned error: %v", err)
}
res.SerialNumbers = sns
// TODO(tailscale/corp#21371, 2024-07-10): once this has landed in a stable release
// and looks good in client metrics, remove this parameter and always report MAC
@@ -352,6 +369,8 @@ func handleC2NPostureIdentityGet(b *LocalBackend, w http.ResponseWriter, r *http
res.PostureDisabled = true
}
b.logf("c2n: posture identity disabled=%v reported %d serials %d hwaddrs", res.PostureDisabled, len(res.SerialNumbers), len(res.IfaceHardwareAddrs))
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(res)
}
@@ -490,55 +509,6 @@ func regularFileExists(path string) bool {
return err == nil && fi.Mode().IsRegular()
}
func handleC2NWoL(b *LocalBackend, w http.ResponseWriter, r *http.Request) {
r.ParseForm()
var macs []net.HardwareAddr
for _, macStr := range r.Form["mac"] {
mac, err := net.ParseMAC(macStr)
if err != nil {
http.Error(w, "bad 'mac' param", http.StatusBadRequest)
return
}
macs = append(macs, mac)
}
var res struct {
SentTo []string
Errors []string
}
st := b.sys.NetMon.Get().InterfaceState()
if st == nil {
res.Errors = append(res.Errors, "no interface state")
writeJSON(w, &res)
return
}
var password []byte // TODO(bradfitz): support? does anything use WoL passwords?
for _, mac := range macs {
for ifName, ips := range st.InterfaceIPs {
for _, ip := range ips {
if ip.Addr().IsLoopback() || ip.Addr().Is6() {
continue
}
local := &net.UDPAddr{
IP: ip.Addr().AsSlice(),
Port: 0,
}
remote := &net.UDPAddr{
IP: net.IPv4bcast,
Port: 0,
}
if err := wol.Wake(mac, password, local, remote); err != nil {
res.Errors = append(res.Errors, err.Error())
} else {
res.SentTo = append(res.SentTo, ifName)
}
break // one per interface is enough
}
}
}
sort.Strings(res.SentTo)
writeJSON(w, &res)
}
// handleC2NTLSCertStatus returns info about the last TLS certificate issued for the
// provided domain. This can be called by the controlplane to clean up DNS TXT
// records when they're no longer needed by LetsEncrypt.

View File

@@ -32,7 +32,6 @@ import (
"sync"
"time"
"github.com/tailscale/golang-x-crypto/acme"
"tailscale.com/atomicfile"
"tailscale.com/envknob"
"tailscale.com/hostinfo"
@@ -40,6 +39,8 @@ import (
"tailscale.com/ipn/ipnstate"
"tailscale.com/ipn/store"
"tailscale.com/ipn/store/mem"
"tailscale.com/net/bakedroots"
"tailscale.com/tempfork/acme"
"tailscale.com/types/logger"
"tailscale.com/util/testenv"
"tailscale.com/version"
@@ -118,6 +119,9 @@ func (b *LocalBackend) GetCertPEMWithValidity(ctx context.Context, domain string
}
if pair, err := getCertPEMCached(cs, domain, now); err == nil {
if envknob.IsCertShareReadOnlyMode() {
return pair, nil
}
// If we got here, we have a valid unexpired cert.
// Check whether we should start an async renewal.
shouldRenew, err := b.shouldStartDomainRenewal(cs, domain, now, pair, minValidity)
@@ -133,7 +137,7 @@ func (b *LocalBackend) GetCertPEMWithValidity(ctx context.Context, domain string
if minValidity == 0 {
logf("starting async renewal")
// Start renewal in the background, return current valid cert.
go b.getCertPEM(context.Background(), cs, logf, traceACME, domain, now, minValidity)
b.goTracker.Go(func() { getCertPEM(context.Background(), b, cs, logf, traceACME, domain, now, minValidity) })
return pair, nil
}
// If the caller requested a specific validity duration, fall through
@@ -141,7 +145,11 @@ func (b *LocalBackend) GetCertPEMWithValidity(ctx context.Context, domain string
logf("starting sync renewal")
}
pair, err := b.getCertPEM(ctx, cs, logf, traceACME, domain, now, minValidity)
if envknob.IsCertShareReadOnlyMode() {
return nil, fmt.Errorf("retrieving cached TLS certificate failed and cert store is configured in read-only mode, not attempting to issue a new certificate: %w", err)
}
pair, err := getCertPEM(ctx, b, cs, logf, traceACME, domain, now, minValidity)
if err != nil {
logf("getCertPEM: %v", err)
return nil, err
@@ -249,15 +257,13 @@ type certStore interface {
// for now. If they're expired, it returns errCertExpired.
// If they don't exist, it returns ipn.ErrStateNotExist.
Read(domain string, now time.Time) (*TLSCertKeyPair, error)
// WriteCert writes the cert for domain.
WriteCert(domain string, cert []byte) error
// WriteKey writes the key for domain.
WriteKey(domain string, key []byte) error
// ACMEKey returns the value previously stored via WriteACMEKey.
// It is a PEM encoded ECDSA key.
ACMEKey() ([]byte, error)
// WriteACMEKey stores the provided PEM encoded ECDSA key.
WriteACMEKey([]byte) error
// WriteTLSCertAndKey writes the cert and key for domain.
WriteTLSCertAndKey(domain string, cert, key []byte) error
}
var errCertExpired = errors.New("cert expired")
@@ -343,6 +349,13 @@ func (f certFileStore) WriteKey(domain string, key []byte) error {
return atomicfile.WriteFile(keyFile(f.dir, domain), key, 0600)
}
func (f certFileStore) WriteTLSCertAndKey(domain string, cert, key []byte) error {
if err := f.WriteKey(domain, key); err != nil {
return err
}
return f.WriteCert(domain, cert)
}
// certStateStore implements certStore by storing the cert & key files in an ipn.StateStore.
type certStateStore struct {
ipn.StateStore
@@ -352,7 +365,29 @@ type certStateStore struct {
testRoots *x509.CertPool
}
// TLSCertKeyReader is an interface implemented by state stores where it makes
// sense to read the TLS cert and key in a single operation that can be
// distinguished from generic state value reads. Currently this is only implemented
// by the kubestore.Store, which, in some cases, need to read cert and key from a
// non-cached TLS Secret.
type TLSCertKeyReader interface {
ReadTLSCertAndKey(domain string) ([]byte, []byte, error)
}
func (s certStateStore) Read(domain string, now time.Time) (*TLSCertKeyPair, error) {
// If we're using a store that supports atomic reads, use that
if kr, ok := s.StateStore.(TLSCertKeyReader); ok {
cert, key, err := kr.ReadTLSCertAndKey(domain)
if err != nil {
return nil, err
}
if !validCertPEM(domain, key, cert, s.testRoots, now) {
return nil, errCertExpired
}
return &TLSCertKeyPair{CertPEM: cert, KeyPEM: key, Cached: true}, nil
}
// Otherwise fall back to separate reads
certPEM, err := s.ReadState(ipn.StateKey(domain + ".crt"))
if err != nil {
return nil, err
@@ -383,6 +418,27 @@ func (s certStateStore) WriteACMEKey(key []byte) error {
return ipn.WriteState(s.StateStore, ipn.StateKey(acmePEMName), key)
}
// TLSCertKeyWriter is an interface implemented by state stores that can write the TLS
// cert and key in a single atomic operation. Currently this is only implemented
// by the kubestore.StoreKube.
type TLSCertKeyWriter interface {
WriteTLSCertAndKey(domain string, cert, key []byte) error
}
// WriteTLSCertAndKey writes the TLS cert and key for domain to the current
// LocalBackend's StateStore.
func (s certStateStore) WriteTLSCertAndKey(domain string, cert, key []byte) error {
// If we're using a store that supports atomic writes, use that.
if aw, ok := s.StateStore.(TLSCertKeyWriter); ok {
return aw.WriteTLSCertAndKey(domain, cert, key)
}
// Otherwise fall back to separate writes for cert and key.
if err := s.WriteKey(domain, key); err != nil {
return err
}
return s.WriteCert(domain, cert)
}
// TLSCertKeyPair is a TLS public and private key, and whether they were obtained
// from cache or freshly obtained.
type TLSCertKeyPair struct {
@@ -419,7 +475,9 @@ func getCertPEMCached(cs certStore, domain string, now time.Time) (p *TLSCertKey
return cs.Read(domain, now)
}
func (b *LocalBackend) getCertPEM(ctx context.Context, cs certStore, logf logger.Logf, traceACME func(any), domain string, now time.Time, minValidity time.Duration) (*TLSCertKeyPair, error) {
// getCertPem checks if a cert needs to be renewed and if so, renews it.
// It can be overridden in tests.
var getCertPEM = func(ctx context.Context, b *LocalBackend, cs certStore, logf logger.Logf, traceACME func(any), domain string, now time.Time, minValidity time.Duration) (*TLSCertKeyPair, error) {
acmeMu.Lock()
defer acmeMu.Unlock()
@@ -444,6 +502,10 @@ func (b *LocalBackend) getCertPEM(ctx context.Context, cs certStore, logf logger
return nil, err
}
if !isDefaultDirectoryURL(ac.DirectoryURL) {
logf("acme: using Directory URL %q", ac.DirectoryURL)
}
a, err := ac.GetReg(ctx, "" /* pre-RFC param */)
switch {
case err == nil:
@@ -545,9 +607,6 @@ func (b *LocalBackend) getCertPEM(ctx context.Context, cs certStore, logf logger
if err := encodeECDSAKey(&privPEM, certPrivKey); err != nil {
return nil, err
}
if err := cs.WriteKey(domain, privPEM.Bytes()); err != nil {
return nil, err
}
csr, err := certRequest(certPrivKey, domain, nil)
if err != nil {
@@ -555,6 +614,7 @@ func (b *LocalBackend) getCertPEM(ctx context.Context, cs certStore, logf logger
}
logf("requesting cert...")
traceACME(csr)
der, _, err := ac.CreateOrderCert(ctx, order.FinalizeURL, csr, true)
if err != nil {
return nil, fmt.Errorf("CreateOrder: %v", err)
@@ -568,7 +628,7 @@ func (b *LocalBackend) getCertPEM(ctx context.Context, cs certStore, logf logger
return nil, err
}
}
if err := cs.WriteCert(domain, certPEM.Bytes()); err != nil {
if err := cs.WriteTLSCertAndKey(domain, certPEM.Bytes(), privPEM.Bytes()); err != nil {
return nil, err
}
b.domainRenewed(domain)
@@ -577,10 +637,10 @@ func (b *LocalBackend) getCertPEM(ctx context.Context, cs certStore, logf logger
}
// certRequest generates a CSR for the given common name cn and optional SANs.
func certRequest(key crypto.Signer, cn string, ext []pkix.Extension, san ...string) ([]byte, error) {
func certRequest(key crypto.Signer, name string, ext []pkix.Extension) ([]byte, error) {
req := &x509.CertificateRequest{
Subject: pkix.Name{CommonName: cn},
DNSNames: san,
Subject: pkix.Name{CommonName: name},
DNSNames: []string{name},
ExtraExtensions: ext,
}
return x509.CreateCertificateRequest(rand.Reader, req, key)
@@ -657,15 +717,16 @@ func acmeClient(cs certStore) (*acme.Client, error) {
// LetsEncrypt), we should make sure that they support ARI extension (see
// shouldStartDomainRenewalARI).
return &acme.Client{
Key: key,
UserAgent: "tailscaled/" + version.Long(),
Key: key,
UserAgent: "tailscaled/" + version.Long(),
DirectoryURL: envknob.String("TS_DEBUG_ACME_DIRECTORY_URL"),
}, nil
}
// validCertPEM reports whether the given certificate is valid for domain at now.
//
// If roots != nil, it is used instead of the system root pool. This is meant
// to support testing, and production code should pass roots == nil.
// to support testing; production code should pass roots == nil.
func validCertPEM(domain string, keyPEM, certPEM []byte, roots *x509.CertPool, now time.Time) bool {
if len(keyPEM) == 0 || len(certPEM) == 0 {
return false
@@ -688,16 +749,51 @@ func validCertPEM(domain string, keyPEM, certPEM []byte, roots *x509.CertPool, n
intermediates.AddCert(cert)
}
}
return validateLeaf(leaf, intermediates, domain, now, roots)
}
// validateLeaf is a helper for [validCertPEM].
//
// If called with roots == nil, it will use the system root pool as well as the
// baked-in roots. If non-nil, only those roots are used.
func validateLeaf(leaf *x509.Certificate, intermediates *x509.CertPool, domain string, now time.Time, roots *x509.CertPool) bool {
if leaf == nil {
return false
}
_, err = leaf.Verify(x509.VerifyOptions{
_, err := leaf.Verify(x509.VerifyOptions{
DNSName: domain,
CurrentTime: now,
Roots: roots,
Intermediates: intermediates,
})
return err == nil
if err != nil && roots == nil {
// If validation failed and they specified nil for roots (meaning to use
// the system roots), then give it another chance to validate using the
// binary's baked-in roots (LetsEncrypt). See tailscale/tailscale#14690.
return validateLeaf(leaf, intermediates, domain, now, bakedroots.Get())
}
if err == nil {
return true
}
// When pointed at a non-prod ACME server, we don't expect to have the CA
// in our system or baked-in roots. Verify only throws UnknownAuthorityError
// after first checking the leaf cert's expiry, hostnames etc, so we know
// that the only reason for an error is to do with constructing a full chain.
// Allow this error so that cert caching still works in testing environments.
if errors.As(err, &x509.UnknownAuthorityError{}) {
acmeURL := envknob.String("TS_DEBUG_ACME_DIRECTORY_URL")
if !isDefaultDirectoryURL(acmeURL) {
return true
}
}
return false
}
func isDefaultDirectoryURL(u string) bool {
return u == "" || u == acme.LetsEncryptURL
}
// validLookingCertDomain reports whether name looks like a valid domain name that

178
vendor/tailscale.com/ipn/ipnlocal/desktop_sessions.go generated vendored Normal file
View File

@@ -0,0 +1,178 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// Both the desktop session manager and multi-user support
// are currently available only on Windows.
// This file does not need to be built for other platforms.
//go:build windows && !ts_omit_desktop_sessions
package ipnlocal
import (
"cmp"
"errors"
"fmt"
"sync"
"tailscale.com/feature"
"tailscale.com/ipn"
"tailscale.com/ipn/desktop"
"tailscale.com/tsd"
"tailscale.com/types/logger"
"tailscale.com/util/syspolicy"
)
func init() {
feature.Register("desktop-sessions")
RegisterExtension("desktop-sessions", newDesktopSessionsExt)
}
// desktopSessionsExt implements [localBackendExtension].
var _ localBackendExtension = (*desktopSessionsExt)(nil)
// desktopSessionsExt extends [LocalBackend] with desktop session management.
// It keeps Tailscale running in the background if Always-On mode is enabled,
// and switches to an appropriate profile when a user signs in or out,
// locks their screen, or disconnects a remote session.
type desktopSessionsExt struct {
logf logger.Logf
sm desktop.SessionManager
*LocalBackend // or nil, until Init is called
cleanup []func() // cleanup functions to call on shutdown
// mu protects all following fields.
// When both mu and [LocalBackend.mu] need to be taken,
// [LocalBackend.mu] must be taken before mu.
mu sync.Mutex
id2sess map[desktop.SessionID]*desktop.Session
}
// newDesktopSessionsExt returns a new [desktopSessionsExt],
// or an error if [desktop.SessionManager] is not available.
func newDesktopSessionsExt(logf logger.Logf, sys *tsd.System) (localBackendExtension, error) {
sm, ok := sys.SessionManager.GetOK()
if !ok {
return nil, errors.New("session manager is not available")
}
return &desktopSessionsExt{logf: logf, sm: sm, id2sess: make(map[desktop.SessionID]*desktop.Session)}, nil
}
// Init implements [localBackendExtension].
func (e *desktopSessionsExt) Init(lb *LocalBackend) (err error) {
e.LocalBackend = lb
unregisterResolver := lb.RegisterBackgroundProfileResolver(e.getBackgroundProfile)
unregisterSessionCb, err := e.sm.RegisterStateCallback(e.updateDesktopSessionState)
if err != nil {
unregisterResolver()
return fmt.Errorf("session callback registration failed: %w", err)
}
e.cleanup = []func(){unregisterResolver, unregisterSessionCb}
return nil
}
// updateDesktopSessionState is a [desktop.SessionStateCallback]
// invoked by [desktop.SessionManager] once for each existing session
// and whenever the session state changes. It updates the session map
// and switches to the best profile if necessary.
func (e *desktopSessionsExt) updateDesktopSessionState(session *desktop.Session) {
e.mu.Lock()
if session.Status != desktop.ClosedSession {
e.id2sess[session.ID] = session
} else {
delete(e.id2sess, session.ID)
}
e.mu.Unlock()
var action string
switch session.Status {
case desktop.ForegroundSession:
// The user has either signed in or unlocked their session.
// For remote sessions, this may also mean the user has connected.
// The distinction isn't important for our purposes,
// so let's always say "signed in".
action = "signed in to"
case desktop.BackgroundSession:
action = "locked"
case desktop.ClosedSession:
action = "signed out from"
default:
panic("unreachable")
}
maybeUsername, _ := session.User.Username()
userIdentifier := cmp.Or(maybeUsername, string(session.User.UserID()), "user")
reason := fmt.Sprintf("%s %s session %v", userIdentifier, action, session.ID)
e.SwitchToBestProfile(reason)
}
// getBackgroundProfile is a [profileResolver] that works as follows:
//
// If Always-On mode is disabled, it returns no profile ("","",false).
//
// If AlwaysOn mode is enabled, it returns the current profile unless:
// - The current user has signed out.
// - Another user has a foreground (i.e. active/unlocked) session.
//
// If the current user's session runs in the background and no other user
// has a foreground session, it returns the current profile. This applies
// when a locally signed-in user locks their screen or when a remote user
// disconnects without signing out.
//
// In all other cases, it returns no profile ("","",false).
//
// It is called with [LocalBackend.mu] locked.
func (e *desktopSessionsExt) getBackgroundProfile() (_ ipn.WindowsUserID, _ ipn.ProfileID, ok bool) {
e.mu.Lock()
defer e.mu.Unlock()
if alwaysOn, _ := syspolicy.GetBoolean(syspolicy.AlwaysOn, false); !alwaysOn {
return "", "", false
}
isCurrentUserSingedIn := false
var foregroundUIDs []ipn.WindowsUserID
for _, s := range e.id2sess {
switch uid := s.User.UserID(); uid {
case e.pm.CurrentUserID():
isCurrentUserSingedIn = true
if s.Status == desktop.ForegroundSession {
// Keep the current profile if the user has a foreground session.
return e.pm.CurrentUserID(), e.pm.CurrentProfile().ID(), true
}
default:
if s.Status == desktop.ForegroundSession {
foregroundUIDs = append(foregroundUIDs, uid)
}
}
}
// If there's no current user (e.g., tailscaled just started), or if the current
// user has no foreground session, switch to the default profile of the first user
// with a foreground session, if any.
for _, uid := range foregroundUIDs {
if profileID := e.pm.DefaultUserProfileID(uid); profileID != "" {
return uid, profileID, true
}
}
// If no user has a foreground session but the current user is still signed in,
// keep the current profile even if the session is not in the foreground,
// such as when the screen is locked or a remote session is disconnected.
if len(foregroundUIDs) == 0 && isCurrentUserSingedIn {
return e.pm.CurrentUserID(), e.pm.CurrentProfile().ID(), true
}
return "", "", false
}
// Shutdown implements [localBackendExtension].
func (e *desktopSessionsExt) Shutdown() error {
for _, f := range e.cleanup {
f()
}
e.cleanup = nil
e.LocalBackend = nil
return nil
}

View File

@@ -347,16 +347,14 @@ func (b *LocalBackend) driveRemotesFromPeers(nm *netmap.NetworkMap) []*drive.Rem
// TODO(oxtoacart): for some reason, this correctly
// catches when a node goes from offline to online,
// but not the other way around...
online := peer.Online()
if online == nil || !*online {
if !peer.Online().Get() {
return false
}
// Check that the peer is allowed to share with us.
addresses := peer.Addresses()
for i := range addresses.Len() {
addr := addresses.At(i)
capsMap := b.PeerCaps(addr.Addr())
for _, p := range addresses.All() {
capsMap := b.PeerCaps(p.Addr())
if capsMap.HasCapability(tailcfg.PeerCapabilityTaildriveSharer) {
return true
}

View File

@@ -116,7 +116,7 @@ func (em *expiryManager) flagExpiredPeers(netmap *netmap.NetworkMap, localNow ti
// since we discover endpoints via DERP, and due to DERP return
// path optimization.
mut.Endpoints = nil
mut.DERP = ""
mut.HomeDERP = 0
// Defense-in-depth: break the node's public key as well, in
// case something tries to communicate.

File diff suppressed because it is too large Load Diff

View File

@@ -407,7 +407,7 @@ func (b *LocalBackend) tkaApplyDisablementLocked(secret []byte) error {
//
// b.mu must be held.
func (b *LocalBackend) chonkPathLocked() string {
return filepath.Join(b.TailscaleVarRoot(), "tka-profiles", string(b.pm.CurrentProfile().ID))
return filepath.Join(b.TailscaleVarRoot(), "tka-profiles", string(b.pm.CurrentProfile().ID()))
}
// tkaBootstrapFromGenesisLocked initializes the local (on-disk) state of the
@@ -430,8 +430,7 @@ func (b *LocalBackend) tkaBootstrapFromGenesisLocked(g tkatype.MarshaledAUM, per
}
bootstrapStateID := fmt.Sprintf("%d:%d", genesis.State.StateID1, genesis.State.StateID2)
for i := range persist.DisallowedTKAStateIDs().Len() {
stateID := persist.DisallowedTKAStateIDs().At(i)
for _, stateID := range persist.DisallowedTKAStateIDs().All() {
if stateID == bootstrapStateID {
return fmt.Errorf("TKA with stateID of %q is disallowed on this node", stateID)
}
@@ -456,7 +455,7 @@ func (b *LocalBackend) tkaBootstrapFromGenesisLocked(g tkatype.MarshaledAUM, per
}
b.tka = &tkaState{
profile: b.pm.CurrentProfile().ID,
profile: b.pm.CurrentProfile().ID(),
authority: authority,
storage: chonk,
}
@@ -572,8 +571,7 @@ func tkaStateFromPeer(p tailcfg.NodeView) ipnstate.TKAPeer {
TailscaleIPs: make([]netip.Addr, 0, p.Addresses().Len()),
NodeKey: p.Key(),
}
for i := range p.Addresses().Len() {
addr := p.Addresses().At(i)
for _, addr := range p.Addresses().All() {
if addr.IsSingleIP() && tsaddr.IsTailscaleIP(addr.Addr()) {
fp.TailscaleIPs = append(fp.TailscaleIPs, addr.Addr())
}

View File

@@ -20,13 +20,11 @@ import (
"path/filepath"
"runtime"
"slices"
"sort"
"strconv"
"strings"
"sync"
"time"
"github.com/kortschak/wol"
"golang.org/x/net/dns/dnsmessage"
"golang.org/x/net/http/httpguts"
"tailscale.com/drive"
@@ -226,6 +224,23 @@ type peerAPIHandler struct {
peerUser tailcfg.UserProfile // profile of peerNode
}
// PeerAPIHandler is the interface implemented by [peerAPIHandler] and needed by
// module features registered via tailscale.com/feature/*.
type PeerAPIHandler interface {
Peer() tailcfg.NodeView
PeerCaps() tailcfg.PeerCapMap
Self() tailcfg.NodeView
LocalBackend() *LocalBackend
IsSelfUntagged() bool // whether the peer is untagged and the same as this user
}
func (h *peerAPIHandler) IsSelfUntagged() bool {
return !h.selfNode.IsTagged() && !h.peerNode.IsTagged() && h.isSelf
}
func (h *peerAPIHandler) Peer() tailcfg.NodeView { return h.peerNode }
func (h *peerAPIHandler) Self() tailcfg.NodeView { return h.selfNode }
func (h *peerAPIHandler) LocalBackend() *LocalBackend { return h.ps.b }
func (h *peerAPIHandler) logf(format string, a ...any) {
h.ps.b.logf("peerapi: "+format, a...)
}
@@ -233,11 +248,13 @@ func (h *peerAPIHandler) logf(format string, a ...any) {
// isAddressValid reports whether addr is a valid destination address for this
// node originating from the peer.
func (h *peerAPIHandler) isAddressValid(addr netip.Addr) bool {
if v := h.peerNode.SelfNodeV4MasqAddrForThisPeer(); v != nil {
return *v == addr
if !addr.IsValid() {
return false
}
if v := h.peerNode.SelfNodeV6MasqAddrForThisPeer(); v != nil {
return *v == addr
v4MasqAddr, hasMasqV4 := h.peerNode.SelfNodeV4MasqAddrForThisPeer().GetOk()
v6MasqAddr, hasMasqV6 := h.peerNode.SelfNodeV6MasqAddrForThisPeer().GetOk()
if hasMasqV4 || hasMasqV6 {
return addr == v4MasqAddr || addr == v6MasqAddr
}
pfx := netip.PrefixFrom(addr, addr.BitLen())
return views.SliceContains(h.selfNode.Addresses(), pfx)
@@ -300,6 +317,20 @@ func peerAPIRequestShouldGetSecurityHeaders(r *http.Request) bool {
return false
}
// RegisterPeerAPIHandler registers a PeerAPI handler.
//
// The path should be of the form "/v0/foo".
//
// It panics if the path is already registered.
func RegisterPeerAPIHandler(path string, f func(PeerAPIHandler, http.ResponseWriter, *http.Request)) {
if _, ok := peerAPIHandlers[path]; ok {
panic(fmt.Sprintf("duplicate PeerAPI handler %q", path))
}
peerAPIHandlers[path] = f
}
var peerAPIHandlers = map[string]func(PeerAPIHandler, http.ResponseWriter, *http.Request){} // by URL.Path
func (h *peerAPIHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if err := h.validatePeerAPIRequest(r); err != nil {
metricInvalidRequests.Add(1)
@@ -344,10 +375,6 @@ func (h *peerAPIHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
case "/v0/dnsfwd":
h.handleServeDNSFwd(w, r)
return
case "/v0/wol":
metricWakeOnLANCalls.Add(1)
h.handleWakeOnLAN(w, r)
return
case "/v0/interfaces":
h.handleServeInterfaces(w, r)
return
@@ -362,6 +389,10 @@ func (h *peerAPIHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
h.handleServeIngress(w, r)
return
}
if ph, ok := peerAPIHandlers[r.URL.Path]; ok {
ph(h, w, r)
return
}
who := h.peerUser.DisplayName
fmt.Fprintf(w, `<html>
<meta name="viewport" content="width=device-width, initial-scale=1">
@@ -450,7 +481,7 @@ func (h *peerAPIHandler) handleServeInterfaces(w http.ResponseWriter, r *http.Re
fmt.Fprintf(w, "<h3>Could not get the default route: %s</h3>\n", html.EscapeString(err.Error()))
}
if hasCGNATInterface, err := netmon.HasCGNATInterface(); hasCGNATInterface {
if hasCGNATInterface, err := h.ps.b.sys.NetMon.Get().HasCGNATInterface(); hasCGNATInterface {
fmt.Fprintln(w, "<p>There is another interface using the CGNAT range.</p>")
} else if err != nil {
fmt.Fprintf(w, "<p>Could not check for CGNAT interfaces: %s</p>\n", html.EscapeString(err.Error()))
@@ -622,14 +653,6 @@ func (h *peerAPIHandler) canDebug() bool {
return h.isSelf || h.peerHasCap(tailcfg.PeerCapabilityDebugPeer)
}
// canWakeOnLAN reports whether h can send a Wake-on-LAN packet from this node.
func (h *peerAPIHandler) canWakeOnLAN() bool {
if h.peerNode.UnsignedPeerAPIOnly() {
return false
}
return h.isSelf || h.peerHasCap(tailcfg.PeerCapabilityWakeOnLAN)
}
var allowSelfIngress = envknob.RegisterBool("TS_ALLOW_SELF_INGRESS")
// canIngress reports whether h can send ingress requests to this node.
@@ -638,10 +661,10 @@ func (h *peerAPIHandler) canIngress() bool {
}
func (h *peerAPIHandler) peerHasCap(wantCap tailcfg.PeerCapability) bool {
return h.peerCaps().HasCapability(wantCap)
return h.PeerCaps().HasCapability(wantCap)
}
func (h *peerAPIHandler) peerCaps() tailcfg.PeerCapMap {
func (h *peerAPIHandler) PeerCaps() tailcfg.PeerCapMap {
return h.ps.b.PeerCaps(h.remoteAddr.Addr())
}
@@ -815,61 +838,6 @@ func (h *peerAPIHandler) handleServeDNSFwd(w http.ResponseWriter, r *http.Reques
dh.ServeHTTP(w, r)
}
func (h *peerAPIHandler) handleWakeOnLAN(w http.ResponseWriter, r *http.Request) {
if !h.canWakeOnLAN() {
http.Error(w, "no WoL access", http.StatusForbidden)
return
}
if r.Method != "POST" {
http.Error(w, "bad method", http.StatusMethodNotAllowed)
return
}
macStr := r.FormValue("mac")
if macStr == "" {
http.Error(w, "missing 'mac' param", http.StatusBadRequest)
return
}
mac, err := net.ParseMAC(macStr)
if err != nil {
http.Error(w, "bad 'mac' param", http.StatusBadRequest)
return
}
var password []byte // TODO(bradfitz): support? does anything use WoL passwords?
st := h.ps.b.sys.NetMon.Get().InterfaceState()
if st == nil {
http.Error(w, "failed to get interfaces state", http.StatusInternalServerError)
return
}
var res struct {
SentTo []string
Errors []string
}
for ifName, ips := range st.InterfaceIPs {
for _, ip := range ips {
if ip.Addr().IsLoopback() || ip.Addr().Is6() {
continue
}
local := &net.UDPAddr{
IP: ip.Addr().AsSlice(),
Port: 0,
}
remote := &net.UDPAddr{
IP: net.IPv4bcast,
Port: 0,
}
if err := wol.Wake(mac, password, local, remote); err != nil {
res.Errors = append(res.Errors, err.Error())
} else {
res.SentTo = append(res.SentTo, ifName)
}
break // one per interface is enough
}
}
sort.Strings(res.SentTo)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(res)
}
func (h *peerAPIHandler) replyToDNSQueries() bool {
if h.isSelf {
// If the peer is owned by the same user, just allow it
@@ -964,7 +932,11 @@ func (h *peerAPIHandler) handleDNSQuery(w http.ResponseWriter, r *http.Request)
// instead to avoid re-parsing the DNS response for improved performance in
// the future.
if h.ps.b.OfferingAppConnector() {
h.ps.b.ObserveDNSResponse(res)
if err := h.ps.b.ObserveDNSResponse(res); err != nil {
h.logf("ObserveDNSResponse error: %v", err)
// This is not fatal, we probably just failed to parse the upstream
// response. Return it to the caller anyway.
}
}
if pretty {
@@ -1148,7 +1120,7 @@ func (h *peerAPIHandler) handleServeDrive(w http.ResponseWriter, r *http.Request
return
}
capsMap := h.peerCaps()
capsMap := h.PeerCaps()
driveCaps, ok := capsMap[tailcfg.PeerCapabilityTaildrive]
if !ok {
h.logf("taildrive: not permitted")
@@ -1272,8 +1244,7 @@ var (
metricInvalidRequests = clientmetric.NewCounter("peerapi_invalid_requests")
// Non-debug PeerAPI endpoints.
metricPutCalls = clientmetric.NewCounter("peerapi_put")
metricDNSCalls = clientmetric.NewCounter("peerapi_dns")
metricWakeOnLANCalls = clientmetric.NewCounter("peerapi_wol")
metricIngressCalls = clientmetric.NewCounter("peerapi_ingress")
metricPutCalls = clientmetric.NewCounter("peerapi_put")
metricDNSCalls = clientmetric.NewCounter("peerapi_dns")
metricIngressCalls = clientmetric.NewCounter("peerapi_ingress")
)

View File

@@ -35,9 +35,9 @@ type profileManager struct {
health *health.Tracker
currentUserID ipn.WindowsUserID
knownProfiles map[ipn.ProfileID]*ipn.LoginProfile // always non-nil
currentProfile *ipn.LoginProfile // always non-nil
prefs ipn.PrefsView // always Valid.
knownProfiles map[ipn.ProfileID]ipn.LoginProfileView // always non-nil
currentProfile ipn.LoginProfileView // always Valid.
prefs ipn.PrefsView // always Valid.
}
func (pm *profileManager) dlogf(format string, args ...any) {
@@ -77,6 +77,49 @@ func (pm *profileManager) SetCurrentUserID(uid ipn.WindowsUserID) {
}
}
// SetCurrentUserAndProfile sets the current user ID and switches the specified
// profile, if it is accessible to the user. If the profile does not exist,
// or is not accessible, it switches to the user's default profile,
// creating a new one if necessary.
//
// It is a shorthand for [profileManager.SetCurrentUserID] followed by
// [profileManager.SwitchProfile], but it is more efficient as it switches
// directly to the specified profile rather than switching to the user's
// default profile first.
//
// As a special case, if the specified profile ID "", it creates a new
// profile for the user and switches to it, unless the current profile
// is already a new, empty profile owned by the user.
//
// It returns the current profile and whether the call resulted
// in a profile switch.
func (pm *profileManager) SetCurrentUserAndProfile(uid ipn.WindowsUserID, profileID ipn.ProfileID) (cp ipn.LoginProfileView, changed bool) {
pm.currentUserID = uid
if profileID == "" {
if pm.currentProfile.ID() == "" && pm.currentProfile.LocalUserID() == uid {
return pm.currentProfile, false
}
pm.NewProfileForUser(uid)
return pm.currentProfile, true
}
if profile, err := pm.ProfileByID(profileID); err == nil {
if pm.CurrentProfile().ID() == profileID {
return pm.currentProfile, false
}
if err := pm.SwitchProfile(profile.ID()); err == nil {
return pm.currentProfile, true
}
}
if err := pm.SwitchToDefaultProfile(); err != nil {
pm.logf("%q's default profile cannot be used; creating a new one: %v", uid, err)
pm.NewProfile()
}
return pm.currentProfile, true
}
// DefaultUserProfileID returns [ipn.ProfileID] of the default (last used) profile for the specified user,
// or an empty string if the specified user does not have a default profile.
func (pm *profileManager) DefaultUserProfileID(uid ipn.WindowsUserID) ipn.ProfileID {
@@ -89,7 +132,7 @@ func (pm *profileManager) DefaultUserProfileID(uid ipn.WindowsUserID) ipn.Profil
pm.dlogf("DefaultUserProfileID: windows: migrating from legacy preferences")
profile, err := pm.migrateFromLegacyPrefs(uid, false)
if err == nil {
return profile.ID
return profile.ID()
}
pm.logf("failed to migrate from legacy preferences: %v", err)
}
@@ -97,41 +140,48 @@ func (pm *profileManager) DefaultUserProfileID(uid ipn.WindowsUserID) ipn.Profil
}
pk := ipn.StateKey(string(b))
prof := pm.findProfileByKey(pk)
if prof == nil {
prof := pm.findProfileByKey(uid, pk)
if !prof.Valid() {
pm.dlogf("DefaultUserProfileID: no profile found for key: %q", pk)
return ""
}
return prof.ID
return prof.ID()
}
// checkProfileAccess returns an [errProfileAccessDenied] if the current user
// does not have access to the specified profile.
func (pm *profileManager) checkProfileAccess(profile *ipn.LoginProfile) error {
if pm.currentUserID != "" && profile.LocalUserID != pm.currentUserID {
func (pm *profileManager) checkProfileAccess(profile ipn.LoginProfileView) error {
return pm.checkProfileAccessAs(pm.currentUserID, profile)
}
// checkProfileAccessAs returns an [errProfileAccessDenied] if the specified user
// does not have access to the specified profile.
func (pm *profileManager) checkProfileAccessAs(uid ipn.WindowsUserID, profile ipn.LoginProfileView) error {
if uid != "" && profile.LocalUserID() != uid {
return errProfileAccessDenied
}
return nil
}
// allProfiles returns all profiles accessible to the current user.
// allProfilesFor returns all profiles accessible to the specified user.
// The returned profiles are sorted by Name.
func (pm *profileManager) allProfiles() (out []*ipn.LoginProfile) {
func (pm *profileManager) allProfilesFor(uid ipn.WindowsUserID) []ipn.LoginProfileView {
out := make([]ipn.LoginProfileView, 0, len(pm.knownProfiles))
for _, p := range pm.knownProfiles {
if pm.checkProfileAccess(p) == nil {
if pm.checkProfileAccessAs(uid, p) == nil {
out = append(out, p)
}
}
slices.SortFunc(out, func(a, b *ipn.LoginProfile) int {
return cmp.Compare(a.Name, b.Name)
slices.SortFunc(out, func(a, b ipn.LoginProfileView) int {
return cmp.Compare(a.Name(), b.Name())
})
return out
}
// matchingProfiles is like [profileManager.allProfiles], but returns only profiles
// matchingProfiles is like [profileManager.allProfilesFor], but returns only profiles
// matching the given predicate.
func (pm *profileManager) matchingProfiles(f func(*ipn.LoginProfile) bool) (out []*ipn.LoginProfile) {
all := pm.allProfiles()
func (pm *profileManager) matchingProfiles(uid ipn.WindowsUserID, f func(ipn.LoginProfileView) bool) (out []ipn.LoginProfileView) {
all := pm.allProfilesFor(uid)
out = all[:0]
for _, p := range all {
if f(p) {
@@ -144,11 +194,11 @@ func (pm *profileManager) matchingProfiles(f func(*ipn.LoginProfile) bool) (out
// findMatchingProfiles returns all profiles accessible to the current user
// that represent the same node/user as prefs.
// The returned profiles are sorted by Name.
func (pm *profileManager) findMatchingProfiles(prefs ipn.PrefsView) []*ipn.LoginProfile {
return pm.matchingProfiles(func(p *ipn.LoginProfile) bool {
return p.ControlURL == prefs.ControlURL() &&
(p.UserProfile.ID == prefs.Persist().UserProfile().ID ||
p.NodeID == prefs.Persist().NodeID())
func (pm *profileManager) findMatchingProfiles(uid ipn.WindowsUserID, prefs ipn.PrefsView) []ipn.LoginProfileView {
return pm.matchingProfiles(uid, func(p ipn.LoginProfileView) bool {
return p.ControlURL() == prefs.ControlURL() &&
(p.UserProfile().ID == prefs.Persist().UserProfile().ID ||
p.NodeID() == prefs.Persist().NodeID())
})
}
@@ -156,19 +206,19 @@ func (pm *profileManager) findMatchingProfiles(prefs ipn.PrefsView) []*ipn.Login
// given name. It returns "" if no such profile exists among profiles
// accessible to the current user.
func (pm *profileManager) ProfileIDForName(name string) ipn.ProfileID {
p := pm.findProfileByName(name)
if p == nil {
p := pm.findProfileByName(pm.currentUserID, name)
if !p.Valid() {
return ""
}
return p.ID
return p.ID()
}
func (pm *profileManager) findProfileByName(name string) *ipn.LoginProfile {
out := pm.matchingProfiles(func(p *ipn.LoginProfile) bool {
return p.Name == name
func (pm *profileManager) findProfileByName(uid ipn.WindowsUserID, name string) ipn.LoginProfileView {
out := pm.matchingProfiles(uid, func(p ipn.LoginProfileView) bool {
return p.Name() == name && pm.checkProfileAccessAs(uid, p) == nil
})
if len(out) == 0 {
return nil
return ipn.LoginProfileView{}
}
if len(out) > 1 {
pm.logf("[unexpected] multiple profiles with the same name")
@@ -176,12 +226,12 @@ func (pm *profileManager) findProfileByName(name string) *ipn.LoginProfile {
return out[0]
}
func (pm *profileManager) findProfileByKey(key ipn.StateKey) *ipn.LoginProfile {
out := pm.matchingProfiles(func(p *ipn.LoginProfile) bool {
return p.Key == key
func (pm *profileManager) findProfileByKey(uid ipn.WindowsUserID, key ipn.StateKey) ipn.LoginProfileView {
out := pm.matchingProfiles(uid, func(p ipn.LoginProfileView) bool {
return p.Key() == key && pm.checkProfileAccessAs(uid, p) == nil
})
if len(out) == 0 {
return nil
return ipn.LoginProfileView{}
}
if len(out) > 1 {
pm.logf("[unexpected] multiple profiles with the same key")
@@ -194,8 +244,8 @@ func (pm *profileManager) setUnattendedModeAsConfigured() error {
return nil
}
if pm.currentProfile.Key != "" && pm.prefs.ForceDaemon() {
return pm.WriteState(ipn.ServerModeStartKey, []byte(pm.currentProfile.Key))
if pm.currentProfile.Key() != "" && pm.prefs.ForceDaemon() {
return pm.WriteState(ipn.ServerModeStartKey, []byte(pm.currentProfile.Key()))
} else {
return pm.WriteState(ipn.ServerModeStartKey, nil)
}
@@ -222,36 +272,43 @@ func (pm *profileManager) SetPrefs(prefsIn ipn.PrefsView, np ipn.NetworkProfile)
}
// Check if we already have an existing profile that matches the user/node.
if existing := pm.findMatchingProfiles(prefsIn); len(existing) > 0 {
if existing := pm.findMatchingProfiles(pm.currentUserID, prefsIn); len(existing) > 0 {
// We already have a profile for this user/node we should reuse it. Also
// cleanup any other duplicate profiles.
cp = existing[0]
existing = existing[1:]
for _, p := range existing {
// Clear the state.
if err := pm.store.WriteState(p.Key, nil); err != nil {
if err := pm.store.WriteState(p.Key(), nil); err != nil {
// We couldn't delete the state, so keep the profile around.
continue
}
// Remove the profile, knownProfiles will be persisted
// in [profileManager.setProfilePrefs] below.
delete(pm.knownProfiles, p.ID)
delete(pm.knownProfiles, p.ID())
}
}
pm.currentProfile = cp
if err := pm.SetProfilePrefs(cp, prefsIn, np); err != nil {
cp, err := pm.setProfilePrefs(nil, prefsIn, np)
if err != nil {
return err
}
return pm.setProfileAsUserDefault(cp)
}
// SetProfilePrefs is like [profileManager.SetPrefs], but sets prefs for the specified [ipn.LoginProfile]
// which is not necessarily the [profileManager.CurrentProfile]. It returns an [errProfileAccessDenied]
// if the specified profile is not accessible by the current user.
func (pm *profileManager) SetProfilePrefs(lp *ipn.LoginProfile, prefsIn ipn.PrefsView, np ipn.NetworkProfile) error {
if err := pm.checkProfileAccess(lp); err != nil {
return err
// setProfilePrefs is like [profileManager.SetPrefs], but sets prefs for the specified [ipn.LoginProfile],
// returning a read-only view of the updated profile on success. If the specified profile is nil,
// it defaults to the current profile. If the profile is not accessible by the current user,
// the method returns an [errProfileAccessDenied].
func (pm *profileManager) setProfilePrefs(lp *ipn.LoginProfile, prefsIn ipn.PrefsView, np ipn.NetworkProfile) (ipn.LoginProfileView, error) {
isCurrentProfile := lp == nil || (lp.ID != "" && lp.ID == pm.currentProfile.ID())
if isCurrentProfile {
lp = pm.CurrentProfile().AsStruct()
}
if err := pm.checkProfileAccess(lp.View()); err != nil {
return ipn.LoginProfileView{}, err
}
// An empty profile.ID indicates that the profile is new, the node info wasn't available,
@@ -291,23 +348,29 @@ func (pm *profileManager) SetProfilePrefs(lp *ipn.LoginProfile, prefsIn ipn.Pref
lp.UserProfile = up
lp.NetworkProfile = np
// Update the current profile view to reflect the changes
// if the specified profile is the current profile.
if isCurrentProfile {
pm.currentProfile = lp.View()
}
// An empty profile.ID indicates that the node info is not available yet,
// and the profile doesn't need to be saved on disk.
if lp.ID != "" {
pm.knownProfiles[lp.ID] = lp
pm.knownProfiles[lp.ID] = lp.View()
if err := pm.writeKnownProfiles(); err != nil {
return err
return ipn.LoginProfileView{}, err
}
// Clone prefsIn and create a read-only view as a safety measure to
// prevent accidental preference mutations, both externally and internally.
if err := pm.setProfilePrefsNoPermCheck(lp, prefsIn.AsStruct().View()); err != nil {
return err
if err := pm.setProfilePrefsNoPermCheck(lp.View(), prefsIn.AsStruct().View()); err != nil {
return ipn.LoginProfileView{}, err
}
}
return nil
return lp.View(), nil
}
func newUnusedID(knownProfiles map[ipn.ProfileID]*ipn.LoginProfile) (ipn.ProfileID, ipn.StateKey) {
func newUnusedID(knownProfiles map[ipn.ProfileID]ipn.LoginProfileView) (ipn.ProfileID, ipn.StateKey) {
var idb [2]byte
for {
rand.Read(idb[:])
@@ -326,14 +389,14 @@ func newUnusedID(knownProfiles map[ipn.ProfileID]*ipn.LoginProfile) (ipn.Profile
// The method does not perform any additional checks on the specified
// profile, such as verifying the caller's access rights or checking
// if another profile for the same node already exists.
func (pm *profileManager) setProfilePrefsNoPermCheck(profile *ipn.LoginProfile, clonedPrefs ipn.PrefsView) error {
func (pm *profileManager) setProfilePrefsNoPermCheck(profile ipn.LoginProfileView, clonedPrefs ipn.PrefsView) error {
isCurrentProfile := pm.currentProfile == profile
if isCurrentProfile {
pm.prefs = clonedPrefs
pm.updateHealth()
}
if profile.Key != "" {
if err := pm.writePrefsToStore(profile.Key, clonedPrefs); err != nil {
if profile.Key() != "" {
if err := pm.writePrefsToStore(profile.Key(), clonedPrefs); err != nil {
return err
}
} else if !isCurrentProfile {
@@ -362,38 +425,33 @@ func (pm *profileManager) writePrefsToStore(key ipn.StateKey, prefs ipn.PrefsVie
}
// Profiles returns the list of known profiles accessible to the current user.
func (pm *profileManager) Profiles() []ipn.LoginProfile {
allProfiles := pm.allProfiles()
out := make([]ipn.LoginProfile, len(allProfiles))
for i, p := range allProfiles {
out[i] = *p
}
return out
func (pm *profileManager) Profiles() []ipn.LoginProfileView {
return pm.allProfilesFor(pm.currentUserID)
}
// ProfileByID returns a profile with the given id, if it is accessible to the current user.
// If the profile exists but is not accessible to the current user, it returns an [errProfileAccessDenied].
// If the profile does not exist, it returns an [errProfileNotFound].
func (pm *profileManager) ProfileByID(id ipn.ProfileID) (ipn.LoginProfile, error) {
func (pm *profileManager) ProfileByID(id ipn.ProfileID) (ipn.LoginProfileView, error) {
kp, err := pm.profileByIDNoPermCheck(id)
if err != nil {
return ipn.LoginProfile{}, err
return ipn.LoginProfileView{}, err
}
if err := pm.checkProfileAccess(kp); err != nil {
return ipn.LoginProfile{}, err
return ipn.LoginProfileView{}, err
}
return *kp, nil
return kp, nil
}
// profileByIDNoPermCheck is like [profileManager.ProfileByID], but it doesn't
// check user's access rights to the profile.
func (pm *profileManager) profileByIDNoPermCheck(id ipn.ProfileID) (*ipn.LoginProfile, error) {
if id == pm.currentProfile.ID {
func (pm *profileManager) profileByIDNoPermCheck(id ipn.ProfileID) (ipn.LoginProfileView, error) {
if id == pm.currentProfile.ID() {
return pm.currentProfile, nil
}
kp, ok := pm.knownProfiles[id]
if !ok {
return nil, errProfileNotFound
return ipn.LoginProfileView{}, errProfileNotFound
}
return kp, nil
}
@@ -412,11 +470,11 @@ func (pm *profileManager) ProfilePrefs(id ipn.ProfileID) (ipn.PrefsView, error)
return pm.profilePrefs(kp)
}
func (pm *profileManager) profilePrefs(p *ipn.LoginProfile) (ipn.PrefsView, error) {
if p.ID == pm.currentProfile.ID {
func (pm *profileManager) profilePrefs(p ipn.LoginProfileView) (ipn.PrefsView, error) {
if p.ID() == pm.currentProfile.ID() {
return pm.prefs, nil
}
return pm.loadSavedPrefs(p.Key)
return pm.loadSavedPrefs(p.Key())
}
// SwitchProfile switches to the profile with the given id.
@@ -429,14 +487,14 @@ func (pm *profileManager) SwitchProfile(id ipn.ProfileID) error {
if !ok {
return errProfileNotFound
}
if pm.currentProfile != nil && kp.ID == pm.currentProfile.ID && pm.prefs.Valid() {
if pm.currentProfile.Valid() && kp.ID() == pm.currentProfile.ID() && pm.prefs.Valid() {
return nil
}
if err := pm.checkProfileAccess(kp); err != nil {
return fmt.Errorf("%w: profile %q is not accessible to the current user", err, id)
}
prefs, err := pm.loadSavedPrefs(kp.Key)
prefs, err := pm.loadSavedPrefs(kp.Key())
if err != nil {
return err
}
@@ -459,8 +517,8 @@ func (pm *profileManager) SwitchToDefaultProfile() error {
// setProfileAsUserDefault sets the specified profile as the default for the current user.
// It returns an [errProfileAccessDenied] if the specified profile is not accessible to the current user.
func (pm *profileManager) setProfileAsUserDefault(profile *ipn.LoginProfile) error {
if profile.Key == "" {
func (pm *profileManager) setProfileAsUserDefault(profile ipn.LoginProfileView) error {
if profile.Key() == "" {
// The profile has not been persisted yet; ignore it for now.
return nil
}
@@ -468,7 +526,7 @@ func (pm *profileManager) setProfileAsUserDefault(profile *ipn.LoginProfile) err
return errProfileAccessDenied
}
k := ipn.CurrentProfileKey(string(pm.currentUserID))
return pm.WriteState(k, []byte(profile.Key))
return pm.WriteState(k, []byte(profile.Key()))
}
func (pm *profileManager) loadSavedPrefs(key ipn.StateKey) (ipn.PrefsView, error) {
@@ -507,10 +565,10 @@ func (pm *profileManager) loadSavedPrefs(key ipn.StateKey) (ipn.PrefsView, error
return savedPrefs.View(), nil
}
// CurrentProfile returns the current LoginProfile.
// CurrentProfile returns a read-only [ipn.LoginProfileView] of the current profile.
// The value may be zero if the profile is not persisted.
func (pm *profileManager) CurrentProfile() ipn.LoginProfile {
return *pm.currentProfile
func (pm *profileManager) CurrentProfile() ipn.LoginProfileView {
return pm.currentProfile
}
// errProfileNotFound is returned by methods that accept a ProfileID
@@ -533,7 +591,7 @@ var errProfileAccessDenied = errors.New("profile access denied")
// recommended to call [profileManager.SwitchProfile] first.
func (pm *profileManager) DeleteProfile(id ipn.ProfileID) error {
metricDeleteProfile.Add(1)
if id == pm.currentProfile.ID {
if id == pm.currentProfile.ID() {
return pm.deleteCurrentProfile()
}
kp, ok := pm.knownProfiles[id]
@@ -550,7 +608,7 @@ func (pm *profileManager) deleteCurrentProfile() error {
if err := pm.checkProfileAccess(pm.currentProfile); err != nil {
return err
}
if pm.currentProfile.ID == "" {
if pm.currentProfile.ID() == "" {
// Deleting the in-memory only new profile, just create a new one.
pm.NewProfile()
return nil
@@ -560,14 +618,14 @@ func (pm *profileManager) deleteCurrentProfile() error {
// deleteProfileNoPermCheck is like [profileManager.DeleteProfile],
// but it doesn't check user's access rights to the profile.
func (pm *profileManager) deleteProfileNoPermCheck(profile *ipn.LoginProfile) error {
if profile.ID == pm.currentProfile.ID {
func (pm *profileManager) deleteProfileNoPermCheck(profile ipn.LoginProfileView) error {
if profile.ID() == pm.currentProfile.ID() {
pm.NewProfile()
}
if err := pm.WriteState(profile.Key, nil); err != nil {
if err := pm.WriteState(profile.Key(), nil); err != nil {
return err
}
delete(pm.knownProfiles, profile.ID)
delete(pm.knownProfiles, profile.ID())
return pm.writeKnownProfiles()
}
@@ -578,7 +636,7 @@ func (pm *profileManager) DeleteAllProfilesForUser() error {
currentProfileDeleted := false
writeKnownProfiles := func() error {
if currentProfileDeleted || pm.currentProfile.ID == "" {
if currentProfileDeleted || pm.currentProfile.ID() == "" {
pm.NewProfile()
}
return pm.writeKnownProfiles()
@@ -589,14 +647,14 @@ func (pm *profileManager) DeleteAllProfilesForUser() error {
// Skip profiles we don't have access to.
continue
}
if err := pm.WriteState(kp.Key, nil); err != nil {
if err := pm.WriteState(kp.Key(), nil); err != nil {
// Write to remove references to profiles we've already deleted, but
// return the original error.
writeKnownProfiles()
return err
}
delete(pm.knownProfiles, kp.ID)
if kp.ID == pm.currentProfile.ID {
delete(pm.knownProfiles, kp.ID())
if kp.ID() == pm.currentProfile.ID() {
currentProfileDeleted = true
}
}
@@ -633,26 +691,27 @@ func (pm *profileManager) NewProfileForUser(uid ipn.WindowsUserID) {
pm.prefs = defaultPrefs
pm.updateHealth()
pm.currentProfile = &ipn.LoginProfile{LocalUserID: uid}
newProfile := &ipn.LoginProfile{LocalUserID: uid}
pm.currentProfile = newProfile.View()
}
// newProfileWithPrefs creates a new profile with the specified prefs and assigns
// the specified uid as the profile owner. If switchNow is true, it switches to the
// newly created profile immediately. It returns the newly created profile on success,
// or an error on failure.
func (pm *profileManager) newProfileWithPrefs(uid ipn.WindowsUserID, prefs ipn.PrefsView, switchNow bool) (*ipn.LoginProfile, error) {
func (pm *profileManager) newProfileWithPrefs(uid ipn.WindowsUserID, prefs ipn.PrefsView, switchNow bool) (ipn.LoginProfileView, error) {
metricNewProfile.Add(1)
profile := &ipn.LoginProfile{LocalUserID: uid}
if err := pm.SetProfilePrefs(profile, prefs, ipn.NetworkProfile{}); err != nil {
return nil, err
profile, err := pm.setProfilePrefs(&ipn.LoginProfile{LocalUserID: uid}, prefs, ipn.NetworkProfile{})
if err != nil {
return ipn.LoginProfileView{}, err
}
if switchNow {
pm.currentProfile = profile
pm.prefs = prefs.AsStruct().View()
pm.updateHealth()
if err := pm.setProfileAsUserDefault(profile); err != nil {
return nil, err
return ipn.LoginProfileView{}, err
}
}
return profile, nil
@@ -711,8 +770,8 @@ func readAutoStartKey(store ipn.StateStore, goos string) (ipn.StateKey, error) {
return ipn.StateKey(autoStartKey), nil
}
func readKnownProfiles(store ipn.StateStore) (map[ipn.ProfileID]*ipn.LoginProfile, error) {
var knownProfiles map[ipn.ProfileID]*ipn.LoginProfile
func readKnownProfiles(store ipn.StateStore) (map[ipn.ProfileID]ipn.LoginProfileView, error) {
var knownProfiles map[ipn.ProfileID]ipn.LoginProfileView
prfB, err := store.ReadState(ipn.KnownProfilesStateKey)
switch err {
case nil:
@@ -720,7 +779,7 @@ func readKnownProfiles(store ipn.StateStore) (map[ipn.ProfileID]*ipn.LoginProfil
return nil, fmt.Errorf("unmarshaling known profiles: %w", err)
}
case ipn.ErrStateNotExist:
knownProfiles = make(map[ipn.ProfileID]*ipn.LoginProfile)
knownProfiles = make(map[ipn.ProfileID]ipn.LoginProfileView)
default:
return nil, fmt.Errorf("calling ReadState on state store: %w", err)
}
@@ -749,17 +808,17 @@ func newProfileManagerWithGOOS(store ipn.StateStore, logf logger.Logf, ht *healt
if stateKey != "" {
for _, v := range knownProfiles {
if v.Key == stateKey {
if v.Key() == stateKey {
pm.currentProfile = v
}
}
if pm.currentProfile == nil {
if !pm.currentProfile.Valid() {
if suf, ok := strings.CutPrefix(string(stateKey), "user-"); ok {
pm.currentUserID = ipn.WindowsUserID(suf)
}
pm.NewProfile()
} else {
pm.currentUserID = pm.currentProfile.LocalUserID
pm.currentUserID = pm.currentProfile.LocalUserID()
}
prefs, err := pm.loadSavedPrefs(stateKey)
if err != nil {
@@ -788,18 +847,18 @@ func newProfileManagerWithGOOS(store ipn.StateStore, logf logger.Logf, ht *healt
return pm, nil
}
func (pm *profileManager) migrateFromLegacyPrefs(uid ipn.WindowsUserID, switchNow bool) (*ipn.LoginProfile, error) {
func (pm *profileManager) migrateFromLegacyPrefs(uid ipn.WindowsUserID, switchNow bool) (ipn.LoginProfileView, error) {
metricMigration.Add(1)
sentinel, prefs, err := pm.loadLegacyPrefs(uid)
if err != nil {
metricMigrationError.Add(1)
return nil, fmt.Errorf("load legacy prefs: %w", err)
return ipn.LoginProfileView{}, fmt.Errorf("load legacy prefs: %w", err)
}
pm.dlogf("loaded legacy preferences; sentinel=%q", sentinel)
profile, err := pm.newProfileWithPrefs(uid, prefs, switchNow)
if err != nil {
metricMigrationError.Add(1)
return nil, fmt.Errorf("migrating _daemon profile: %w", err)
return ipn.LoginProfileView{}, fmt.Errorf("migrating _daemon profile: %w", err)
}
pm.completeMigration(sentinel)
pm.dlogf("completed legacy preferences migration with sentinel=%q", sentinel)
@@ -809,8 +868,8 @@ func (pm *profileManager) migrateFromLegacyPrefs(uid ipn.WindowsUserID, switchNo
func (pm *profileManager) requiresBackfill() bool {
return pm != nil &&
pm.currentProfile != nil &&
pm.currentProfile.NetworkProfile.RequiresBackfill()
pm.currentProfile.Valid() &&
pm.currentProfile.NetworkProfile().RequiresBackfill()
}
var (

View File

@@ -54,8 +54,9 @@ var ErrETagMismatch = errors.New("etag mismatch")
var serveHTTPContextKey ctxkey.Key[*serveHTTPContext]
type serveHTTPContext struct {
SrcAddr netip.AddrPort
DestPort uint16
SrcAddr netip.AddrPort
ForVIPService tailcfg.ServiceName // "" means local
DestPort uint16
// provides funnel-specific context, nil if not funneled
Funnel *funnelFlow
@@ -242,8 +243,7 @@ func (b *LocalBackend) updateServeTCPPortNetMapAddrListenersLocked(ports []uint1
}
addrs := nm.GetAddresses()
for i := range addrs.Len() {
a := addrs.At(i)
for _, a := range addrs.All() {
for _, p := range ports {
addrPort := netip.AddrPortFrom(a.Addr(), p)
if _, ok := b.serveListeners[addrPort]; ok {
@@ -276,6 +276,12 @@ func (b *LocalBackend) setServeConfigLocked(config *ipn.ServeConfig, etag string
return errors.New("can't reconfigure tailscaled when using a config file; config file is locked")
}
if config != nil {
if err := config.CheckValidServicesConfig(); err != nil {
return err
}
}
nm := b.netMap
if nm == nil {
return errors.New("netMap is nil")
@@ -312,7 +318,7 @@ func (b *LocalBackend) setServeConfigLocked(config *ipn.ServeConfig, etag string
bs = j
}
profileID := b.pm.CurrentProfile().ID
profileID := b.pm.CurrentProfile().ID()
confKey := ipn.ServeConfigKey(profileID)
if err := b.store.WriteState(confKey, bs); err != nil {
return fmt.Errorf("writing ServeConfig to StateStore: %w", err)
@@ -327,7 +333,7 @@ func (b *LocalBackend) setServeConfigLocked(config *ipn.ServeConfig, etag string
if b.serveConfig.Valid() {
has = b.serveConfig.Foreground().Contains
}
prevConfig.Foreground().Range(func(k string, v ipn.ServeConfigView) (cont bool) {
for k := range prevConfig.Foreground().All() {
if !has(k) {
for _, sess := range b.notifyWatchers {
if sess.sessionID == k {
@@ -335,8 +341,7 @@ func (b *LocalBackend) setServeConfigLocked(config *ipn.ServeConfig, etag string
}
}
}
return true
})
}
}
return nil
@@ -434,6 +439,105 @@ func (b *LocalBackend) HandleIngressTCPConn(ingressPeer tailcfg.NodeView, target
handler(c)
}
// tcpHandlerForVIPService returns a handler for a TCP connection to a VIP service
// that is being served via the ipn.ServeConfig. It returns nil if the destination
// address is not a VIP service or if the VIP service does not have a TCP handler set.
func (b *LocalBackend) tcpHandlerForVIPService(dstAddr, srcAddr netip.AddrPort) (handler func(net.Conn) error) {
b.mu.Lock()
sc := b.serveConfig
ipVIPServiceMap := b.ipVIPServiceMap
b.mu.Unlock()
if !sc.Valid() {
return nil
}
dport := dstAddr.Port()
dstSvc, ok := ipVIPServiceMap[dstAddr.Addr()]
if !ok {
return nil
}
tcph, ok := sc.FindServiceTCP(dstSvc, dstAddr.Port())
if !ok {
b.logf("The destination service doesn't have a TCP handler set.")
return nil
}
if tcph.HTTPS() || tcph.HTTP() {
hs := &http.Server{
Handler: http.HandlerFunc(b.serveWebHandler),
BaseContext: func(_ net.Listener) context.Context {
return serveHTTPContextKey.WithValue(context.Background(), &serveHTTPContext{
SrcAddr: srcAddr,
ForVIPService: dstSvc,
DestPort: dport,
})
},
}
if tcph.HTTPS() {
// TODO(kevinliang10): just leaving this TLS cert creation as if we don't have other
// hostnames, but for services this getTLSServeCetForPort will need a version that also take
// in the hostname. How to store the TLS cert is still being discussed.
hs.TLSConfig = &tls.Config{
GetCertificate: b.getTLSServeCertForPort(dport, dstSvc),
}
return func(c net.Conn) error {
return hs.ServeTLS(netutil.NewOneConnListener(c, nil), "", "")
}
}
return func(c net.Conn) error {
return hs.Serve(netutil.NewOneConnListener(c, nil))
}
}
if backDst := tcph.TCPForward(); backDst != "" {
return func(conn net.Conn) error {
defer conn.Close()
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
backConn, err := b.dialer.SystemDial(ctx, "tcp", backDst)
cancel()
if err != nil {
b.logf("localbackend: failed to TCP proxy port %v (from %v) to %s: %v", dport, srcAddr, backDst, err)
return nil
}
defer backConn.Close()
if sni := tcph.TerminateTLS(); sni != "" {
conn = tls.Server(conn, &tls.Config{
GetCertificate: func(hi *tls.ClientHelloInfo) (*tls.Certificate, error) {
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel()
pair, err := b.GetCertPEM(ctx, sni)
if err != nil {
return nil, err
}
cert, err := tls.X509KeyPair(pair.CertPEM, pair.KeyPEM)
if err != nil {
return nil, err
}
return &cert, nil
},
})
}
errc := make(chan error, 1)
go func() {
_, err := io.Copy(backConn, conn)
errc <- err
}()
go func() {
_, err := io.Copy(conn, backConn)
errc <- err
}()
return <-errc
}
}
return nil
}
// tcpHandlerForServe returns a handler for a TCP connection to be served via
// the ipn.ServeConfig. The funnelFlow can be nil if this is not a funneled
// connection.
@@ -464,7 +568,7 @@ func (b *LocalBackend) tcpHandlerForServe(dport uint16, srcAddr netip.AddrPort,
}
if tcph.HTTPS() {
hs.TLSConfig = &tls.Config{
GetCertificate: b.getTLSServeCertForPort(dport),
GetCertificate: b.getTLSServeCertForPort(dport, ""),
}
return func(c net.Conn) error {
return hs.ServeTLS(netutil.NewOneConnListener(c, nil), "", "")
@@ -544,7 +648,7 @@ func (b *LocalBackend) getServeHandler(r *http.Request) (_ ipn.HTTPHandlerView,
b.logf("[unexpected] localbackend: no serveHTTPContext in request")
return z, "", false
}
wsc, ok := b.webServerConfig(hostname, sctx.DestPort)
wsc, ok := b.webServerConfig(hostname, sctx.ForVIPService, sctx.DestPort)
if !ok {
return z, "", false
}
@@ -902,7 +1006,7 @@ func allNumeric(s string) bool {
return s != ""
}
func (b *LocalBackend) webServerConfig(hostname string, port uint16) (c ipn.WebServerConfigView, ok bool) {
func (b *LocalBackend) webServerConfig(hostname string, forVIPService tailcfg.ServiceName, port uint16) (c ipn.WebServerConfigView, ok bool) {
key := ipn.HostPort(fmt.Sprintf("%s:%v", hostname, port))
b.mu.Lock()
@@ -911,15 +1015,18 @@ func (b *LocalBackend) webServerConfig(hostname string, port uint16) (c ipn.WebS
if !b.serveConfig.Valid() {
return c, false
}
if forVIPService != "" {
return b.serveConfig.FindServiceWeb(forVIPService, key)
}
return b.serveConfig.FindWeb(key)
}
func (b *LocalBackend) getTLSServeCertForPort(port uint16) func(hi *tls.ClientHelloInfo) (*tls.Certificate, error) {
func (b *LocalBackend) getTLSServeCertForPort(port uint16, forVIPService tailcfg.ServiceName) func(hi *tls.ClientHelloInfo) (*tls.Certificate, error) {
return func(hi *tls.ClientHelloInfo) (*tls.Certificate, error) {
if hi == nil || hi.ServerName == "" {
return nil, errors.New("no SNI ServerName")
}
_, ok := b.webServerConfig(hi.ServerName, port)
_, ok := b.webServerConfig(hi.ServerName, forVIPService, port)
if !ok {
return nil, errors.New("no webserver configured for name/port")
}

View File

@@ -24,10 +24,10 @@ import (
"strings"
"sync"
"github.com/tailscale/golang-x-crypto/ssh"
"go4.org/mem"
"golang.org/x/crypto/ssh"
"tailscale.com/tailcfg"
"tailscale.com/util/lineread"
"tailscale.com/util/lineiter"
"tailscale.com/util/mak"
)
@@ -80,30 +80,32 @@ func (b *LocalBackend) getSSHUsernames(req *tailcfg.C2NSSHUsernamesRequest) (*ta
if err != nil {
return nil, err
}
lineread.Reader(bytes.NewReader(out), func(line []byte) error {
for line := range lineiter.Bytes(out) {
line = bytes.TrimSpace(line)
if len(line) == 0 || line[0] == '_' {
return nil
continue
}
add(string(line))
return nil
})
}
default:
lineread.File("/etc/passwd", func(line []byte) error {
for lr := range lineiter.File("/etc/passwd") {
line, err := lr.Value()
if err != nil {
break
}
line = bytes.TrimSpace(line)
if len(line) == 0 || line[0] == '#' || line[0] == '_' {
return nil
continue
}
if mem.HasSuffix(mem.B(line), mem.S("/nologin")) ||
mem.HasSuffix(mem.B(line), mem.S("/false")) {
return nil
continue
}
colon := bytes.IndexByte(line, ':')
if colon != -1 {
add(string(line[:colon]))
}
return nil
})
}
}
return res, nil
}

View File

@@ -17,7 +17,7 @@ import (
"sync"
"time"
"tailscale.com/client/tailscale"
"tailscale.com/client/local"
"tailscale.com/client/web"
"tailscale.com/logtail/backoff"
"tailscale.com/net/netutil"
@@ -36,16 +36,16 @@ type webClient struct {
server *web.Server // or nil, initialized lazily
// lc optionally specifies a LocalClient to use to connect
// lc optionally specifies a local.Client to use to connect
// to the localapi for this tailscaled instance.
// If nil, a default is used.
lc *tailscale.LocalClient
lc *local.Client
}
// ConfigureWebClient configures b.web prior to use.
// Specifially, it sets b.web.lc to the provided LocalClient.
// Specifially, it sets b.web.lc to the provided local.Client.
// If provided as nil, b.web.lc is cleared out.
func (b *LocalBackend) ConfigureWebClient(lc *tailscale.LocalClient) {
func (b *LocalBackend) ConfigureWebClient(lc *local.Client) {
b.webClient.mu.Lock()
defer b.webClient.mu.Unlock()
b.webClient.lc = lc
@@ -121,8 +121,8 @@ func (b *LocalBackend) updateWebClientListenersLocked() {
}
addrs := b.netMap.GetAddresses()
for i := range addrs.Len() {
addrPort := netip.AddrPortFrom(addrs.At(i).Addr(), webClientPort)
for _, pfx := range addrs.All() {
addrPort := netip.AddrPortFrom(pfx.Addr(), webClientPort)
if _, ok := b.webClientListeners[addrPort]; ok {
continue // already listening
}

View File

@@ -9,14 +9,14 @@ import (
"errors"
"net"
"tailscale.com/client/tailscale"
"tailscale.com/client/local"
)
const webClientPort = 5252
type webClient struct{}
func (b *LocalBackend) ConfigureWebClient(lc *tailscale.LocalClient) {}
func (b *LocalBackend) ConfigureWebClient(lc *local.Client) {}
func (b *LocalBackend) webClientGetOrInit() error {
return errors.New("not implemented")

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