Compare commits

..

1 Commits

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

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

Fixes #7398

Signed-off-by: Aaron Klotz <aaron@tailscale.com>
2023-02-28 13:11:47 -07:00
350 changed files with 5402 additions and 19936 deletions

View File

@@ -17,7 +17,7 @@ concurrency:
cancel-in-progress: true
jobs:
update-licenses:
tailscale:
runs-on: ubuntu-latest
steps:
@@ -25,7 +25,7 @@ jobs:
uses: actions/checkout@v3
- name: Set up Go
uses: actions/setup-go@v4
uses: actions/setup-go@v3
with:
go-version-file: go.mod
@@ -42,7 +42,7 @@ jobs:
go-licenses report tailscale.com/cmd/tailscale tailscale.com/cmd/tailscaled > licenses/tailscale.md --template .github/licenses.tmpl
- name: Get access token
uses: tibdex/github-app-token@b62528385c34dbc9f38e5f4225ac829252d1ea92 # v1.8.0
uses: tibdex/github-app-token@f717b5ecd4534d3c4df4ce9b5c1c2214f0f7cd06 # v1.6.0
id: generate-token
with:
app_id: ${{ secrets.LICENSING_APP_ID }}
@@ -50,7 +50,7 @@ jobs:
private_key: ${{ secrets.LICENSING_APP_PRIVATE_KEY }}
- name: Send pull request
uses: peter-evans/create-pull-request@5b4a9f6a9e2af26e5f02351490b90d01eb8ec1e5 #v5.0.0
uses: peter-evans/create-pull-request@ad43dccb4d726ca8514126628bec209b8354b6dd #v4.1.4
with:
token: ${{ steps.generate-token.outputs.token }}
author: License Updater <noreply@tailscale.com>

View File

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

View File

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

View File

@@ -0,0 +1,31 @@
name: "@tailscale/connect npm publish"
on: workflow_dispatch
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Set up node
uses: actions/setup-node@v3
with:
node-version: "16.x"
registry-url: "https://registry.npmjs.org"
- name: Build package
# Build with build_dist.sh to ensure that version information is embedded.
# GOROOT is specified so that the Go/Wasm that is trigged by build-pk
# also picks up our custom Go toolchain.
run: |
export TS_USE_TOOLCHAIN=1
./build_dist.sh tailscale.com/cmd/tsconnect
GOROOT="${HOME}/.cache/tailscale-go" ./tsconnect build-pkg
- name: Publish
env:
NODE_AUTH_TOKEN: ${{ secrets.TSCONNECT_NPM_PUBLISH_AUTH_TOKEN }}
run: ./tool/yarn --cwd ./cmd/tsconnect/pkg publish --access public

View File

@@ -16,7 +16,7 @@ concurrency:
cancel-in-progress: true
jobs:
update-flake:
tailscale:
runs-on: ubuntu-latest
steps:
@@ -27,7 +27,7 @@ jobs:
run: ./update-flake.sh
- name: Get access token
uses: tibdex/github-app-token@b62528385c34dbc9f38e5f4225ac829252d1ea92 # v1.8.0
uses: tibdex/github-app-token@f717b5ecd4534d3c4df4ce9b5c1c2214f0f7cd06 # v1.6.0
id: generate-token
with:
app_id: ${{ secrets.LICENSING_APP_ID }}
@@ -35,7 +35,7 @@ jobs:
private_key: ${{ secrets.LICENSING_APP_PRIVATE_KEY }}
- name: Send pull request
uses: peter-evans/create-pull-request@5b4a9f6a9e2af26e5f02351490b90d01eb8ec1e5 #v5.0.0
uses: peter-evans/create-pull-request@ad43dccb4d726ca8514126628bec209b8354b6dd #v4.1.4
with:
token: ${{ steps.generate-token.outputs.token }}
author: Flakes Updater <noreply@tailscale.com>

View File

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

View File

@@ -1 +1 @@
1.39.0
1.37.0

1843
api.md

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@@ -16,7 +16,7 @@ if [ -n "${TS_USE_TOOLCHAIN:-}" ]; then
go="./tool/go"
fi
eval `GOOS=$($go env GOHOSTOS) GOARCH=$($go env GOHOSTARCH) $go run ./cmd/mkversion`
eval `$go run ./cmd/mkversion`
if [ "$1" = "shellvars" ]; then
cat <<EOF

View File

@@ -103,7 +103,7 @@ func (c *Client) ACL(ctx context.Context) (acl *ACL, err error) {
// it as a string.
// HuJSON is JSON with a few modifications to make it more human-friendly. The primary
// changes are allowing comments and trailing comments. See the following links for more info:
// https://tailscale.com/s/acl-format
// https://tailscale.com/kb/1018/acls?q=acl#tailscale-acl-policy-format
// https://github.com/tailscale/hujson
func (c *Client) ACLHuJSON(ctx context.Context) (acl *ACLHuJSON, err error) {
// Format return errors to be descriptive.
@@ -436,7 +436,7 @@ func (c *Client) ValidateACLJSON(ctx context.Context, source, dest string) (test
}
}()
tests := []ACLTest{{User: source, Allow: []string{dest}}}
tests := []ACLTest{ACLTest{User: source, Allow: []string{dest}}}
postData, err := json.Marshal(tests)
if err != nil {
return nil, err

View File

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

View File

@@ -36,7 +36,6 @@ import (
"tailscale.com/tailcfg"
"tailscale.com/tka"
"tailscale.com/types/key"
"tailscale.com/types/tkatype"
)
// defaultLocalClient is the default LocalClient when using the legacy
@@ -96,9 +95,8 @@ func (lc *LocalClient) defaultDialer(ctx context.Context, network, addr string)
// a TCP server on a random port, find the random port. For HTTP connections,
// we don't send the token. It gets added in an HTTP Basic-Auth header.
if port, _, err := safesocket.LocalTCPPortAndToken(); err == nil {
// We use 127.0.0.1 and not "localhost" (issue 7851).
var d net.Dialer
return d.DialContext(ctx, "tcp", "127.0.0.1:"+strconv.Itoa(port))
return d.DialContext(ctx, "tcp", "localhost:"+strconv.Itoa(port))
}
}
s := safesocket.DefaultConnectionStrategy(lc.socket())
@@ -369,34 +367,6 @@ func (lc *LocalClient) DebugAction(ctx context.Context, action string) error {
return nil
}
// DebugPortmap invokes the debug-portmap endpoint, and returns an
// io.ReadCloser that can be used to read the logs that are printed during this
// process.
func (lc *LocalClient) DebugPortmap(ctx context.Context, duration time.Duration, ty, gwSelf string) (io.ReadCloser, error) {
vals := make(url.Values)
vals.Set("duration", duration.String())
vals.Set("type", ty)
if gwSelf != "" {
vals.Set("gateway_and_self", gwSelf)
}
req, err := http.NewRequestWithContext(ctx, "GET", "http://"+apitype.LocalAPIHost+"/localapi/v0/debug-portmap?"+vals.Encode(), nil)
if err != nil {
return nil, err
}
res, err := lc.doLocalRequestNiceError(req)
if err != nil {
return nil, err
}
if res.StatusCode != 200 {
body, _ := io.ReadAll(res.Body)
res.Body.Close()
return nil, fmt.Errorf("HTTP %s: %s", res.Status, body)
}
return res.Body, nil
}
// 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 {
@@ -851,30 +821,6 @@ func (lc *LocalClient) NetworkLockInit(ctx context.Context, keys []tka.Key, disa
return decodeJSON[*ipnstate.NetworkLockStatus](body)
}
// 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) {
encodedPrivate, err := tkaKey.MarshalText()
if err != nil {
return "", err
}
var b bytes.Buffer
type wrapRequest struct {
TSKey string
TKAKey string // key.NLPrivate.MarshalText
}
if err := json.NewEncoder(&b).Encode(wrapRequest{TSKey: preauthKey, TKAKey: string(encodedPrivate)}); err != nil {
return "", err
}
body, err := lc.send(ctx, "POST", "/localapi/v0/tka/wrap-preauth-key", 200, &b)
if err != nil {
return "", fmt.Errorf("error: %w", err)
}
return string(body), nil
}
// NetworkLockModify adds and/or removes key(s) to the tailnet key authority.
func (lc *LocalClient) NetworkLockModify(ctx context.Context, addKeys, removeKeys []tka.Key) error {
var b bytes.Buffer
@@ -912,15 +858,6 @@ func (lc *LocalClient) NetworkLockSign(ctx context.Context, nodeKey key.NodePubl
return nil
}
// NetworkLockAffectedSigs returns all signatures signed by the specified keyID.
func (lc *LocalClient) 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)
}
return decodeJSON[[]tkatype.MarshaledSignature](body)
}
// NetworkLockLog returns up to maxEntries number of changes to network-lock state.
func (lc *LocalClient) NetworkLockLog(ctx context.Context, maxEntries int) ([]ipnstate.NetworkLockUpdate, error) {
v := url.Values{}
@@ -1102,6 +1039,7 @@ func (lc *LocalClient) StreamDebugCapture(ctx context.Context) (io.ReadCloser, e
}
res, err := lc.doLocalRequestNiceError(req)
if err != nil {
res.Body.Close()
return nil, err
}
if res.StatusCode != 200 {

View File

@@ -6,28 +6,138 @@
package main
import (
"bytes"
"context"
"crypto/tls"
"crypto/x509"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"path/filepath"
"strings"
"time"
"tailscale.com/kube"
"tailscale.com/tailcfg"
"tailscale.com/util/multierr"
)
// checkSecretPermissions checks the secret access permissions of the current
// pod. It returns an error if the basic permissions tailscale needs are
// missing, and reports whether the patch permission is additionally present.
//
// Errors encountered during the access checking process are logged, but ignored
// so that the pod tries to fail alive if the permissions exist and there's just
// something wrong with SelfSubjectAccessReviews. There shouldn't be, pods
// should always be able to use SSARs to assess their own permissions, but since
// we didn't use to check permissions this way we'll be cautious in case some
// old version of k8s deviates from the current behavior.
func checkSecretPermissions(ctx context.Context, secretName string) (canPatch bool, err error) {
var errs []error
for _, verb := range []string{"get", "update"} {
ok, err := checkPermission(ctx, verb, secretName)
if err != nil {
log.Printf("error checking %s permission on secret %s: %v", verb, secretName, err)
} else if !ok {
errs = append(errs, fmt.Errorf("missing %s permission on secret %q", verb, secretName))
}
}
if len(errs) > 0 {
return false, multierr.New(errs...)
}
ok, err := checkPermission(ctx, "patch", secretName)
if err != nil {
log.Printf("error checking patch permission on secret %s: %v", secretName, err)
return false, nil
}
return ok, nil
}
// checkPermission reports whether the current pod has permission to use the
// given verb (e.g. get, update, patch) on secretName.
func checkPermission(ctx context.Context, verb, secretName string) (bool, error) {
sar := map[string]any{
"apiVersion": "authorization.k8s.io/v1",
"kind": "SelfSubjectAccessReview",
"spec": map[string]any{
"resourceAttributes": map[string]any{
"namespace": kubeNamespace,
"verb": verb,
"resource": "secrets",
"name": secretName,
},
},
}
bs, err := json.Marshal(sar)
if err != nil {
return false, err
}
req, err := http.NewRequest("POST", "/apis/authorization.k8s.io/v1/selfsubjectaccessreviews", bytes.NewReader(bs))
if err != nil {
return false, err
}
resp, err := doKubeRequest(ctx, req)
if err != nil {
return false, err
}
defer resp.Body.Close()
bs, err = io.ReadAll(resp.Body)
if err != nil {
return false, err
}
var res struct {
Status struct {
Allowed bool `json:"allowed"`
} `json:"status"`
}
if err := json.Unmarshal(bs, &res); err != nil {
return false, err
}
return res.Status.Allowed, nil
}
// findKeyInKubeSecret inspects the kube secret secretName for a data
// field called "authkey", and returns its value if present.
func findKeyInKubeSecret(ctx context.Context, secretName string) (string, error) {
s, err := kc.GetSecret(ctx, secretName)
req, err := http.NewRequest("GET", fmt.Sprintf("/api/v1/namespaces/%s/secrets/%s", kubeNamespace, secretName), nil)
if err != nil {
return "", err
}
ak, ok := s.Data["authkey"]
if !ok {
return "", nil
resp, err := doKubeRequest(ctx, req)
if err != nil {
if resp != nil && resp.StatusCode == http.StatusNotFound {
// Kube secret doesn't exist yet, can't have an authkey.
return "", nil
}
return "", err
}
return string(ak), nil
defer resp.Body.Close()
bs, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
// We use a map[string]any here rather than import corev1.Secret,
// because we only do very limited things to the secret, and
// importing corev1 adds 12MiB to the compiled binary.
var s map[string]any
if err := json.Unmarshal(bs, &s); err != nil {
return "", err
}
if d, ok := s["data"].(map[string]any); ok {
if v, ok := d["authkey"].(string); ok {
bs, err := base64.StdEncoding.DecodeString(v)
if err != nil {
return "", err
}
return string(bs), nil
}
}
return "", nil
}
// storeDeviceInfo writes deviceID into the "device_id" data field of the kube
@@ -35,38 +145,65 @@ func findKeyInKubeSecret(ctx context.Context, secretName string) (string, error)
func storeDeviceInfo(ctx context.Context, secretName string, deviceID tailcfg.StableNodeID, fqdn string) error {
// First check if the secret exists at all. Even if running on
// kubernetes, we do not necessarily store state in a k8s secret.
if _, err := kc.GetSecret(ctx, secretName); err != nil {
if s, ok := err.(*kube.Status); ok {
if s.Code >= 400 && s.Code <= 499 {
// Assume the secret doesn't exist, or we don't have
// permission to access it.
return nil
}
req, err := http.NewRequest("GET", fmt.Sprintf("/api/v1/namespaces/%s/secrets/%s", kubeNamespace, secretName), nil)
if err != nil {
return err
}
resp, err := doKubeRequest(ctx, req)
if err != nil {
if resp != nil && resp.StatusCode >= 400 && resp.StatusCode <= 499 {
// Assume the secret doesn't exist, or we don't have
// permission to access it.
return nil
}
return err
}
m := &kube.Secret{
Data: map[string][]byte{
"device_id": []byte(deviceID),
"device_fqdn": []byte(fqdn),
m := map[string]map[string]string{
"stringData": {
"device_id": string(deviceID),
"device_fqdn": fqdn,
},
}
return kc.StrategicMergePatchSecret(ctx, secretName, m, "tailscale-container")
var b bytes.Buffer
if err := json.NewEncoder(&b).Encode(m); err != nil {
return err
}
req, err = http.NewRequest("PATCH", fmt.Sprintf("/api/v1/namespaces/%s/secrets/%s?fieldManager=tailscale-container", kubeNamespace, secretName), &b)
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/strategic-merge-patch+json")
if _, err := doKubeRequest(ctx, req); err != nil {
return err
}
return nil
}
// deleteAuthKey deletes the 'authkey' field of the given kube
// secret. No-op if there is no authkey in the secret.
func deleteAuthKey(ctx context.Context, secretName string) error {
// m is a JSON Patch data structure, see https://jsonpatch.com/ or RFC 6902.
m := []kube.JSONPatch{
m := []struct {
Op string `json:"op"`
Path string `json:"path"`
}{
{
Op: "remove",
Path: "/data/authkey",
},
}
if err := kc.JSONPatchSecret(ctx, secretName, m); err != nil {
if s, ok := err.(*kube.Status); ok && s.Code == http.StatusUnprocessableEntity {
var b bytes.Buffer
if err := json.NewEncoder(&b).Encode(m); err != nil {
return err
}
req, err := http.NewRequest("PATCH", fmt.Sprintf("/api/v1/namespaces/%s/secrets/%s?fieldManager=tailscale-container", kubeNamespace, secretName), &b)
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json-patch+json")
if resp, err := doKubeRequest(ctx, req); err != nil {
if resp != nil && resp.StatusCode == http.StatusUnprocessableEntity {
// This is kubernetes-ese for "the field you asked to
// delete already doesn't exist", aka no-op.
return nil
@@ -76,22 +213,65 @@ func deleteAuthKey(ctx context.Context, secretName string) error {
return nil
}
var kc *kube.Client
var (
kubeHost string
kubeNamespace string
kubeToken string
kubeHTTP *http.Transport
)
func initKube(root string) {
if root != "/" {
// If we are running in a test, we need to set the root path to the fake
// service account directory.
kube.SetRootPathForTesting(root)
// If running in Kubernetes, set things up so that doKubeRequest
// can talk successfully to the kube apiserver.
if os.Getenv("KUBERNETES_SERVICE_HOST") == "" {
return
}
var err error
kc, err = kube.New()
kubeHost = os.Getenv("KUBERNETES_SERVICE_HOST") + ":" + os.Getenv("KUBERNETES_SERVICE_PORT_HTTPS")
bs, err := os.ReadFile(filepath.Join(root, "var/run/secrets/kubernetes.io/serviceaccount/namespace"))
if err != nil {
log.Fatalf("Error creating kube client: %v", err)
log.Fatalf("Error reading kube namespace: %v", err)
}
if root != "/" {
// If we are running in a test, we need to set the URL to the
// httptest server.
kc.SetURL(fmt.Sprintf("https://%s:%s", os.Getenv("KUBERNETES_SERVICE_HOST"), os.Getenv("KUBERNETES_SERVICE_PORT_HTTPS")))
kubeNamespace = strings.TrimSpace(string(bs))
bs, err = os.ReadFile(filepath.Join(root, "var/run/secrets/kubernetes.io/serviceaccount/token"))
if err != nil {
log.Fatalf("Error reading kube token: %v", err)
}
kubeToken = strings.TrimSpace(string(bs))
bs, err = os.ReadFile(filepath.Join(root, "var/run/secrets/kubernetes.io/serviceaccount/ca.crt"))
if err != nil {
log.Fatalf("Error reading kube CA cert: %v", err)
}
cp := x509.NewCertPool()
cp.AppendCertsFromPEM(bs)
kubeHTTP = &http.Transport{
TLSClientConfig: &tls.Config{
RootCAs: cp,
},
IdleConnTimeout: time.Second,
}
}
// doKubeRequest sends r to the kube apiserver.
func doKubeRequest(ctx context.Context, r *http.Request) (*http.Response, error) {
if kubeHTTP == nil {
panic("not in kubernetes")
}
r.URL.Scheme = "https"
r.URL.Host = kubeHost
r.Header.Set("Authorization", "Bearer "+kubeToken)
r.Header.Set("Accept", "application/json")
resp, err := kubeHTTP.RoundTrip(r)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
return resp, fmt.Errorf("got non-200/201 status code %d", resp.StatusCode)
}
return resp, nil
}

View File

@@ -123,7 +123,7 @@ func main() {
defer cancel()
if cfg.InKubernetes && cfg.KubeSecret != "" {
canPatch, err := kc.CheckSecretPermissions(ctx, cfg.KubeSecret)
canPatch, err := checkSecretPermissions(ctx, cfg.KubeSecret)
if err != nil {
log.Fatalf("Some Kubernetes permissions are missing, please check your RBAC configuration: %v", err)
}

View File

@@ -607,7 +607,7 @@ func TestContainerBoot(t *testing.T) {
}()
var wantCmds []string
for i, p := range test.Phases {
for _, p := range test.Phases {
lapi.Notify(p.Notify)
wantCmds = append(wantCmds, p.WantCmds...)
waitArgs(t, 2*time.Second, d, argFile, strings.Join(wantCmds, "\n"))
@@ -626,7 +626,7 @@ func TestContainerBoot(t *testing.T) {
return nil
})
if err != nil {
t.Fatalf("phase %d: %v", i, err)
t.Fatal(err)
}
err = tstest.WaitFor(2*time.Second, func() error {
for path, want := range p.WantFiles {
@@ -983,13 +983,13 @@ func (k *kubeServer) serveSecret(w http.ResponseWriter, r *http.Request) {
}
case "application/strategic-merge-patch+json":
req := struct {
Data map[string][]byte `json:"data"`
Data map[string]string `json:"stringData"`
}{}
if err := json.Unmarshal(bs, &req); err != nil {
panic(fmt.Sprintf("json decode failed: %v. Body:\n\n%s", err, string(bs)))
}
for key, val := range req.Data {
k.secret[key] = string(val)
k.secret[key] = val
}
default:
panic(fmt.Sprintf("unknown content type %q", r.Header.Get("Content-Type")))

View File

@@ -14,7 +14,6 @@ import (
"time"
"tailscale.com/syncs"
"tailscale.com/util/slicesx"
)
const refreshTimeout = time.Minute
@@ -53,13 +52,6 @@ func refreshBootstrapDNS() {
ctx, cancel := context.WithTimeout(context.Background(), refreshTimeout)
defer cancel()
dnsEntries := resolveList(ctx, strings.Split(*bootstrapDNS, ","))
// Randomize the order of the IPs for each name to avoid the client biasing
// to IPv6
for k := range dnsEntries {
ips := dnsEntries[k]
slicesx.Shuffle(ips)
dnsEntries[k] = ips
}
j, err := json.MarshalIndent(dnsEntries, "", "\t")
if err != nil {
// leave the old values in place

View File

@@ -11,12 +11,14 @@ import (
"net/url"
"reflect"
"testing"
"tailscale.com/tstest"
)
func BenchmarkHandleBootstrapDNS(b *testing.B) {
tstest.Replace(b, bootstrapDNS, "log.tailscale.io,login.tailscale.com,controlplane.tailscale.com,login.us.tailscale.com")
prev := *bootstrapDNS
*bootstrapDNS = "log.tailscale.io,login.tailscale.com,controlplane.tailscale.com,login.us.tailscale.com"
defer func() {
*bootstrapDNS = prev
}()
refreshBootstrapDNS()
w := new(bitbucketResponseWriter)
req, _ := http.NewRequest("GET", "https://localhost/bootstrap-dns?q="+url.QueryEscape("log.tailscale.io"), nil)

View File

@@ -8,63 +8,21 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
W 💣 github.com/alexbrainman/sspi from github.com/alexbrainman/sspi/internal/common+
W github.com/alexbrainman/sspi/internal/common from github.com/alexbrainman/sspi/negotiate
W 💣 github.com/alexbrainman/sspi/negotiate from tailscale.com/net/tshttpproxy
github.com/beorn7/perks/quantile from github.com/prometheus/client_golang/prometheus
💣 github.com/cespare/xxhash/v2 from github.com/prometheus/client_golang/prometheus
github.com/fxamacker/cbor/v2 from tailscale.com/tka
github.com/golang/groupcache/lru from tailscale.com/net/dnscache
github.com/golang/protobuf/proto from github.com/matttproud/golang_protobuf_extensions/pbutil+
github.com/golang/protobuf/ptypes/timestamp from github.com/prometheus/client_model/go
github.com/hdevalence/ed25519consensus from tailscale.com/tka
L github.com/josharian/native from github.com/mdlayher/netlink+
L 💣 github.com/jsimonetti/rtnetlink from tailscale.com/net/interfaces+
L 💣 github.com/jsimonetti/rtnetlink from tailscale.com/net/interfaces
L github.com/jsimonetti/rtnetlink/internal/unix from github.com/jsimonetti/rtnetlink
github.com/klauspost/compress/flate from nhooyr.io/websocket
github.com/matttproud/golang_protobuf_extensions/pbutil from github.com/prometheus/common/expfmt
L 💣 github.com/mdlayher/netlink from github.com/jsimonetti/rtnetlink+
L 💣 github.com/mdlayher/netlink/nlenc from github.com/jsimonetti/rtnetlink+
L 💣 github.com/mdlayher/socket from github.com/mdlayher/netlink
💣 github.com/mitchellh/go-ps from tailscale.com/safesocket
💣 github.com/prometheus/client_golang/prometheus from tailscale.com/tsweb/promvarz
github.com/prometheus/client_golang/prometheus/internal from github.com/prometheus/client_golang/prometheus
github.com/prometheus/client_model/go from github.com/prometheus/client_golang/prometheus+
github.com/prometheus/common/expfmt from github.com/prometheus/client_golang/prometheus+
github.com/prometheus/common/internal/bitbucket.org/ww/goautoneg from github.com/prometheus/common/expfmt
github.com/prometheus/common/model from github.com/prometheus/client_golang/prometheus+
LD github.com/prometheus/procfs from github.com/prometheus/client_golang/prometheus
LD github.com/prometheus/procfs/internal/fs from github.com/prometheus/procfs
LD github.com/prometheus/procfs/internal/util from github.com/prometheus/procfs
github.com/x448/float16 from github.com/fxamacker/cbor/v2
💣 go4.org/mem from tailscale.com/client/tailscale+
go4.org/netipx from tailscale.com/wgengine/filter
W 💣 golang.zx2c4.com/wireguard/windows/tunnel/winipcfg from tailscale.com/net/interfaces+
google.golang.org/protobuf/encoding/prototext from github.com/golang/protobuf/proto+
google.golang.org/protobuf/encoding/protowire from github.com/golang/protobuf/proto+
google.golang.org/protobuf/internal/descfmt from google.golang.org/protobuf/internal/filedesc
google.golang.org/protobuf/internal/descopts from google.golang.org/protobuf/internal/filedesc+
google.golang.org/protobuf/internal/detrand from google.golang.org/protobuf/internal/descfmt+
google.golang.org/protobuf/internal/encoding/defval from google.golang.org/protobuf/internal/encoding/tag+
google.golang.org/protobuf/internal/encoding/messageset from google.golang.org/protobuf/encoding/prototext+
google.golang.org/protobuf/internal/encoding/tag from google.golang.org/protobuf/internal/impl
google.golang.org/protobuf/internal/encoding/text from google.golang.org/protobuf/encoding/prototext+
google.golang.org/protobuf/internal/errors from google.golang.org/protobuf/encoding/prototext+
google.golang.org/protobuf/internal/filedesc from google.golang.org/protobuf/internal/encoding/tag+
google.golang.org/protobuf/internal/filetype from google.golang.org/protobuf/runtime/protoimpl
google.golang.org/protobuf/internal/flags from google.golang.org/protobuf/encoding/prototext+
google.golang.org/protobuf/internal/genid from google.golang.org/protobuf/encoding/prototext+
💣 google.golang.org/protobuf/internal/impl from google.golang.org/protobuf/internal/filetype+
google.golang.org/protobuf/internal/order from google.golang.org/protobuf/encoding/prototext+
google.golang.org/protobuf/internal/pragma from google.golang.org/protobuf/encoding/prototext+
google.golang.org/protobuf/internal/set from google.golang.org/protobuf/encoding/prototext
💣 google.golang.org/protobuf/internal/strs from google.golang.org/protobuf/encoding/prototext+
google.golang.org/protobuf/internal/version from google.golang.org/protobuf/runtime/protoimpl
google.golang.org/protobuf/proto from github.com/golang/protobuf/proto+
google.golang.org/protobuf/reflect/protodesc from github.com/golang/protobuf/proto
💣 google.golang.org/protobuf/reflect/protoreflect from github.com/golang/protobuf/proto+
google.golang.org/protobuf/reflect/protoregistry from github.com/golang/protobuf/proto+
google.golang.org/protobuf/runtime/protoiface from github.com/golang/protobuf/proto+
google.golang.org/protobuf/runtime/protoimpl from github.com/golang/protobuf/proto+
google.golang.org/protobuf/types/descriptorpb from google.golang.org/protobuf/reflect/protodesc
google.golang.org/protobuf/types/known/timestamppb from github.com/golang/protobuf/ptypes/timestamp+
nhooyr.io/websocket from tailscale.com/cmd/derper+
nhooyr.io/websocket/internal/errd from nhooyr.io/websocket
nhooyr.io/websocket/internal/xsync from nhooyr.io/websocket
@@ -86,11 +44,9 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
💣 tailscale.com/net/interfaces from tailscale.com/net/netns+
tailscale.com/net/netaddr from tailscale.com/ipn+
tailscale.com/net/netknob from tailscale.com/net/netns
tailscale.com/net/netmon from tailscale.com/net/sockstats+
tailscale.com/net/netns from tailscale.com/derp/derphttp
tailscale.com/net/netutil from tailscale.com/client/tailscale
tailscale.com/net/packet from tailscale.com/wgengine/filter
tailscale.com/net/sockstats from tailscale.com/derp/derphttp
tailscale.com/net/stun from tailscale.com/cmd/derper
tailscale.com/net/tlsdial from tailscale.com/derp/derphttp
tailscale.com/net/tsaddr from tailscale.com/ipn+
@@ -103,10 +59,8 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
tailscale.com/tka from tailscale.com/client/tailscale+
W tailscale.com/tsconst from tailscale.com/net/interfaces
💣 tailscale.com/tstime/mono from tailscale.com/tstime/rate
tailscale.com/tstime/rate from tailscale.com/wgengine/filter+
tailscale.com/tstime/rate from tailscale.com/wgengine/filter
tailscale.com/tsweb from tailscale.com/cmd/derper
tailscale.com/tsweb/promvarz from tailscale.com/tsweb
tailscale.com/tsweb/varz from tailscale.com/tsweb+
tailscale.com/types/dnstype from tailscale.com/tailcfg
tailscale.com/types/empty from tailscale.com/ipn
tailscale.com/types/ipproto from tailscale.com/net/flowtrack+
@@ -130,9 +84,8 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
tailscale.com/util/lineread from tailscale.com/hostinfo+
tailscale.com/util/mak from tailscale.com/syncs+
tailscale.com/util/multierr from tailscale.com/health
tailscale.com/util/set from tailscale.com/health+
tailscale.com/util/set from tailscale.com/health
tailscale.com/util/singleflight from tailscale.com/net/dnscache
tailscale.com/util/slicesx from tailscale.com/cmd/derper+
tailscale.com/util/vizerror from tailscale.com/tsweb
W 💣 tailscale.com/util/winutil from tailscale.com/hostinfo+
tailscale.com/version from tailscale.com/derp+
@@ -157,7 +110,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
L golang.org/x/net/bpf from github.com/mdlayher/netlink+
golang.org/x/net/dns/dnsmessage from net+
golang.org/x/net/http/httpguts from net/http
golang.org/x/net/http/httpproxy from net/http+
golang.org/x/net/http/httpproxy from net/http
golang.org/x/net/http2/hpack from net/http
golang.org/x/net/idna from golang.org/x/crypto/acme/autocert+
golang.org/x/net/proxy from tailscale.com/net/netns
@@ -214,10 +167,8 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
expvar from tailscale.com/cmd/derper+
flag from tailscale.com/cmd/derper
fmt from compress/flate+
go/token from google.golang.org/protobuf/internal/strs
hash from crypto+
hash/crc32 from compress/gzip+
hash/fnv from google.golang.org/protobuf/internal/detrand
hash/maphash from go4.org/mem
html from net/http/pprof+
io from bufio+
@@ -235,7 +186,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
net/http from expvar+
net/http/httptrace from net/http+
net/http/internal from net/http
net/http/pprof from tailscale.com/tsweb+
net/http/pprof from tailscale.com/tsweb
net/netip from go4.org/netipx+
net/textproto from golang.org/x/net/http/httpguts+
net/url from crypto/x509+
@@ -248,7 +199,6 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
regexp from internal/profile+
regexp/syntax from regexp
runtime/debug from golang.org/x/crypto/acme+
runtime/metrics from github.com/prometheus/client_golang/prometheus+
runtime/pprof from net/http/pprof
runtime/trace from net/http/pprof
sort from compress/flate+

View File

@@ -36,8 +36,8 @@ import (
)
var (
dev = flag.Bool("dev", false, "run in localhost development mode (overrides -a)")
addr = flag.String("a", ":443", "server HTTP/HTTPS listen address, in form \":port\", \"ip:port\", or for IPv6 \"[ip]:port\". If the IP is omitted, it defaults to all interfaces. Serves HTTPS if the port is 443 and/or -certmode is manual, otherwise HTTP.")
dev = flag.Bool("dev", false, "run in localhost development mode")
addr = flag.String("a", ":443", "server HTTPS listen address, in form \":port\", \"ip:port\", or for IPv6 \"[ip]:port\". If the IP is omitted, it defaults to all interfaces.")
httpPort = flag.Int("http-port", 80, "The port on which to serve HTTP. Set to -1 to disable. The listener is bound to the same IP (if any) as specified in the -a flag.")
stunPort = flag.Int("stun-port", 3478, "The UDP port on which to serve STUN. The listener is bound to the same IP (if any) as specified in the -a flag.")
configPath = flag.String("c", "", "config file path")

View File

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

View File

@@ -2,7 +2,7 @@
// SPDX-License-Identifier: BSD-3-Clause
// get-authkey allocates an authkey using an OAuth API client
// https://tailscale.com/s/oauth-clients and prints it
// https://tailscale.com/kb/1215/oauth-clients/ and prints it
// to stdout for scripts to capture and use.
package main

View File

@@ -22,7 +22,6 @@ import (
"github.com/peterbourgon/ff/v3/ffcli"
"github.com/tailscale/hujson"
"golang.org/x/oauth2/clientcredentials"
"tailscale.com/util/httpm"
)
@@ -43,9 +42,9 @@ func modifiedExternallyError() {
}
}
func apply(cache *Cache, client *http.Client, tailnet, apiKey string) func(context.Context, []string) error {
func apply(cache *Cache, tailnet, apiKey string) func(context.Context, []string) error {
return func(ctx context.Context, args []string) error {
controlEtag, err := getACLETag(ctx, client, tailnet, apiKey)
controlEtag, err := getACLETag(ctx, tailnet, apiKey)
if err != nil {
return err
}
@@ -74,7 +73,7 @@ func apply(cache *Cache, client *http.Client, tailnet, apiKey string) func(conte
return nil
}
if err := applyNewACL(ctx, client, tailnet, apiKey, *policyFname, controlEtag); err != nil {
if err := applyNewACL(ctx, tailnet, apiKey, *policyFname, controlEtag); err != nil {
return err
}
@@ -84,9 +83,9 @@ func apply(cache *Cache, client *http.Client, tailnet, apiKey string) func(conte
}
}
func test(cache *Cache, client *http.Client, tailnet, apiKey string) func(context.Context, []string) error {
func test(cache *Cache, tailnet, apiKey string) func(context.Context, []string) error {
return func(ctx context.Context, args []string) error {
controlEtag, err := getACLETag(ctx, client, tailnet, apiKey)
controlEtag, err := getACLETag(ctx, tailnet, apiKey)
if err != nil {
return err
}
@@ -114,16 +113,16 @@ func test(cache *Cache, client *http.Client, tailnet, apiKey string) func(contex
return nil
}
if err := testNewACLs(ctx, client, tailnet, apiKey, *policyFname); err != nil {
if err := testNewACLs(ctx, tailnet, apiKey, *policyFname); err != nil {
return err
}
return nil
}
}
func getChecksums(cache *Cache, client *http.Client, tailnet, apiKey string) func(context.Context, []string) error {
func getChecksums(cache *Cache, tailnet, apiKey string) func(context.Context, []string) error {
return func(ctx context.Context, args []string) error {
controlEtag, err := getACLETag(ctx, client, tailnet, apiKey)
controlEtag, err := getACLETag(ctx, tailnet, apiKey)
if err != nil {
return err
}
@@ -152,24 +151,8 @@ func main() {
log.Fatal("set envvar TS_TAILNET to your tailnet's name")
}
apiKey, ok := os.LookupEnv("TS_API_KEY")
oauthId, oiok := os.LookupEnv("TS_OAUTH_ID")
oauthSecret, osok := os.LookupEnv("TS_OAUTH_SECRET")
if !ok && (!oiok || !osok) {
log.Fatal("set envvar TS_API_KEY to your Tailscale API key or TS_OAUTH_ID and TS_OAUTH_SECRET to your Tailscale OAuth ID and Secret")
}
if ok && (oiok || osok) {
log.Fatal("set either the envvar TS_API_KEY or TS_OAUTH_ID and TS_OAUTH_SECRET")
}
var client *http.Client
if oiok {
oauthConfig := &clientcredentials.Config{
ClientID: oauthId,
ClientSecret: oauthSecret,
TokenURL: fmt.Sprintf("https://%s/api/v2/oauth/token", *apiServer),
}
client = oauthConfig.Client(context.Background())
} else {
client = http.DefaultClient
if !ok {
log.Fatal("set envvar TS_API_KEY to your Tailscale API key")
}
cache, err := LoadCache(*cacheFname)
if err != nil {
@@ -186,7 +169,7 @@ func main() {
ShortUsage: "gitops-pusher [options] apply",
ShortHelp: "Pushes changes to CONTROL",
LongHelp: `Pushes changes to CONTROL`,
Exec: apply(cache, client, tailnet, apiKey),
Exec: apply(cache, tailnet, apiKey),
}
testCmd := &ffcli.Command{
@@ -194,7 +177,7 @@ func main() {
ShortUsage: "gitops-pusher [options] test",
ShortHelp: "Tests ACL changes",
LongHelp: "Tests ACL changes",
Exec: test(cache, client, tailnet, apiKey),
Exec: test(cache, tailnet, apiKey),
}
cksumCmd := &ffcli.Command{
@@ -202,7 +185,7 @@ func main() {
ShortUsage: "Shows checksums of ACL files",
ShortHelp: "Fetch checksum of CONTROL's ACL and the local ACL for comparison",
LongHelp: "Fetch checksum of CONTROL's ACL and the local ACL for comparison",
Exec: getChecksums(cache, client, tailnet, apiKey),
Exec: getChecksums(cache, tailnet, apiKey),
}
root := &ffcli.Command{
@@ -245,7 +228,7 @@ func sumFile(fname string) (string, error) {
return fmt.Sprintf("%x", h.Sum(nil)), nil
}
func applyNewACL(ctx context.Context, client *http.Client, tailnet, apiKey, policyFname, oldEtag string) error {
func applyNewACL(ctx context.Context, tailnet, apiKey, policyFname, oldEtag string) error {
fin, err := os.Open(policyFname)
if err != nil {
return err
@@ -261,7 +244,7 @@ func applyNewACL(ctx context.Context, client *http.Client, tailnet, apiKey, poli
req.Header.Set("Content-Type", "application/hujson")
req.Header.Set("If-Match", `"`+oldEtag+`"`)
resp, err := client.Do(req)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
@@ -282,7 +265,7 @@ func applyNewACL(ctx context.Context, client *http.Client, tailnet, apiKey, poli
return nil
}
func testNewACLs(ctx context.Context, client *http.Client, tailnet, apiKey, policyFname string) error {
func testNewACLs(ctx context.Context, tailnet, apiKey, policyFname string) error {
data, err := os.ReadFile(policyFname)
if err != nil {
return err
@@ -300,7 +283,7 @@ func testNewACLs(ctx context.Context, client *http.Client, tailnet, apiKey, poli
req.SetBasicAuth(apiKey, "")
req.Header.Set("Content-Type", "application/hujson")
resp, err := client.Do(req)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
@@ -363,7 +346,7 @@ type ACLTestErrorDetail struct {
Errors []string `json:"errors"`
}
func getACLETag(ctx context.Context, client *http.Client, tailnet, apiKey string) (string, error) {
func getACLETag(ctx context.Context, tailnet, apiKey string) (string, error) {
req, err := http.NewRequestWithContext(ctx, httpm.GET, fmt.Sprintf("https://%s/api/v2/tailnet/%s/acl", *apiServer, tailnet), nil)
if err != nil {
return "", err
@@ -372,7 +355,7 @@ func getACLETag(ctx context.Context, client *http.Client, tailnet, apiKey string
req.SetBasicAuth(apiKey, "")
req.Header.Set("Accept", "application/hujson")
resp, err := client.Do(req)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return "", err
}

View File

@@ -7,7 +7,7 @@ metadata:
name: tailscale-auth-proxy
rules:
- apiGroups: [""]
resources: ["users", "groups"]
resources: ["users"]
verbs: ["impersonate"]
---
apiVersion: rbac.authorization.k8s.io/v1

View File

@@ -7,10 +7,8 @@ package main
import (
"context"
"crypto/tls"
_ "embed"
"fmt"
"net/http"
"os"
"strings"
"time"
@@ -27,7 +25,7 @@ import (
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/transport"
"k8s.io/client-go/rest"
"sigs.k8s.io/controller-runtime/pkg/builder"
"sigs.k8s.io/controller-runtime/pkg/cache"
"sigs.k8s.io/controller-runtime/pkg/client"
@@ -101,9 +99,9 @@ func main() {
tsClient.HTTPClient = credentials.Client(context.Background())
if shouldRunAuthProxy {
hostinfo.SetApp("k8s-operator-proxy")
hostinfo.SetPackage("k8s-operator-proxy")
} else {
hostinfo.SetApp("k8s-operator")
hostinfo.SetPackage("k8s-operator")
}
s := &tsnet.Server{
@@ -168,7 +166,7 @@ waitOnline:
loginDone = true
case "NeedsMachineAuth":
if !machineAuthShown {
startlog.Infof("Machine approval required, please visit the admin panel to approve")
startlog.Infof("Machine authorization required, please visit the admin panel to authorize")
machineAuthShown = true
}
default:
@@ -237,25 +235,15 @@ waitOnline:
startlog.Infof("Startup complete, operator running")
if shouldRunAuthProxy {
cfg, err := restConfig.TransportConfig()
rc, err := rest.TransportFor(restConfig)
if err != nil {
startlog.Fatalf("could not get rest.TransportConfig(): %v", err)
startlog.Fatalf("could not get rest transport: %v", err)
}
// Kubernetes uses SPDY for exec and port-forward, however SPDY is
// incompatible with HTTP/2; so disable HTTP/2 in the proxy.
tr := http.DefaultTransport.(*http.Transport).Clone()
tr.TLSClientConfig, err = transport.TLSConfigFor(cfg)
authProxyListener, err := s.Listen("tcp", ":443")
if err != nil {
startlog.Fatalf("could not get transport.TLSConfigFor(): %v", err)
startlog.Fatalf("could not listen on :443: %v", err)
}
tr.TLSNextProto = make(map[string]func(authority string, c *tls.Conn) http.RoundTripper)
rt, err := transport.HTTPWrappersForConfig(cfg, tr)
if err != nil {
startlog.Fatalf("could not get rest.TransportConfig(): %v", err)
}
go runAuthProxy(s, rt, zlog.Named("auth-proxy").Infof)
go runAuthProxy(lc, authProxyListener, rc, zlog.Named("auth-proxy").Infof)
}
if err := mgr.Start(signals.SetupSignalHandler()); err != nil {
startlog.Fatalf("could not start manager: %v", err)

View File

@@ -14,6 +14,7 @@ import (
"go.uber.org/zap"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
v1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
@@ -669,11 +670,11 @@ func expectedSTS(stsName, secretName, hostname string) *appsv1.StatefulSet {
},
},
},
Containers: []corev1.Container{
Containers: []v1.Container{
{
Name: "tailscale",
Image: "tailscale/tailscale",
Env: []corev1.EnvVar{
Env: []v1.EnvVar{
{Name: "TS_USERSPACE", Value: "false"},
{Name: "TS_AUTH_ONCE", Value: "true"},
{Name: "TS_DEST_IP", Value: "10.20.30.40"},

View File

@@ -8,6 +8,7 @@ import (
"crypto/tls"
"fmt"
"log"
"net"
"net/http"
"net/http/httputil"
"net/url"
@@ -16,7 +17,6 @@ import (
"tailscale.com/client/tailscale"
"tailscale.com/client/tailscale/apitype"
"tailscale.com/tsnet"
"tailscale.com/types/logger"
)
@@ -41,42 +41,23 @@ func (h *authProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
h.rp.ServeHTTP(w, r)
}
// runAuthProxy runs an HTTP server that authenticates requests using the
// Tailscale LocalAPI and then proxies them to the Kubernetes API.
// It listens on :443 and uses the Tailscale HTTPS certificate.
// s will be started if it is not already running.
// rt is used to proxy requests to the Kubernetes API.
//
// It never returns.
func runAuthProxy(s *tsnet.Server, rt http.RoundTripper, logf logger.Logf) {
ln, err := s.Listen("tcp", ":443")
if err != nil {
log.Fatalf("could not listen on :443: %v", err)
}
func runAuthProxy(lc *tailscale.LocalClient, ls net.Listener, rt http.RoundTripper, logf logger.Logf) {
u, err := url.Parse(fmt.Sprintf("https://%s:%s", os.Getenv("KUBERNETES_SERVICE_HOST"), os.Getenv("KUBERNETES_SERVICE_PORT_HTTPS")))
if err != nil {
log.Fatalf("runAuthProxy: failed to parse URL %v", err)
}
lc, err := s.LocalClient()
if err != nil {
log.Fatalf("could not get local client: %v", err)
}
ap := &authProxy{
logf: logf,
lc: lc,
rp: &httputil.ReverseProxy{
Director: func(r *http.Request) {
// We want to proxy to the Kubernetes API, but we want to use
// the caller's identity to do so. We do this by impersonating
// the caller using the Kubernetes User Impersonation feature:
// https://kubernetes.io/docs/reference/access-authn-authz/authentication/#user-impersonation
// Replace the request with the user's identity.
who := r.Context().Value(whoIsKey{}).(*apitype.WhoIsResponse)
r.Header.Set("Impersonate-User", who.UserProfile.LoginName)
// Out of paranoia, remove all authentication headers that might
// have been set by the client.
// Remove all authentication headers.
r.Header.Del("Authorization")
r.Header.Del("Impersonate-Group")
r.Header.Del("Impersonate-User")
r.Header.Del("Impersonate-Uid")
for k := range r.Header {
if strings.HasPrefix(k, "Impersonate-Extra-") {
@@ -84,19 +65,6 @@ func runAuthProxy(s *tsnet.Server, rt http.RoundTripper, logf logger.Logf) {
}
}
// Now add the impersonation headers that we want.
who := r.Context().Value(whoIsKey{}).(*apitype.WhoIsResponse)
if who.Node.IsTagged() {
// Use the nodes FQDN as the username, and the nodes tags as the groups.
// "Impersonate-Group" requires "Impersonate-User" to be set.
r.Header.Set("Impersonate-User", strings.TrimSuffix(who.Node.Name, "."))
for _, tag := range who.Node.Tags {
r.Header.Add("Impersonate-Group", tag)
}
} else {
r.Header.Set("Impersonate-User", who.UserProfile.LoginName)
}
// Replace the URL with the Kubernetes APIServer.
r.URL.Scheme = u.Scheme
r.URL.Host = u.Host
@@ -104,17 +72,9 @@ func runAuthProxy(s *tsnet.Server, rt http.RoundTripper, logf logger.Logf) {
Transport: rt,
},
}
hs := &http.Server{
// Kubernetes uses SPDY for exec and port-forward, however SPDY is
// incompatible with HTTP/2; so disable HTTP/2 in the proxy.
TLSConfig: &tls.Config{
GetCertificate: lc.GetCertificate,
NextProtos: []string{"http/1.1"},
},
TLSNextProto: make(map[string]func(*http.Server, *tls.Conn, http.Handler)),
Handler: ap,
}
if err := hs.ServeTLS(ln, "", ""); err != nil {
if err := http.Serve(tls.NewListener(ls, &tls.Config{
GetCertificate: lc.GetCertificate,
}), ap); err != nil {
log.Fatalf("runAuthProxy: failed to serve %v", err)
}
}

View File

@@ -19,7 +19,7 @@ func main() {
arch := winres.Arch(os.Args[1])
switch arch {
case winres.ArchAMD64, winres.ArchARM64, winres.ArchI386:
case winres.ArchAMD64, winres.ArchARM64, winres.ArchI386, winres.ArchARM:
default:
log.Fatalf("unsupported arch: %s", arch)
}

View File

@@ -43,7 +43,7 @@ import (
jsonv2 "github.com/go-json-experiment/json"
"golang.org/x/exp/maps"
"golang.org/x/exp/slices"
"tailscale.com/types/logid"
"tailscale.com/logtail"
"tailscale.com/types/netlogtype"
"tailscale.com/util/must"
)
@@ -136,8 +136,8 @@ func processObject(dec *jsonv2.Decoder) {
type message struct {
Logtail struct {
ID logid.PublicID `json:"id"`
Logged time.Time `json:"server_time"`
ID logtail.PublicID `json:"id"`
Logged time.Time `json:"server_time"`
} `json:"logtail"`
Logged time.Time `json:"logged"`
netlogtype.Message

View File

@@ -56,7 +56,7 @@ func main() {
return
}
if info.Node.IsTagged() {
if len(info.Node.Tags) != 0 {
w.WriteHeader(http.StatusForbidden)
log.Printf("node %s is tagged", info.Node.Hostinfo.Hostname())
return

View File

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

View File

@@ -147,7 +147,7 @@ func getTailscaleUser(ctx context.Context, localClient *tailscale.LocalClient, i
if err != nil {
return nil, fmt.Errorf("failed to identify remote host: %w", err)
}
if whois.Node.IsTagged() {
if len(whois.Node.Tags) != 0 {
return nil, fmt.Errorf("tagged nodes are not users")
}
if whois.UserProfile == nil || whois.UserProfile.LoginName == "" {

View File

@@ -1,219 +0,0 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// The sniproxy is an outbound SNI proxy. It receives TLS connections over
// Tailscale on one or more TCP ports and sends them out to the same SNI
// hostname & port on the internet. It only does TCP.
package main
import (
"context"
"flag"
"log"
"net"
"net/http"
"strings"
"time"
"golang.org/x/net/dns/dnsmessage"
"inet.af/tcpproxy"
"tailscale.com/client/tailscale"
"tailscale.com/net/netutil"
"tailscale.com/tsnet"
"tailscale.com/types/nettype"
)
var (
ports = flag.String("ports", "443", "comma-separated list of ports to proxy")
promoteHTTPS = flag.Bool("promote-https", true, "promote HTTP to HTTPS")
)
var tsMBox = dnsmessage.MustNewName("support.tailscale.com.")
func main() {
flag.Parse()
if *ports == "" {
log.Fatal("no ports")
}
var s server
defer s.ts.Close()
lc, err := s.ts.LocalClient()
if err != nil {
log.Fatal(err)
}
s.lc = lc
for _, portStr := range strings.Split(*ports, ",") {
ln, err := s.ts.Listen("tcp", ":"+portStr)
if err != nil {
log.Fatal(err)
}
log.Printf("Serving on port %v ...", portStr)
go s.serve(ln)
}
ln, err := s.ts.Listen("udp", ":53")
if err != nil {
log.Fatal(err)
}
go s.serveDNS(ln)
if *promoteHTTPS {
ln, err := s.ts.Listen("tcp", ":80")
if err != nil {
log.Fatal(err)
}
log.Printf("Promoting HTTP to HTTPS ...")
go s.promoteHTTPS(ln)
}
select {}
}
type server struct {
ts tsnet.Server
lc *tailscale.LocalClient
}
func (s *server) serve(ln net.Listener) {
for {
c, err := ln.Accept()
if err != nil {
log.Fatal(err)
}
go s.serveConn(c)
}
}
func (s *server) serveDNS(ln net.Listener) {
for {
c, err := ln.Accept()
if err != nil {
log.Fatal(err)
}
go s.serveDNSConn(c.(nettype.ConnPacketConn))
}
}
func (s *server) serveDNSConn(c nettype.ConnPacketConn) {
defer c.Close()
c.SetReadDeadline(time.Now().Add(5 * time.Second))
buf := make([]byte, 1500)
n, err := c.Read(buf)
if err != nil {
log.Printf("c.Read failed: %v\n ", err)
return
}
var msg dnsmessage.Message
err = msg.Unpack(buf[:n])
if err != nil {
log.Printf("dnsmessage unpack failed: %v\n ", err)
return
}
buf, err = s.dnsResponse(&msg)
if err != nil {
log.Printf("s.dnsResponse failed: %v\n", err)
return
}
_, err = c.Write(buf)
if err != nil {
log.Printf("c.Write failed: %v\n", err)
return
}
}
func (s *server) serveConn(c net.Conn) {
addrPortStr := c.LocalAddr().String()
_, port, err := net.SplitHostPort(addrPortStr)
if err != nil {
log.Printf("bogus addrPort %q", addrPortStr)
c.Close()
return
}
var dialer net.Dialer
dialer.Timeout = 5 * time.Second
var p tcpproxy.Proxy
p.ListenFunc = func(net, laddr string) (net.Listener, error) {
return netutil.NewOneConnListener(c, nil), nil
}
p.AddSNIRouteFunc(addrPortStr, func(ctx context.Context, sniName string) (t tcpproxy.Target, ok bool) {
return &tcpproxy.DialProxy{
Addr: net.JoinHostPort(sniName, port),
DialContext: dialer.DialContext,
}, true
})
p.Start()
}
func (s *server) dnsResponse(req *dnsmessage.Message) (buf []byte, err error) {
resp := dnsmessage.NewBuilder(buf,
dnsmessage.Header{
ID: req.Header.ID,
Response: true,
Authoritative: true,
})
resp.EnableCompression()
if len(req.Questions) == 0 {
buf, _ = resp.Finish()
return
}
q := req.Questions[0]
err = resp.StartQuestions()
if err != nil {
return
}
resp.Question(q)
ip4, ip6 := s.ts.TailscaleIPs()
err = resp.StartAnswers()
if err != nil {
return
}
switch q.Type {
case dnsmessage.TypeAAAA:
err = resp.AAAAResource(
dnsmessage.ResourceHeader{Name: q.Name, Class: q.Class, TTL: 120},
dnsmessage.AAAAResource{AAAA: ip6.As16()},
)
case dnsmessage.TypeA:
err = resp.AResource(
dnsmessage.ResourceHeader{Name: q.Name, Class: q.Class, TTL: 120},
dnsmessage.AResource{A: ip4.As4()},
)
case dnsmessage.TypeSOA:
err = resp.SOAResource(
dnsmessage.ResourceHeader{Name: q.Name, Class: q.Class, TTL: 120},
dnsmessage.SOAResource{NS: q.Name, MBox: tsMBox, Serial: 2023030600,
Refresh: 120, Retry: 120, Expire: 120, MinTTL: 60},
)
case dnsmessage.TypeNS:
err = resp.NSResource(
dnsmessage.ResourceHeader{Name: q.Name, Class: q.Class, TTL: 120},
dnsmessage.NSResource{NS: tsMBox},
)
}
if err != nil {
return
}
return resp.Finish()
}
func (s *server) promoteHTTPS(ln net.Listener) {
err := http.Serve(ln, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "https://"+r.Host+r.RequestURI, http.StatusFound)
}))
log.Fatalf("promoteHTTPS http.Serve: %v", err)
}

View File

@@ -113,15 +113,12 @@ change in the future.
loginCmd,
logoutCmd,
switchCmd,
configureCmd,
netcheckCmd,
ipCmd,
statusCmd,
pingCmd,
ncCmd,
sshCmd,
funnelCmd,
serveCmd,
versionCmd,
webCmd,
fileCmd,
@@ -149,8 +146,12 @@ change in the future.
switch {
case slices.Contains(args, "debug"):
rootCmd.Subcommands = append(rootCmd.Subcommands, debugCmd)
case slices.Contains(args, "serve"):
rootCmd.Subcommands = append(rootCmd.Subcommands, serveCmd)
case slices.Contains(args, "update"):
rootCmd.Subcommands = append(rootCmd.Subcommands, updateCmd)
case slices.Contains(args, "configure"):
rootCmd.Subcommands = append(rootCmd.Subcommands, configureCmd)
}
if runtime.GOOS == "linux" && distro.Get() == distro.Synology {
rootCmd.Subcommands = append(rootCmd.Subcommands, configureHostCmd)

View File

@@ -621,16 +621,9 @@ func TestPrefsFromUpArgs(t *testing.T) {
{
name: "error_long_hostname",
args: upArgsT{
hostname: strings.Repeat(strings.Repeat("a", 63)+".", 4),
hostname: strings.Repeat("a", 300),
},
wantErr: `"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" is too long to be a DNS name`,
},
{
name: "error_long_label",
args: upArgsT{
hostname: strings.Repeat("a", 64) + ".example.com",
},
wantErr: `"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" is not a valid DNS label`,
wantErr: `hostname too long: 300 bytes (max 256)`,
},
{
name: "error_linux_netfilter_empty",
@@ -1078,42 +1071,20 @@ func TestUpdatePrefs(t *testing.T) {
},
env: upCheckEnv{backendState: "Running"},
},
{
name: "force_reauth_over_ssh_no_risk",
flags: []string{"--force-reauth"},
sshOverTailscale: true,
curPrefs: &ipn.Prefs{
ControlURL: "https://login.tailscale.com",
AllowSingleHosts: true,
CorpDNS: true,
NetfilterMode: preftype.NetfilterOn,
},
env: upCheckEnv{backendState: "Running"},
wantErrSubtr: "aborted, no changes made",
},
{
name: "force_reauth_over_ssh",
flags: []string{"--force-reauth", "--accept-risk=lose-ssh"},
sshOverTailscale: true,
curPrefs: &ipn.Prefs{
ControlURL: "https://login.tailscale.com",
AllowSingleHosts: true,
CorpDNS: true,
NetfilterMode: preftype.NetfilterOn,
},
wantJustEditMP: nil,
env: upCheckEnv{backendState: "Running"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.sshOverTailscale {
tstest.Replace(t, &getSSHClientEnvVar, func() string { return "100.100.100.100 1 1" })
old := getSSHClientEnvVar
getSSHClientEnvVar = func() string { return "100.100.100.100 1 1" }
t.Cleanup(func() { getSSHClientEnvVar = old })
} else if isSSHOverTailscale() {
// The test is being executed over a "real" tailscale SSH
// session, but sshOverTailscale is unset. Make the test appear
// as if it's not over tailscale SSH.
tstest.Replace(t, &getSSHClientEnvVar, func() string { return "" })
old := getSSHClientEnvVar
getSSHClientEnvVar = func() string { return "" }
t.Cleanup(func() { getSSHClientEnvVar = old })
}
if tt.env.goos == "" {
tt.env.goos = "linux"

View File

@@ -26,14 +26,12 @@ func init() {
var configureKubeconfigCmd = &ffcli.Command{
Name: "kubeconfig",
ShortHelp: "[ALPHA] Connect to a Kubernetes cluster using a Tailscale Auth Proxy",
ShortHelp: "Configure kubeconfig to use Tailscale",
ShortUsage: "kubeconfig <hostname-or-fqdn>",
LongHelp: strings.TrimSpace(`
Run this command to configure kubectl to connect to a Kubernetes cluster over Tailscale.
Run this command to configure your kubeconfig to use Tailscale for authentication to a Kubernetes cluster.
The hostname argument should be set to the Tailscale hostname of the peer running as an auth proxy in the cluster.
See: https://tailscale.com/s/k8s-auth-proxy
`),
FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("kubeconfig")

View File

@@ -35,13 +35,13 @@ var configureHostCmd = &ffcli.Command{
var synologyConfigureCmd = &ffcli.Command{
Name: "synology",
Exec: runConfigureSynology,
ShortHelp: "Configure Synology to enable outbound connections",
ShortHelp: "Configure Synology to enable more Tailscale features",
LongHelp: strings.TrimSpace(`
This command is intended to run at boot as root on a Synology device to
create the /dev/net/tun device and give the tailscaled binary permission
to use it.
The 'configure-host' command is intended to run at boot as root
to create the /dev/net/tun device and give the tailscaled binary
permission to use it.
See: https://tailscale.com/s/synology-outbound
See: https://tailscale.com/kb/1152/synology-outbound/
`),
FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("synology")

View File

@@ -15,10 +15,10 @@ import (
var configureCmd = &ffcli.Command{
Name: "configure",
ShortHelp: "[ALPHA] Configure the host to enable more Tailscale features",
ShortHelp: "Configure the host to enable more Tailscale features",
LongHelp: strings.TrimSpace(`
The 'configure' set of commands are intended to provide a way to enable different
services on the host to use Tailscale in more ways.
The 'configure' command is intended to provide a way to configure different
services on the host to enable more Tailscale features.
`),
FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("configure")

View File

@@ -201,23 +201,6 @@ var debugCmd = &ffcli.Command{
return fs
})(),
},
{
Name: "portmap",
Exec: debugPortmap,
ShortHelp: "run portmap debugging debugging",
FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("portmap")
fs.DurationVar(&debugPortmapArgs.duration, "duration", 5*time.Second, "timeout for port mapping")
fs.StringVar(&debugPortmapArgs.ty, "type", "", `portmap debug type (one of "", "pmp", "pcp", or "upnp")`)
fs.StringVar(&debugPortmapArgs.gwSelf, "gw-self", "", `override gateway and self IP (format: "gatewayIP/selfIP")`)
return fs
})(),
},
{
Name: "peer-endpoint-changes",
Exec: runPeerEndpointChanges,
ShortHelp: "prints debug information about a peer's endpoint changes",
},
},
}
@@ -806,82 +789,3 @@ func runCapture(ctx context.Context, args []string) error {
_, err = io.Copy(f, stream)
return err
}
var debugPortmapArgs struct {
duration time.Duration
gwSelf string
ty string
}
func debugPortmap(ctx context.Context, args []string) error {
rc, err := localClient.DebugPortmap(ctx,
debugPortmapArgs.duration,
debugPortmapArgs.ty,
debugPortmapArgs.gwSelf,
)
if err != nil {
return err
}
defer rc.Close()
_, err = io.Copy(os.Stdout, rc)
return err
}
func runPeerEndpointChanges(ctx context.Context, args []string) error {
st, err := localClient.Status(ctx)
if err != nil {
return fixTailscaledConnectError(err)
}
description, ok := isRunningOrStarting(st)
if !ok {
printf("%s\n", description)
os.Exit(1)
}
if len(args) != 1 || args[0] == "" {
return errors.New("usage: peer-status <hostname-or-IP>")
}
var ip string
hostOrIP := args[0]
ip, self, err := tailscaleIPFromArg(ctx, hostOrIP)
if err != nil {
return err
}
if self {
printf("%v is local Tailscale IP\n", ip)
return nil
}
if ip != hostOrIP {
log.Printf("lookup %q => %q", hostOrIP, ip)
}
req, err := http.NewRequestWithContext(ctx, "GET", "http://local-tailscaled.sock/localapi/v0/debug-peer-endpoint-changes?ip="+ip, nil)
if err != nil {
return err
}
resp, err := localClient.DoLocalRequest(req)
if err != nil {
return err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
var dst bytes.Buffer
if err := json.Indent(&dst, body, "", " "); err != nil {
return fmt.Errorf("indenting returned JSON: %w", err)
}
if ss := dst.String(); !strings.HasSuffix(ss, "\n") {
dst.WriteByte('\n')
}
fmt.Printf("%s", dst.String())
return nil
}

View File

@@ -1,138 +0,0 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package cli
import (
"context"
"flag"
"fmt"
"net"
"os"
"strconv"
"strings"
"github.com/peterbourgon/ff/v3/ffcli"
"tailscale.com/ipn"
"tailscale.com/util/mak"
)
var funnelCmd = newFunnelCommand(&serveEnv{lc: &localClient})
// newFunnelCommand returns a new "funnel" subcommand using e as its environment.
// The funnel subcommand is used to turn on/off the Funnel service.
// Funnel is off by default.
// Funnel allows you to publish a 'tailscale serve' server publicly, open to the
// entire internet.
// newFunnelCommand shares the same serveEnv as the "serve" subcommand. See
// newServeCommand and serve.go for more details.
func newFunnelCommand(e *serveEnv) *ffcli.Command {
return &ffcli.Command{
Name: "funnel",
ShortHelp: "Turn on/off Funnel service",
ShortUsage: strings.TrimSpace(`
funnel <serve-port> {on|off}
funnel status [--json]
`),
LongHelp: strings.Join([]string{
"Funnel allows you to publish a 'tailscale serve'",
"server publicly, open to the entire internet.",
"",
"Turning off Funnel only turns off serving to the internet.",
"It does not affect serving to your tailnet.",
}, "\n"),
Exec: e.runFunnel,
UsageFunc: usageFunc,
Subcommands: []*ffcli.Command{
{
Name: "status",
Exec: e.runServeStatus,
ShortHelp: "show current serve/funnel status",
FlagSet: e.newFlags("funnel-status", func(fs *flag.FlagSet) {
fs.BoolVar(&e.json, "json", false, "output JSON")
}),
UsageFunc: usageFunc,
},
},
}
}
// runFunnel is the entry point for the "tailscale funnel" subcommand and
// manages turning on/off funnel. Funnel is off by default.
//
// Note: funnel is only supported on single DNS name for now. (2022-11-15)
func (e *serveEnv) runFunnel(ctx context.Context, args []string) error {
if len(args) != 2 {
return flag.ErrHelp
}
var on bool
switch args[1] {
case "on", "off":
on = args[1] == "on"
default:
return flag.ErrHelp
}
sc, err := e.lc.GetServeConfig(ctx)
if err != nil {
return err
}
if sc == nil {
sc = new(ipn.ServeConfig)
}
st, err := e.getLocalClientStatus(ctx)
if err != nil {
return fmt.Errorf("getting client status: %w", err)
}
port64, err := strconv.ParseUint(args[0], 10, 16)
if err != nil {
return err
}
port := uint16(port64)
if err := ipn.CheckFunnelAccess(port, st.Self.Capabilities); err != nil {
return err
}
dnsName := strings.TrimSuffix(st.Self.DNSName, ".")
hp := ipn.HostPort(dnsName + ":" + strconv.Itoa(int(port)))
if on == sc.AllowFunnel[hp] {
printFunnelWarning(sc)
// Nothing to do.
return nil
}
if on {
mak.Set(&sc.AllowFunnel, hp, true)
} else {
delete(sc.AllowFunnel, hp)
// clear map mostly for testing
if len(sc.AllowFunnel) == 0 {
sc.AllowFunnel = nil
}
}
if err := e.lc.SetServeConfig(ctx, sc); err != nil {
return err
}
printFunnelWarning(sc)
return nil
}
// printFunnelWarning prints a warning if the Funnel is on but there is no serve
// config for its host:port.
func printFunnelWarning(sc *ipn.ServeConfig) {
var warn bool
for hp, a := range sc.AllowFunnel {
if !a {
continue
}
_, portStr, _ := net.SplitHostPort(string(hp))
p, _ := strconv.ParseUint(portStr, 10, 16)
if _, ok := sc.TCP[uint16(p)]; !ok {
warn = true
fmt.Fprintf(os.Stderr, "Warning: funnel=on for %s, but no serve config\n", hp)
}
}
if warn {
fmt.Fprintf(os.Stderr, " run: `tailscale serve --help` to see how to configure handlers\n")
}
}

View File

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

View File

@@ -15,7 +15,6 @@ import (
"os"
"strconv"
"strings"
"time"
"github.com/mattn/go-colorable"
"github.com/mattn/go-isatty"
@@ -41,16 +40,7 @@ var netlockCmd = &ffcli.Command{
nlLogCmd,
nlLocalDisableCmd,
},
Exec: runNetworkLockNoSubcommand,
}
func runNetworkLockNoSubcommand(ctx context.Context, args []string) error {
// Detect & handle the deprecated command 'lock tskey-wrap'.
if len(args) >= 2 && args[0] == "tskey-wrap" {
return runTskeyWrapCmd(ctx, args[1:])
}
return runNetworkLockStatus(ctx, args)
Exec: runNetworkLockStatus,
}
var nlInitArgs struct {
@@ -240,15 +230,6 @@ func runNetworkLockStatus(ctx context.Context, args []string) error {
if k.Key == st.PublicKey {
line.WriteString("(self)")
}
if k.Metadata["purpose"] == "pre-auth key" {
if preauthKeyID := k.Metadata["authkey_stableid"]; preauthKeyID != "" {
line.WriteString("(pre-auth key ")
line.WriteString(preauthKeyID)
line.WriteString(")")
} else {
line.WriteString("(pre-auth key)")
}
}
fmt.Println(line.String())
}
}
@@ -264,13 +245,11 @@ func runNetworkLockStatus(ctx context.Context, args []string) error {
for i, addr := range p.TailscaleIPs {
line.WriteString(addr.String())
if i < len(p.TailscaleIPs)-1 {
line.WriteString(",")
line.WriteString(", ")
}
}
line.WriteString("\t")
line.WriteString(string(p.StableID))
line.WriteString("\t")
line.WriteString(p.NodeKey.String())
fmt.Println(line.String())
}
}
@@ -288,78 +267,14 @@ var nlAddCmd = &ffcli.Command{
},
}
var nlRemoveArgs struct {
resign bool
}
var nlRemoveCmd = &ffcli.Command{
Name: "remove",
ShortUsage: "remove [--re-sign=false] <public-key>...",
ShortUsage: "remove <public-key>...",
ShortHelp: "Removes one or more trusted signing keys from tailnet lock",
LongHelp: "Removes one or more trusted signing keys from tailnet lock",
Exec: runNetworkLockRemove,
FlagSet: (func() *flag.FlagSet {
fs := newFlagSet("lock remove")
fs.BoolVar(&nlRemoveArgs.resign, "re-sign", true, "resign signatures which would be invalidated by removal of trusted signing keys")
return fs
})(),
}
func runNetworkLockRemove(ctx context.Context, args []string) error {
removeKeys, _, err := parseNLArgs(args, true, false)
if err != nil {
return err
}
st, err := localClient.NetworkLockStatus(ctx)
if err != nil {
return fixTailscaledConnectError(err)
}
if !st.Enabled {
return errors.New("tailnet lock is not enabled")
}
if nlRemoveArgs.resign {
// Validate we are not removing trust in ourselves while resigning. This is because
// we resign with our own key, so the signatures would be immediately invalid.
for _, k := range removeKeys {
kID, err := k.ID()
if err != nil {
return fmt.Errorf("computing KeyID for key %v: %w", k, err)
}
if bytes.Equal(st.PublicKey.KeyID(), kID) {
return errors.New("cannot remove local trusted signing key while resigning; run command on a different node or with --re-sign=false")
}
}
// Resign affected signatures for each of the keys we are removing.
for _, k := range removeKeys {
kID, _ := k.ID() // err already checked above
sigs, err := localClient.NetworkLockAffectedSigs(ctx, kID)
if err != nil {
return fmt.Errorf("affected sigs for key %X: %w", kID, err)
}
for _, sigBytes := range sigs {
var sig tka.NodeKeySignature
if err := sig.Unserialize(sigBytes); err != nil {
return fmt.Errorf("failed decoding signature: %w", err)
}
var nodeKey key.NodePublic
if err := nodeKey.UnmarshalBinary(sig.Pubkey); err != nil {
return fmt.Errorf("failed decoding pubkey for signature: %w", err)
}
// Safety: NetworkLockAffectedSigs() verifies all signatures before
// successfully returning.
rotationKey, _ := sig.UnverifiedWrappingPublic()
if err := localClient.NetworkLockSign(ctx, nodeKey, []byte(rotationKey)); err != nil {
return fmt.Errorf("failed to sign %v: %w", nodeKey, err)
}
}
}
}
return localClient.NetworkLockModify(ctx, nil, removeKeys)
Exec: func(ctx context.Context, args []string) error {
return runNetworkLockModify(ctx, nil, args)
},
}
// parseNLArgs parses a slice of strings into slices of tka.Key & disablement
@@ -435,19 +350,13 @@ func runNetworkLockModify(ctx context.Context, addArgs, removeArgs []string) err
var nlSignCmd = &ffcli.Command{
Name: "sign",
ShortUsage: "sign <node-key> [<rotation-key>] or sign <auth-key>",
ShortHelp: "Signs a node or pre-approved auth key",
LongHelp: `Either:
- signs a node key and transmits the signature to the coordination server, or
- signs a pre-approved auth key, printing it in a form that can be used to bring up nodes under tailnet lock`,
Exec: runNetworkLockSign,
ShortUsage: "sign <node-key> [<rotation-key>]",
ShortHelp: "Signs a node key and transmits the signature to the coordination server",
LongHelp: "Signs a node key and transmits the signature to the coordination server",
Exec: runNetworkLockSign,
}
func runNetworkLockSign(ctx context.Context, args []string) error {
if len(args) > 0 && strings.HasPrefix(args[0], "tskey-auth-") {
return runTskeyWrapCmd(ctx, args)
}
var (
nodeKey key.NodePublic
rotationKey key.NLPublic
@@ -649,56 +558,3 @@ func runNetworkLockLog(ctx context.Context, args []string) error {
}
return nil
}
func runTskeyWrapCmd(ctx context.Context, args []string) error {
if len(args) != 1 {
return errors.New("usage: lock tskey-wrap <tailscale pre-auth key>")
}
if strings.Contains(args[0], "--TL") {
return errors.New("Error: provided key was already wrapped")
}
st, err := localClient.StatusWithoutPeers(ctx)
if err != nil {
return fixTailscaledConnectError(err)
}
return wrapAuthKey(ctx, args[0], st)
}
func wrapAuthKey(ctx context.Context, keyStr string, status *ipnstate.Status) error {
// Generate a separate tailnet-lock key just for the credential signature.
// We use the free-form meta strings to mark a little bit of metadata about this
// key.
priv := key.NewNLPrivate()
m := map[string]string{
"purpose": "pre-auth key",
"wrapper_stableid": string(status.Self.ID),
"wrapper_createtime": fmt.Sprint(time.Now().Unix()),
}
if strings.HasPrefix(keyStr, "tskey-auth-") && strings.Index(keyStr[len("tskey-auth-"):], "-") > 0 {
// We don't want to accidentally embed the nonce part of the authkey in
// the event the format changes. As such, we make sure its in the format we
// expect (tskey-auth-<stableID, inc CNTRL suffix>-nonce) before we parse
// out and embed the stableID.
s := strings.TrimPrefix(keyStr, "tskey-auth-")
m["authkey_stableid"] = s[:strings.Index(s, "-")]
}
k := tka.Key{
Kind: tka.Key25519,
Public: priv.Public().Verifier(),
Votes: 1,
Meta: m,
}
wrapped, err := localClient.NetworkLockWrapPreauthKey(ctx, keyStr, priv)
if err != nil {
return fmt.Errorf("wrapping failed: %w", err)
}
if err := localClient.NetworkLockModify(ctx, []tka.Key{k}, nil); err != nil {
return fmt.Errorf("add key failed: %w", err)
}
fmt.Println(wrapped)
return nil
}

View File

@@ -21,8 +21,10 @@ import (
"strings"
"github.com/peterbourgon/ff/v3/ffcli"
"golang.org/x/exp/slices"
"tailscale.com/ipn"
"tailscale.com/ipn/ipnstate"
"tailscale.com/tailcfg"
"tailscale.com/util/mak"
"tailscale.com/version"
)
@@ -33,59 +35,80 @@ var serveCmd = newServeCommand(&serveEnv{lc: &localClient})
func newServeCommand(e *serveEnv) *ffcli.Command {
return &ffcli.Command{
Name: "serve",
ShortHelp: "Serve content and local servers",
ShortHelp: "[ALPHA] Serve from your Tailscale node",
ShortUsage: strings.TrimSpace(`
serve https:<port> <mount-point> <source> [off]
serve tcp:<port> tcp://localhost:<local-port> [off]
serve tls-terminated-tcp:<port> tcp://localhost:<local-port> [off]
serve status [--json]
`),
serve [flags] <mount-point> {proxy|path|text} <arg>
serve [flags] <sub-command> [sub-flags] <args>`),
LongHelp: strings.TrimSpace(`
*** BETA; all of this is subject to change ***
*** ALPHA; all of this is subject to change ***
The 'tailscale serve' set of commands allows you to serve
content and local servers from your Tailscale node to
your tailnet.
your tailnet.
You can also choose to enable the Tailscale Funnel with:
'tailscale funnel on'. Funnel allows you to publish
'tailscale serve funnel on'. Funnel allows you to publish
a 'tailscale serve' server publicly, open to the entire
internet. See https://tailscale.com/funnel.
EXAMPLES
- To proxy requests to a web server at 127.0.0.1:3000:
$ tailscale serve https:443 / http://127.0.0.1:3000
Or, using the default port:
$ tailscale serve https / http://127.0.0.1:3000
$ tailscale serve / proxy 3000
- To serve a single file or a directory of files:
$ tailscale serve https / /home/alice/blog/index.html
$ tailscale serve https /images/ /home/alice/blog/images
$ tailscale serve / path /home/alice/blog/index.html
$ tailscale serve /images/ path /home/alice/blog/images
- To serve simple static text:
$ tailscale serve https:8080 / text:"Hello, world!"
- To forward incoming TCP connections on port 2222 to a local TCP server on
port 22 (e.g. to run OpenSSH in parallel with Tailscale SSH):
$ tailscale serve tcp:2222 tcp://localhost:22
- To accept TCP TLS connections (terminated within tailscaled) proxied to a
local plaintext server on port 80:
$ tailscale serve tls-terminated-tcp:443 tcp://localhost:80
$ tailscale serve / text "Hello, world!"
`),
Exec: e.runServe,
Exec: e.runServe,
FlagSet: e.newFlags("serve", func(fs *flag.FlagSet) {
fs.BoolVar(&e.remove, "remove", false, "remove an existing serve config")
fs.UintVar(&e.servePort, "serve-port", 443, "port to serve on (443, 8443 or 10000)")
}),
UsageFunc: usageFunc,
Subcommands: []*ffcli.Command{
{
Name: "status",
Exec: e.runServeStatus,
ShortHelp: "show current serve/funnel status",
ShortHelp: "show current serve status",
FlagSet: e.newFlags("serve-status", func(fs *flag.FlagSet) {
fs.BoolVar(&e.json, "json", false, "output JSON")
}),
UsageFunc: usageFunc,
},
{
Name: "tcp",
Exec: e.runServeTCP,
ShortHelp: "add or remove a TCP port forward",
LongHelp: strings.Join([]string{
"EXAMPLES",
" - Forward TLS over TCP to a local TCP server on port 5432:",
" $ tailscale serve tcp 5432",
"",
" - Forward raw, TLS-terminated TCP packets to a local TCP server on port 5432:",
" $ tailscale serve tcp --terminate-tls 5432",
}, "\n"),
FlagSet: e.newFlags("serve-tcp", func(fs *flag.FlagSet) {
fs.BoolVar(&e.terminateTLS, "terminate-tls", false, "terminate TLS before forwarding TCP connection")
}),
UsageFunc: usageFunc,
},
{
Name: "funnel",
Exec: e.runServeFunnel,
ShortUsage: "funnel [flags] {on|off}",
ShortHelp: "turn Tailscale Funnel on or off",
LongHelp: strings.Join([]string{
"Funnel allows you to publish a 'tailscale serve'",
"server publicly, open to the entire internet.",
"",
"Turning off Funnel only turns off serving to the internet.",
"It does not affect serving to your tailnet.",
}, "\n"),
UsageFunc: usageFunc,
},
},
}
}
@@ -122,7 +145,10 @@ type localServeClient interface {
// It also contains the flags, as registered with newServeCommand.
type serveEnv struct {
// flags
json bool // output JSON (status only for now)
servePort uint // Port to serve on. Defaults to 443.
terminateTLS bool
remove bool // remove a serve config
json bool // output JSON (status only for now)
lc localServeClient // localClient interface, specific to serve
@@ -162,15 +188,28 @@ func (e *serveEnv) getLocalClientStatus(ctx context.Context) (*ipnstate.Status,
return st, nil
}
// validateServePort returns --serve-port flag value,
// or an error if the port is not a valid port to serve on.
func (e *serveEnv) validateServePort() (port uint16, err error) {
// make sure e.servePort is uint16
port = uint16(e.servePort)
if uint(port) != e.servePort {
return 0, fmt.Errorf("serve-port %d is out of range", e.servePort)
}
// make sure e.servePort is 443, 8443 or 10000
if port != 443 && port != 8443 && port != 10000 {
return 0, fmt.Errorf("serve-port %d is invalid; must be 443, 8443 or 10000", e.servePort)
}
return port, nil
}
// runServe is the entry point for the "serve" subcommand, managing Web
// serve config types like proxy, path, and text.
//
// Examples:
// - tailscale serve https / http://localhost:3000
// - tailscale serve https /images/ /var/www/images/
// - tailscale serve https:10000 /motd.txt text:"Hello, world!"
// - tailscale serve tcp:2222 tcp://localhost:22
// - tailscale serve tls-terminated-tcp:443 tcp://localhost:80
// - tailscale serve / proxy 3000
// - tailscale serve /images/ path /var/www/images/
// - tailscale --serve-port=10000 serve /motd.txt text "Hello, world!"
func (e *serveEnv) runServe(ctx context.Context, args []string) error {
if len(args) == 0 {
return flag.ErrHelp
@@ -190,94 +229,39 @@ func (e *serveEnv) runServe(ctx context.Context, args []string) error {
return e.lc.SetServeConfig(ctx, sc)
}
parsePort := func(portStr string) (uint16, error) {
port64, err := strconv.ParseUint(portStr, 10, 16)
if err != nil {
return 0, err
}
return uint16(port64), nil
}
srcType, srcPortStr, found := strings.Cut(args[0], ":")
if !found {
if srcType == "https" && srcPortStr == "" {
// Default https port to 443.
srcPortStr = "443"
} else {
return flag.ErrHelp
}
}
turnOff := "off" == args[len(args)-1]
if len(args) < 2 || (srcType == "https" && !turnOff && len(args) < 3) {
if !(len(args) == 3 || (e.remove && len(args) >= 1)) {
fmt.Fprintf(os.Stderr, "error: invalid number of arguments\n\n")
return flag.ErrHelp
}
srcPort, err := parsePort(srcPortStr)
srvPort, err := e.validateServePort()
if err != nil {
return err
}
srvPortStr := strconv.Itoa(int(srvPort))
mount, err := cleanMountPoint(args[0])
if err != nil {
return err
}
switch srcType {
case "https":
mount, err := cleanMountPoint(args[1])
if err != nil {
return err
}
if turnOff {
return e.handleWebServeRemove(ctx, srcPort, mount)
}
return e.handleWebServe(ctx, srcPort, mount, args[2])
case "tcp", "tls-terminated-tcp":
if turnOff {
return e.handleTCPServeRemove(ctx, srcPort)
}
return e.handleTCPServe(ctx, srcType, srcPort, args[1])
default:
fmt.Fprintf(os.Stderr, "error: invalid serve type %q\n", srcType)
fmt.Fprint(os.Stderr, "must be one of: https:<port>, tcp:<port> or tls-terminated-tcp:<port>\n\n", srcType)
return flag.ErrHelp
if e.remove {
return e.handleWebServeRemove(ctx, mount)
}
}
// handleWebServe handles the "tailscale serve https:..." subcommand.
// It configures the serve config to forward HTTPS connections to the
// given source.
//
// Examples:
// - tailscale serve https / http://localhost:3000
// - tailscale serve https:8443 /files/ /home/alice/shared-files/
// - tailscale serve https:10000 /motd.txt text:"Hello, world!"
func (e *serveEnv) handleWebServe(ctx context.Context, srvPort uint16, mount, source string) error {
h := new(ipn.HTTPHandler)
ts, _, _ := strings.Cut(source, ":")
switch {
case ts == "text":
text := strings.TrimPrefix(source, "text:")
if text == "" {
return errors.New("unable to serve; text cannot be an empty string")
}
h.Text = text
case isProxyTarget(source):
t, err := expandProxyTarget(source)
if err != nil {
return err
}
h.Proxy = t
default: // assume path
switch args[1] {
case "path":
if version.IsSandboxedMacOS() {
// don't allow path serving for now on macOS (2022-11-15)
return fmt.Errorf("path serving is not supported if sandboxed on macOS")
}
if !filepath.IsAbs(source) {
if !filepath.IsAbs(args[2]) {
fmt.Fprintf(os.Stderr, "error: path must be absolute\n\n")
return flag.ErrHelp
}
source = filepath.Clean(source)
fi, err := os.Stat(source)
fi, err := os.Stat(args[2])
if err != nil {
fmt.Fprintf(os.Stderr, "error: invalid path: %v\n\n", err)
return flag.ErrHelp
@@ -287,7 +271,21 @@ func (e *serveEnv) handleWebServe(ctx context.Context, srvPort uint16, mount, so
// for relative file links to work
mount += "/"
}
h.Path = source
h.Path = args[2]
case "proxy":
t, err := expandProxyTarget(args[2])
if err != nil {
return err
}
h.Proxy = t
case "text":
if args[2] == "" {
return errors.New("unable to serve; text cannot be an empty string")
}
h.Text = args[2]
default:
fmt.Fprintf(os.Stderr, "error: unknown serve type %q\n\n", args[1])
return flag.ErrHelp
}
cursc, err := e.lc.GetServeConfig(ctx)
@@ -302,7 +300,7 @@ func (e *serveEnv) handleWebServe(ctx context.Context, srvPort uint16, mount, so
if err != nil {
return err
}
hp := ipn.HostPort(net.JoinHostPort(dnsName, strconv.Itoa(int(srvPort))))
hp := ipn.HostPort(net.JoinHostPort(dnsName, srvPortStr))
if sc.IsTCPForwardingOnPort(srvPort) {
fmt.Fprintf(os.Stderr, "error: cannot serve web; already serving TCP\n")
@@ -341,36 +339,12 @@ func (e *serveEnv) handleWebServe(ctx context.Context, srvPort uint16, mount, so
return nil
}
// isProxyTarget reports whether source is a valid proxy target.
func isProxyTarget(source string) bool {
if strings.HasPrefix(source, "http://") ||
strings.HasPrefix(source, "https://") ||
strings.HasPrefix(source, "https+insecure://") {
return true
func (e *serveEnv) handleWebServeRemove(ctx context.Context, mount string) error {
srvPort, err := e.validateServePort()
if err != nil {
return err
}
// support "localhost:3000", for example
_, portStr, ok := strings.Cut(source, ":")
if ok && allNumeric(portStr) {
return true
}
return false
}
// allNumeric reports whether s only comprises of digits
// and has at least one digit.
func allNumeric(s string) bool {
for i := 0; i < len(s); i++ {
if s[i] < '0' || s[i] > '9' {
return false
}
}
return s != ""
}
// handleWebServeRemove removes a web handler from the serve config.
// The srvPort argument is the serving port and the mount argument is
// the mount point or registered path to remove.
func (e *serveEnv) handleWebServeRemove(ctx context.Context, srvPort uint16, mount string) error {
srvPortStr := strconv.Itoa(int(srvPort))
sc, err := e.lc.GetServeConfig(ctx)
if err != nil {
return err
@@ -385,9 +359,9 @@ func (e *serveEnv) handleWebServeRemove(ctx context.Context, srvPort uint16, mou
if sc.IsTCPForwardingOnPort(srvPort) {
return errors.New("cannot remove web handler; currently serving TCP")
}
hp := ipn.HostPort(net.JoinHostPort(dnsName, strconv.Itoa(int(srvPort))))
hp := ipn.HostPort(net.JoinHostPort(dnsName, srvPortStr))
if !sc.WebHandlerExists(hp, mount) {
return errors.New("error: handler does not exist")
return errors.New("error: serve config does not exist")
}
// delete existing handler, then cascade delete if empty
delete(sc.Web[hp].Handlers, mount)
@@ -422,11 +396,18 @@ func cleanMountPoint(mount string) (string, error) {
return "", fmt.Errorf("invalid mount point %q", mount)
}
func expandProxyTarget(source string) (string, error) {
if !strings.Contains(source, "://") {
source = "http://" + source
func expandProxyTarget(target string) (string, error) {
if allNumeric(target) {
p, err := strconv.ParseUint(target, 10, 16)
if p == 0 || err != nil {
return "", fmt.Errorf("invalid port %q", target)
}
return "http://127.0.0.1:" + target, nil
}
u, err := url.ParseRequestURI(source)
if !strings.Contains(target, "://") {
target = "http://" + target
}
u, err := url.ParseRequestURI(target)
if err != nil {
return "", fmt.Errorf("parsing url: %w", err)
}
@@ -436,14 +417,9 @@ func expandProxyTarget(source string) (string, error) {
default:
return "", fmt.Errorf("must be a URL starting with http://, https://, or https+insecure://")
}
port, err := strconv.ParseUint(u.Port(), 10, 16)
if port == 0 || err != nil {
return "", fmt.Errorf("invalid port %q: %w", u.Port(), err)
}
host := u.Hostname()
switch host {
// TODO(shayne,bradfitz): do we want to do this?
case "localhost", "127.0.0.1":
host = "127.0.0.1"
default:
@@ -453,115 +429,19 @@ func expandProxyTarget(source string) (string, error) {
if u.Port() != "" {
url += ":" + u.Port()
}
url += u.Path
return url, nil
}
// handleTCPServe handles the "tailscale serve tls-terminated-tcp:..." subcommand.
// It configures the serve config to forward TCP connections to the
// given source.
//
// Examples:
// - tailscale serve tcp:2222 tcp://localhost:22
// - tailscale serve tls-terminated-tcp:8443 tcp://localhost:8080
func (e *serveEnv) handleTCPServe(ctx context.Context, srcType string, srcPort uint16, dest string) error {
var terminateTLS bool
switch srcType {
case "tcp":
terminateTLS = false
case "tls-terminated-tcp":
terminateTLS = true
default:
fmt.Fprintf(os.Stderr, "error: invalid TCP source %q\n\n", dest)
return flag.ErrHelp
}
dstURL, err := url.Parse(dest)
if err != nil {
fmt.Fprintf(os.Stderr, "error: invalid TCP source %q: %v\n\n", dest, err)
return flag.ErrHelp
}
host, dstPortStr, err := net.SplitHostPort(dstURL.Host)
if err != nil {
fmt.Fprintf(os.Stderr, "error: invalid TCP source %q: %v\n\n", dest, err)
return flag.ErrHelp
}
switch host {
case "localhost", "127.0.0.1":
// ok
default:
fmt.Fprintf(os.Stderr, "error: invalid TCP source %q\n", dest)
fmt.Fprint(os.Stderr, "must be one of: localhost or 127.0.0.1\n\n", dest)
return flag.ErrHelp
}
if p, err := strconv.ParseUint(dstPortStr, 10, 16); p == 0 || err != nil {
fmt.Fprintf(os.Stderr, "error: invalid port %q\n\n", dstPortStr)
return flag.ErrHelp
}
cursc, err := e.lc.GetServeConfig(ctx)
if err != nil {
return err
}
sc := cursc.Clone() // nil if no config
if sc == nil {
sc = new(ipn.ServeConfig)
}
fwdAddr := "127.0.0.1:" + dstPortStr
if sc.IsServingWeb(srcPort) {
return fmt.Errorf("cannot serve TCP; already serving web on %d", srcPort)
}
mak.Set(&sc.TCP, srcPort, &ipn.TCPPortHandler{TCPForward: fwdAddr})
dnsName, err := e.getSelfDNSName(ctx)
if err != nil {
return err
}
if terminateTLS {
sc.TCP[srcPort].TerminateTLS = dnsName
}
if !reflect.DeepEqual(cursc, sc) {
if err := e.lc.SetServeConfig(ctx, sc); err != nil {
return err
func allNumeric(s string) bool {
for i := 0; i < len(s); i++ {
if s[i] < '0' || s[i] > '9' {
return false
}
}
return nil
return s != ""
}
// handleTCPServeRemove removes the TCP forwarding configuration for the
// given srvPort, or serving port.
func (e *serveEnv) handleTCPServeRemove(ctx context.Context, src uint16) error {
cursc, err := e.lc.GetServeConfig(ctx)
if err != nil {
return err
}
sc := cursc.Clone() // nil if no config
if sc == nil {
sc = new(ipn.ServeConfig)
}
if sc.IsServingWeb(src) {
return fmt.Errorf("unable to remove; serving web, not TCP forwarding on serve port %d", src)
}
if ph := sc.GetTCPPortHandler(src); ph != nil {
delete(sc.TCP, src)
// clear map mostly for testing
if len(sc.TCP) == 0 {
sc.TCP = nil
}
return e.lc.SetServeConfig(ctx, sc)
}
return errors.New("error: serve config does not exist")
}
// runServeStatus is the entry point for the "serve status"
// subcommand and prints the current serve config.
// runServeStatus prints the current serve config.
//
// Examples:
// - tailscale status
@@ -580,7 +460,6 @@ func (e *serveEnv) runServeStatus(ctx context.Context, args []string) error {
e.stdout().Write(j)
return nil
}
printFunnelStatus(ctx)
if sc == nil || (len(sc.TCP) == 0 && len(sc.Web) == 0 && len(sc.AllowFunnel) == 0) {
printf("No serve config\n")
return nil
@@ -599,7 +478,17 @@ func (e *serveEnv) runServeStatus(ctx context.Context, args []string) error {
printWebStatusTree(sc, hp)
printf("\n")
}
printFunnelWarning(sc)
// warn when funnel on without handlers
for hp, a := range sc.AllowFunnel {
if !a {
continue
}
_, portStr, _ := net.SplitHostPort(string(hp))
p, _ := strconv.ParseUint(portStr, 10, 16)
if _, ok := sc.TCP[uint16(p)]; !ok {
printf("WARNING: funnel=on for %s, but no serve config\n", hp)
}
}
return nil
}
@@ -683,3 +572,152 @@ func elipticallyTruncate(s string, max int) string {
}
return s[:max-3] + "..."
}
// runServeTCP is the entry point for the "serve tcp" subcommand and
// manages the serve config for TCP forwarding.
//
// Examples:
// - tailscale serve tcp 5432
// - tailscale serve --serve-port=8443 tcp 4430
// - tailscale serve --serve-port=10000 tcp --terminate-tls 8080
func (e *serveEnv) runServeTCP(ctx context.Context, args []string) error {
if len(args) != 1 {
fmt.Fprintf(os.Stderr, "error: invalid number of arguments\n\n")
return flag.ErrHelp
}
srvPort, err := e.validateServePort()
if err != nil {
return err
}
portStr := args[0]
p, err := strconv.ParseUint(portStr, 10, 16)
if p == 0 || err != nil {
fmt.Fprintf(os.Stderr, "error: invalid port %q\n\n", portStr)
}
cursc, err := e.lc.GetServeConfig(ctx)
if err != nil {
return err
}
sc := cursc.Clone() // nil if no config
if sc == nil {
sc = new(ipn.ServeConfig)
}
fwdAddr := "127.0.0.1:" + portStr
if sc.IsServingWeb(srvPort) {
if e.remove {
return fmt.Errorf("unable to remove; serving web, not TCP forwarding on serve port %d", srvPort)
}
return fmt.Errorf("cannot serve TCP; already serving web on %d", srvPort)
}
if e.remove {
if ph := sc.GetTCPPortHandler(srvPort); ph != nil && ph.TCPForward == fwdAddr {
delete(sc.TCP, srvPort)
// clear map mostly for testing
if len(sc.TCP) == 0 {
sc.TCP = nil
}
return e.lc.SetServeConfig(ctx, sc)
}
return errors.New("error: serve config does not exist")
}
mak.Set(&sc.TCP, srvPort, &ipn.TCPPortHandler{TCPForward: fwdAddr})
dnsName, err := e.getSelfDNSName(ctx)
if err != nil {
return err
}
if e.terminateTLS {
sc.TCP[srvPort].TerminateTLS = dnsName
}
if !reflect.DeepEqual(cursc, sc) {
if err := e.lc.SetServeConfig(ctx, sc); err != nil {
return err
}
}
return nil
}
// runServeFunnel is the entry point for the "serve funnel" subcommand and
// manages turning on/off funnel. Funnel is off by default.
//
// Note: funnel is only supported on single DNS name for now. (2022-11-15)
func (e *serveEnv) runServeFunnel(ctx context.Context, args []string) error {
if len(args) != 1 {
return flag.ErrHelp
}
srvPort, err := e.validateServePort()
if err != nil {
return err
}
srvPortStr := strconv.Itoa(int(srvPort))
var on bool
switch args[0] {
case "on", "off":
on = args[0] == "on"
default:
return flag.ErrHelp
}
sc, err := e.lc.GetServeConfig(ctx)
if err != nil {
return err
}
if sc == nil {
sc = new(ipn.ServeConfig)
}
st, err := e.getLocalClientStatus(ctx)
if err != nil {
return fmt.Errorf("getting client status: %w", err)
}
if err := checkHasAccess(st.Self.Capabilities); err != nil {
return err
}
dnsName := strings.TrimSuffix(st.Self.DNSName, ".")
hp := ipn.HostPort(dnsName + ":" + srvPortStr)
if on == sc.AllowFunnel[hp] {
// Nothing to do.
return nil
}
if on {
mak.Set(&sc.AllowFunnel, hp, true)
} else {
delete(sc.AllowFunnel, hp)
// clear map mostly for testing
if len(sc.AllowFunnel) == 0 {
sc.AllowFunnel = nil
}
}
if err := e.lc.SetServeConfig(ctx, sc); err != nil {
return err
}
return nil
}
// checkHasAccess checks three things: 1) an invite was used to join the
// Funnel alpha; 2) HTTPS is enabled; 3) the node has the "funnel" attribute.
// If any of these are false, an error is returned describing the problem.
//
// The nodeAttrs arg should be the node's Self.Capabilities which should contain
// the attribute we're checking for and possibly warning-capabilities for Funnel.
func checkHasAccess(nodeAttrs []string) error {
if slices.Contains(nodeAttrs, tailcfg.CapabilityWarnFunnelNoInvite) {
return errors.New("Funnel not available; an invite is required to join the alpha. See https://tailscale.com/kb/1223/tailscale-funnel/.")
}
if slices.Contains(nodeAttrs, tailcfg.CapabilityWarnFunnelNoHTTPS) {
return errors.New("Funnel not available; HTTPS must be enabled. See https://tailscale.com/kb/1153/enabling-https/.")
}
if !slices.Contains(nodeAttrs, tailcfg.NodeAttrFunnel) {
return errors.New("Funnel not available; \"funnel\" node attribute not set. See https://tailscale.com/kb/1223/tailscale-funnel/.")
}
return nil
}

View File

@@ -15,7 +15,6 @@ import (
"strings"
"testing"
"github.com/peterbourgon/ff/v3/ffcli"
"tailscale.com/ipn"
"tailscale.com/ipn/ipnstate"
"tailscale.com/tailcfg"
@@ -49,6 +48,30 @@ func TestCleanMountPoint(t *testing.T) {
}
}
func TestCheckHasAccess(t *testing.T) {
tests := []struct {
caps []string
wantErr bool
}{
{[]string{}, true}, // No "funnel" attribute
{[]string{tailcfg.CapabilityWarnFunnelNoInvite}, true},
{[]string{tailcfg.CapabilityWarnFunnelNoHTTPS}, true},
{[]string{tailcfg.NodeAttrFunnel}, false},
}
for _, tt := range tests {
err := checkHasAccess(tt.caps)
switch {
case err != nil && tt.wantErr,
err == nil && !tt.wantErr:
continue
case tt.wantErr:
t.Fatalf("got no error, want error")
case !tt.wantErr:
t.Fatalf("got error %v, want no error", err)
}
}
}
func TestServeConfigMutations(t *testing.T) {
// Stateful mutations, starting from an empty config.
type step struct {
@@ -57,8 +80,6 @@ func TestServeConfigMutations(t *testing.T) {
want *ipn.ServeConfig // non-nil means we want a save of this value
wantErr func(error) (badErrMsg string) // nil means no error is wanted
line int // line number of addStep call, for error messages
debugBreak func()
}
var steps []step
add := func(s step) {
@@ -69,19 +90,19 @@ func TestServeConfigMutations(t *testing.T) {
// funnel
add(step{reset: true})
add(step{
command: cmd("funnel 443 on"),
command: cmd("funnel on"),
want: &ipn.ServeConfig{AllowFunnel: map[ipn.HostPort]bool{"foo.test.ts.net:443": true}},
})
add(step{
command: cmd("funnel 443 on"),
command: cmd("funnel on"),
want: nil, // nothing to save
})
add(step{
command: cmd("funnel 443 off"),
command: cmd("funnel off"),
want: &ipn.ServeConfig{},
})
add(step{
command: cmd("funnel 443 off"),
command: cmd("funnel off"),
want: nil, // nothing to save
})
add(step{
@@ -92,23 +113,27 @@ func TestServeConfigMutations(t *testing.T) {
// https
add(step{reset: true})
add(step{
command: cmd("https:443 / http://localhost:0"), // invalid port, too low
command: cmd("/ proxy 0"), // invalid port, too low
wantErr: anyErr(),
})
add(step{
command: cmd("https:443 / http://localhost:65536"), // invalid port, too high
command: cmd("/ proxy 65536"), // invalid port, too high
wantErr: anyErr(),
})
add(step{
command: cmd("https:443 / http://somehost:3000"), // invalid host
command: cmd("/ proxy somehost"), // invalid host
wantErr: anyErr(),
})
add(step{
command: cmd("https:443 / httpz://127.0.0.1"), // invalid scheme
command: cmd("/ proxy http://otherhost"), // invalid host
wantErr: anyErr(),
})
add(step{ // allow omitting port (default to 443)
command: cmd("https / http://localhost:3000"),
add(step{
command: cmd("/ proxy httpz://127.0.0.1"), // invalid scheme
wantErr: anyErr(),
})
add(step{
command: cmd("/ proxy 3000"),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
@@ -118,33 +143,12 @@ func TestServeConfigMutations(t *testing.T) {
},
},
})
add(step{ // support non Funnel port
command: cmd("https:9999 /abc http://localhost:3001"),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}, 9999: {HTTPS: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
"/": {Proxy: "http://127.0.0.1:3000"},
}},
"foo.test.ts.net:9999": {Handlers: map[string]*ipn.HTTPHandler{
"/abc": {Proxy: "http://127.0.0.1:3001"},
}},
},
},
add(step{ // invalid port
command: cmd("--serve-port=9999 /abc proxy 3001"),
wantErr: anyErr(),
})
add(step{
command: cmd("https:9999 /abc off"),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
"/": {Proxy: "http://127.0.0.1:3000"},
}},
},
},
})
add(step{
command: cmd("https:8443 /abc http://127.0.0.1:3001"),
command: cmd("--serve-port=8443 /abc proxy 3001"),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}, 8443: {HTTPS: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
@@ -158,7 +162,7 @@ func TestServeConfigMutations(t *testing.T) {
},
})
add(step{
command: cmd("https:10000 / text:hi"),
command: cmd("--serve-port=10000 / text hi"),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{
443: {HTTPS: true}, 8443: {HTTPS: true}, 10000: {HTTPS: true}},
@@ -176,12 +180,12 @@ func TestServeConfigMutations(t *testing.T) {
},
})
add(step{
command: cmd("https:443 /foo off"),
command: cmd("--remove /foo"),
want: nil, // nothing to save
wantErr: anyErr(),
}) // handler doesn't exist, so we get an error
add(step{
command: cmd("https:10000 / off"),
command: cmd("--remove --serve-port=10000 /"),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}, 8443: {HTTPS: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
@@ -195,7 +199,7 @@ func TestServeConfigMutations(t *testing.T) {
},
})
add(step{
command: cmd("https:443 / off"),
command: cmd("--remove /"),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{8443: {HTTPS: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
@@ -206,11 +210,11 @@ func TestServeConfigMutations(t *testing.T) {
},
})
add(step{
command: cmd("https:8443 /abc off"),
command: cmd("--remove --serve-port=8443 /abc"),
want: &ipn.ServeConfig{},
})
add(step{ // clean mount: "bar" becomes "/bar"
command: cmd("https:443 bar https://127.0.0.1:8443"),
add(step{
command: cmd("bar proxy https://127.0.0.1:8443"),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
@@ -221,12 +225,12 @@ func TestServeConfigMutations(t *testing.T) {
},
})
add(step{
command: cmd("https:443 bar https://127.0.0.1:8443"),
command: cmd("bar proxy https://127.0.0.1:8443"),
want: nil, // nothing to save
})
add(step{reset: true})
add(step{
command: cmd("https:443 / https+insecure://127.0.0.1:3001"),
command: cmd("/ proxy https+insecure://127.0.0.1:3001"),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
@@ -238,7 +242,7 @@ func TestServeConfigMutations(t *testing.T) {
})
add(step{reset: true})
add(step{
command: cmd("https:443 /foo localhost:3000"),
command: cmd("/foo proxy localhost:3000"),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
@@ -249,7 +253,7 @@ func TestServeConfigMutations(t *testing.T) {
},
})
add(step{ // test a second handler on the same port
command: cmd("https:8443 /foo localhost:3000"),
command: cmd("--serve-port=8443 /foo proxy localhost:3000"),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}, 8443: {HTTPS: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
@@ -262,50 +266,19 @@ func TestServeConfigMutations(t *testing.T) {
},
},
})
add(step{reset: true})
add(step{ // support path in proxy
command: cmd("https / http://127.0.0.1:3000/foo/bar"),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
"/": {Proxy: "http://127.0.0.1:3000/foo/bar"},
}},
},
},
})
// tcp
add(step{reset: true})
add(step{ // must include scheme for tcp
command: cmd("tls-terminated-tcp:443 localhost:5432"),
wantErr: exactErr(flag.ErrHelp, "flag.ErrHelp"),
})
add(step{ // !somehost, must be localhost or 127.0.0.1
command: cmd("tls-terminated-tcp:443 tcp://somehost:5432"),
wantErr: exactErr(flag.ErrHelp, "flag.ErrHelp"),
})
add(step{ // bad target port, too low
command: cmd("tls-terminated-tcp:443 tcp://somehost:0"),
wantErr: exactErr(flag.ErrHelp, "flag.ErrHelp"),
})
add(step{ // bad target port, too high
command: cmd("tls-terminated-tcp:443 tcp://somehost:65536"),
wantErr: exactErr(flag.ErrHelp, "flag.ErrHelp"),
})
add(step{
command: cmd("tls-terminated-tcp:443 tcp://localhost:5432"),
command: cmd("tcp 5432"),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{
443: {
TCPForward: "127.0.0.1:5432",
TerminateTLS: "foo.test.ts.net",
},
443: {TCPForward: "127.0.0.1:5432"},
},
},
})
add(step{
command: cmd("tls-terminated-tcp:443 tcp://127.0.0.1:8443"),
command: cmd("tcp -terminate-tls 8443"),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{
443: {
@@ -316,11 +289,11 @@ func TestServeConfigMutations(t *testing.T) {
},
})
add(step{
command: cmd("tls-terminated-tcp:443 tcp://127.0.0.1:8443"),
command: cmd("tcp -terminate-tls 8443"),
want: nil, // nothing to save
})
add(step{
command: cmd("tls-terminated-tcp:443 tcp://localhost:8444"),
command: cmd("tcp --terminate-tls 8444"),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{
443: {
@@ -331,41 +304,35 @@ func TestServeConfigMutations(t *testing.T) {
},
})
add(step{
command: cmd("tls-terminated-tcp:443 tcp://127.0.0.1:8445"),
command: cmd("tcp -terminate-tls=false 8445"),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{
443: {
TCPForward: "127.0.0.1:8445",
TerminateTLS: "foo.test.ts.net",
},
443: {TCPForward: "127.0.0.1:8445"},
},
},
})
add(step{reset: true})
add(step{
command: cmd("tls-terminated-tcp:443 tcp://localhost:123"),
command: cmd("tcp 123"),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{
443: {
TCPForward: "127.0.0.1:123",
TerminateTLS: "foo.test.ts.net",
},
443: {TCPForward: "127.0.0.1:123"},
},
},
})
add(step{ // handler doesn't exist, so we get an error
command: cmd("tls-terminated-tcp:8443 off"),
wantErr: anyErr(),
})
add(step{
command: cmd("tls-terminated-tcp:443 off"),
command: cmd("--remove tcp 321"),
wantErr: anyErr(),
}) // handler doesn't exist, so we get an error
add(step{
command: cmd("--remove tcp 123"),
want: &ipn.ServeConfig{},
})
// text
add(step{reset: true})
add(step{
command: cmd("https:443 / text:hello"),
command: cmd("/ text hello"),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
@@ -386,7 +353,7 @@ func TestServeConfigMutations(t *testing.T) {
add(step{reset: true})
writeFile("foo", "this is foo")
add(step{
command: cmd("https:443 / " + filepath.Join(td, "foo")),
command: cmd("/ path " + filepath.Join(td, "foo")),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
@@ -399,7 +366,7 @@ func TestServeConfigMutations(t *testing.T) {
os.MkdirAll(filepath.Join(td, "subdir"), 0700)
writeFile("subdir/file-a", "this is A")
add(step{
command: cmd("https:443 /some/where " + filepath.Join(td, "subdir/file-a")),
command: cmd("/some/where path " + filepath.Join(td, "subdir/file-a")),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
@@ -410,13 +377,13 @@ func TestServeConfigMutations(t *testing.T) {
},
},
})
add(step{ // bad path
command: cmd("https:443 / bad/path"),
add(step{
command: cmd("/ path missing"),
wantErr: exactErr(flag.ErrHelp, "flag.ErrHelp"),
})
add(step{reset: true})
add(step{
command: cmd("https:443 / " + filepath.Join(td, "subdir")),
command: cmd("/ path " + filepath.Join(td, "subdir")),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
@@ -427,14 +394,14 @@ func TestServeConfigMutations(t *testing.T) {
},
})
add(step{
command: cmd("https:443 / off"),
command: cmd("--remove /"),
want: &ipn.ServeConfig{},
})
// combos
add(step{reset: true})
add(step{
command: cmd("https:443 / localhost:3000"),
command: cmd("/ proxy 3000"),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
@@ -445,7 +412,7 @@ func TestServeConfigMutations(t *testing.T) {
},
})
add(step{
command: cmd("funnel 443 on"),
command: cmd("funnel on"),
want: &ipn.ServeConfig{
AllowFunnel: map[ipn.HostPort]bool{"foo.test.ts.net:443": true},
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
@@ -457,7 +424,7 @@ func TestServeConfigMutations(t *testing.T) {
},
})
add(step{ // serving on secondary port doesn't change funnel
command: cmd("https:8443 /bar localhost:3001"),
command: cmd("--serve-port=8443 /bar proxy 3001"),
want: &ipn.ServeConfig{
AllowFunnel: map[ipn.HostPort]bool{"foo.test.ts.net:443": true},
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}, 8443: {HTTPS: true}},
@@ -472,7 +439,7 @@ func TestServeConfigMutations(t *testing.T) {
},
})
add(step{ // turn funnel on for secondary port
command: cmd("funnel 8443 on"),
command: cmd("--serve-port=8443 funnel on"),
want: &ipn.ServeConfig{
AllowFunnel: map[ipn.HostPort]bool{"foo.test.ts.net:443": true, "foo.test.ts.net:8443": true},
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}, 8443: {HTTPS: true}},
@@ -487,7 +454,7 @@ func TestServeConfigMutations(t *testing.T) {
},
})
add(step{ // turn funnel off for primary port 443
command: cmd("funnel 443 off"),
command: cmd("funnel off"),
want: &ipn.ServeConfig{
AllowFunnel: map[ipn.HostPort]bool{"foo.test.ts.net:8443": true},
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}, 8443: {HTTPS: true}},
@@ -502,7 +469,7 @@ func TestServeConfigMutations(t *testing.T) {
},
})
add(step{ // remove secondary port
command: cmd("https:8443 /bar off"),
command: cmd("--serve-port=8443 --remove /bar"),
want: &ipn.ServeConfig{
AllowFunnel: map[ipn.HostPort]bool{"foo.test.ts.net:8443": true},
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
@@ -514,7 +481,7 @@ func TestServeConfigMutations(t *testing.T) {
},
})
add(step{ // start a tcp forwarder on 8443
command: cmd("tcp:8443 tcp://localhost:5432"),
command: cmd("--serve-port=8443 tcp 5432"),
want: &ipn.ServeConfig{
AllowFunnel: map[ipn.HostPort]bool{"foo.test.ts.net:8443": true},
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}, 8443: {TCPForward: "127.0.0.1:5432"}},
@@ -526,27 +493,27 @@ func TestServeConfigMutations(t *testing.T) {
},
})
add(step{ // remove primary port http handler
command: cmd("https:443 / off"),
command: cmd("--remove /"),
want: &ipn.ServeConfig{
AllowFunnel: map[ipn.HostPort]bool{"foo.test.ts.net:8443": true},
TCP: map[uint16]*ipn.TCPPortHandler{8443: {TCPForward: "127.0.0.1:5432"}},
},
})
add(step{ // remove tcp forwarder
command: cmd("tls-terminated-tcp:8443 off"),
command: cmd("--serve-port=8443 --remove tcp 5432"),
want: &ipn.ServeConfig{
AllowFunnel: map[ipn.HostPort]bool{"foo.test.ts.net:8443": true},
},
})
add(step{ // turn off funnel
command: cmd("funnel 8443 off"),
command: cmd("--serve-port=8443 funnel off"),
want: &ipn.ServeConfig{},
})
// tricky steps
add(step{reset: true})
add(step{ // a directory with a trailing slash mount point
command: cmd("https:443 /dir " + filepath.Join(td, "subdir")),
command: cmd("/dir path " + filepath.Join(td, "subdir")),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
@@ -557,7 +524,7 @@ func TestServeConfigMutations(t *testing.T) {
},
})
add(step{ // this should overwrite the previous one
command: cmd("https:443 /dir " + filepath.Join(td, "foo")),
command: cmd("/dir path " + filepath.Join(td, "foo")),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
@@ -569,7 +536,7 @@ func TestServeConfigMutations(t *testing.T) {
})
add(step{reset: true}) // reset and do the opposite
add(step{ // a file without a trailing slash mount point
command: cmd("https:443 /dir " + filepath.Join(td, "foo")),
command: cmd("/dir path " + filepath.Join(td, "foo")),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
@@ -580,7 +547,7 @@ func TestServeConfigMutations(t *testing.T) {
},
})
add(step{ // this should overwrite the previous one
command: cmd("https:443 /dir " + filepath.Join(td, "subdir")),
command: cmd("/dir path " + filepath.Join(td, "subdir")),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
@@ -593,24 +560,37 @@ func TestServeConfigMutations(t *testing.T) {
// error states
add(step{reset: true})
add(step{ // make sure we can't add "tcp" as if it was a mount
command: cmd("tcp text foo"),
wantErr: exactErr(flag.ErrHelp, "flag.ErrHelp"),
})
add(step{ // "/tcp" is fine though as a mount
command: cmd("/tcp text foo"),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
"/tcp": {Text: "foo"},
}},
},
},
})
add(step{reset: true})
add(step{ // tcp forward 5432 on serve port 443
command: cmd("tls-terminated-tcp:443 tcp://localhost:5432"),
command: cmd("tcp 5432"),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{
443: {
TCPForward: "127.0.0.1:5432",
TerminateTLS: "foo.test.ts.net",
},
443: {TCPForward: "127.0.0.1:5432"},
},
},
})
add(step{ // try to start a web handler on the same port
command: cmd("https:443 / localhost:3000"),
command: cmd("/ proxy 3000"),
wantErr: exactErr(flag.ErrHelp, "flag.ErrHelp"),
})
add(step{reset: true})
add(step{ // start a web handler on port 443
command: cmd("https:443 / localhost:3000"),
command: cmd("/ proxy 3000"),
want: &ipn.ServeConfig{
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
Web: map[ipn.HostPort]*ipn.WebServerConfig{
@@ -620,17 +600,14 @@ func TestServeConfigMutations(t *testing.T) {
},
},
})
add(step{ // try to start a tcp forwarder on the same serve port
command: cmd("tls-terminated-tcp:443 tcp://localhost:5432"),
add(step{ // try to start a tcp forwarder on the same serve port (443 default)
command: cmd("tcp 5432"),
wantErr: anyErr(),
})
lc := &fakeLocalServeClient{}
// And now run the steps above.
for i, st := range steps {
if st.debugBreak != nil {
st.debugBreak()
}
if st.reset {
t.Logf("Executing step #%d, line %v: [reset]", i, st.line)
lc.config = nil
@@ -648,16 +625,8 @@ func TestServeConfigMutations(t *testing.T) {
testStdout: &stdout,
}
lastCount := lc.setCount
var cmd *ffcli.Command
var args []string
if st.command[0] == "funnel" {
cmd = newFunnelCommand(e)
args = st.command[1:]
} else {
cmd = newServeCommand(e)
args = st.command
}
err := cmd.ParseAndRun(context.Background(), args)
cmd := newServeCommand(e)
err := cmd.ParseAndRun(context.Background(), st.command)
if flagOut.Len() > 0 {
t.Logf("flag package output: %q", flagOut.Bytes())
}
@@ -708,7 +677,7 @@ var fakeStatus = &ipnstate.Status{
BackendState: ipn.Running.String(),
Self: &ipnstate.PeerStatus{
DNSName: "foo.test.ts.net",
Capabilities: []string{tailcfg.NodeAttrFunnel, tailcfg.CapabilityFunnelPorts + "?ports=443,8443"},
Capabilities: []string{tailcfg.NodeAttrFunnel},
},
}
@@ -748,5 +717,7 @@ func anyErr() func(error) string {
}
func cmd(s string) []string {
return strings.Fields(s)
cmds := strings.Fields(s)
fmt.Printf("cmd: %v", cmds)
return cmds
}

View File

@@ -258,7 +258,6 @@ func printFunnelStatus(ctx context.Context) {
}
printf("# - %s\n", url)
}
outln()
}
// isRunningOrStarting reports whether st is in state Running or Starting.
@@ -276,7 +275,7 @@ func isRunningOrStarting(st *ipnstate.Status) (description string, ok bool) {
}
return s, false
case ipn.NeedsMachineAuth.String():
return "Machine is not yet approved by tailnet admin.", false
return "Machine is not yet authorized by tailnet admin.", false
case ipn.Running.String(), ipn.Starting.String():
return st.BackendState, true
}

View File

@@ -34,7 +34,6 @@ import (
"tailscale.com/tailcfg"
"tailscale.com/types/logger"
"tailscale.com/types/preftype"
"tailscale.com/util/dnsname"
"tailscale.com/version"
"tailscale.com/version/distro"
)
@@ -321,8 +320,8 @@ func prefsFromUpArgs(upArgs upArgsT, warnf logger.Logf, st *ipnstate.Status, goo
}
}
if err := dnsname.ValidHostname(upArgs.hostname); upArgs.hostname != "" && err != nil {
return nil, err
if len(upArgs.hostname) > 256 {
return nil, fmt.Errorf("hostname too long: %d bytes (max 256)", len(upArgs.hostname))
}
prefs := ipn.NewPrefs()
@@ -410,12 +409,6 @@ func updatePrefs(prefs, curPrefs *ipn.Prefs, env upCheckEnv) (simpleUp bool, jus
return false, nil, err
}
if env.upArgs.forceReauth && isSSHOverTailscale() {
if err := presentRiskToUser(riskLoseSSH, `You are connected over Tailscale; this action will result in your SSH session disconnecting.`, env.upArgs.acceptedRisks); err != nil {
return false, nil, err
}
}
tagsChanged := !reflect.DeepEqual(curPrefs.AdvertiseTags, prefs.AdvertiseTags)
simpleUp = env.flagSet.NFlag() == 0 &&
@@ -591,7 +584,7 @@ func runUp(ctx context.Context, cmd string, args []string, upArgs upArgsT) (retE
if env.upArgs.json {
printUpDoneJSON(ipn.NeedsMachineAuth, "")
} else {
fmt.Fprintf(Stderr, "\nTo approve your machine, visit (as admin):\n\n\t%s\n\n", prefs.AdminPageURL())
fmt.Fprintf(Stderr, "\nTo authorize your machine, visit (as admin):\n\n\t%s\n\n", prefs.AdminPageURL())
}
case ipn.Running:
// Done full authentication process

View File

@@ -145,11 +145,11 @@ func newUpdater() (*updater, error) {
case strings.HasSuffix(os.Getenv("HOME"), "/io.tailscale.ipn.macsys/Data"):
up.update = up.updateMacSys
default:
return nil, errors.New("This is the macOS App Store version of Tailscale; update in the App Store, or see https://tailscale.com/s/unstable-clients to use TestFlight or to install the non-App Store version")
return nil, errors.New("This is the macOS App Store version of Tailscale; update in the App Store, or see https://tailscale.com/kb/1083/install-unstable/ to use TestFlight or to install the non-App Store version")
}
}
if up.update == nil {
return nil, errors.New("The 'update' command is not supported on this platform; see https://tailscale.com/s/client-updates")
return nil, errors.New("The 'update' command is not supported on this platform; see https://tailscale.com/kb/1067/update/")
}
return up, nil
}

View File

@@ -228,48 +228,33 @@ func qnapAuthn(r *http.Request) (string, *qnapAuthResponse, error) {
return "", nil, fmt.Errorf("not authenticated by any mechanism")
}
// qnapAuthnURL returns the auth URL to use by inferring where the UI is
// running based on the request URL. This is necessary because QNAP has so
// many options, see https://github.com/tailscale/tailscale/issues/7108
// and https://github.com/tailscale/tailscale/issues/6903
func qnapAuthnURL(requestUrl string, query url.Values) string {
in, err := url.Parse(requestUrl)
scheme := ""
host := ""
if err != nil || in.Scheme == "" {
log.Printf("Cannot parse QNAP login URL %v", err)
// try localhost and hope for the best
scheme = "http"
host = "localhost"
} else {
scheme = in.Scheme
host = in.Host
}
u := url.URL{
Scheme: scheme,
Host: host,
Path: "/cgi-bin/authLogin.cgi",
RawQuery: query.Encode(),
}
return u.String()
}
func qnapAuthnQtoken(r *http.Request, user, token string) (string, *qnapAuthResponse, error) {
query := url.Values{
"qtoken": []string{token},
"user": []string{user},
}
return qnapAuthnFinish(user, qnapAuthnURL(r.URL.String(), query))
u := url.URL{
Scheme: "http",
Host: "127.0.0.1:8080",
Path: "/cgi-bin/authLogin.cgi",
RawQuery: query.Encode(),
}
return qnapAuthnFinish(user, u.String())
}
func qnapAuthnSid(r *http.Request, user, sid string) (string, *qnapAuthResponse, error) {
query := url.Values{
"sid": []string{sid},
}
return qnapAuthnFinish(user, qnapAuthnURL(r.URL.String(), query))
u := url.URL{
Scheme: "http",
Host: "127.0.0.1:8080",
Path: "/cgi-bin/authLogin.cgi",
RawQuery: query.Encode(),
}
return qnapAuthnFinish(user, u.String())
}
func qnapAuthnFinish(user, url string) (string, *qnapAuthResponse, error) {

View File

@@ -26,9 +26,9 @@
<circle opacity="0.2" cx="19.5" cy="19.5" r="2.7" fill="currentColor"></circle>
</svg>
<div class="flex items-center justify-end space-x-2 w-2/3">
{{ with .Profile }}
{{ with .Profile.LoginName }}
<div class="text-right w-full leading-4">
<h4 class="truncate leading-normal">{{.LoginName}}</h4>
<h4 class="truncate leading-normal">{{.}}</h4>
<div class="text-xs text-gray-500 text-right">
<a href="#" class="hover:text-gray-700 js-loginButton">Switch account</a> | <a href="#"
class="hover:text-gray-700 js-loginButton">Reauthenticate</a> | <a href="#"

View File

@@ -3,10 +3,7 @@
package cli
import (
"net/url"
"testing"
)
import "testing"
func TestUrlOfListenAddr(t *testing.T) {
tests := []struct {
@@ -37,64 +34,9 @@ func TestUrlOfListenAddr(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
u := urlOfListenAddr(tt.in)
if u != tt.want {
t.Errorf("expected url: %q, got: %q", tt.want, u)
}
})
}
}
func TestQnapAuthnURL(t *testing.T) {
query := url.Values{
"qtoken": []string{"token"},
}
tests := []struct {
name string
in string
want string
}{
{
name: "localhost http",
in: "http://localhost:8088/",
want: "http://localhost:8088/cgi-bin/authLogin.cgi?qtoken=token",
},
{
name: "localhost https",
in: "https://localhost:5000/",
want: "https://localhost:5000/cgi-bin/authLogin.cgi?qtoken=token",
},
{
name: "IP http",
in: "http://10.1.20.4:80/",
want: "http://10.1.20.4:80/cgi-bin/authLogin.cgi?qtoken=token",
},
{
name: "IP6 https",
in: "https://[ff7d:0:1:2::1]/",
want: "https://[ff7d:0:1:2::1]/cgi-bin/authLogin.cgi?qtoken=token",
},
{
name: "hostname https",
in: "https://qnap.example.com/",
want: "https://qnap.example.com/cgi-bin/authLogin.cgi?qtoken=token",
},
{
name: "invalid URL",
in: "This is not a URL, it is a really really really really really really really really really really really really long string to exercise the URL truncation code in the error path.",
want: "http://localhost/cgi-bin/authLogin.cgi?qtoken=token",
},
{
name: "err != nil",
in: "http://192.168.0.%31/",
want: "http://localhost/cgi-bin/authLogin.cgi?qtoken=token",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
u := qnapAuthnURL(tt.in, query)
if u != tt.want {
t.Errorf("expected url: %q, got: %q", tt.want, u)
url := urlOfListenAddr(tt.in)
if url != tt.want {
t.Errorf("expected url: %q, got: %q", tt.want, url)
}
})
}

View File

@@ -13,7 +13,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
github.com/google/uuid from tailscale.com/util/quarantine+
github.com/hdevalence/ed25519consensus from tailscale.com/tka
L github.com/josharian/native from github.com/mdlayher/netlink+
L 💣 github.com/jsimonetti/rtnetlink from tailscale.com/net/interfaces+
L 💣 github.com/jsimonetti/rtnetlink from tailscale.com/net/interfaces
L github.com/jsimonetti/rtnetlink/internal/unix from github.com/jsimonetti/rtnetlink
github.com/kballard/go-shellquote from tailscale.com/cmd/tailscale/cli
github.com/klauspost/compress/flate from nhooyr.io/websocket
@@ -74,13 +74,11 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
tailscale.com/net/netcheck from tailscale.com/cmd/tailscale/cli
tailscale.com/net/neterror from tailscale.com/net/netcheck+
tailscale.com/net/netknob from tailscale.com/net/netns
tailscale.com/net/netmon from tailscale.com/net/sockstats+
tailscale.com/net/netns from tailscale.com/derp/derphttp+
tailscale.com/net/netutil from tailscale.com/client/tailscale+
tailscale.com/net/packet from tailscale.com/wgengine/filter+
tailscale.com/net/packet from tailscale.com/wgengine/filter
tailscale.com/net/ping from tailscale.com/net/netcheck
tailscale.com/net/portmapper from tailscale.com/net/netcheck+
tailscale.com/net/sockstats from tailscale.com/control/controlhttp+
tailscale.com/net/stun from tailscale.com/net/netcheck
tailscale.com/net/tlsdial from tailscale.com/derp/derphttp+
tailscale.com/net/tsaddr from tailscale.com/net/interfaces+
@@ -93,7 +91,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
tailscale.com/tka from tailscale.com/client/tailscale+
W tailscale.com/tsconst from tailscale.com/net/interfaces
💣 tailscale.com/tstime/mono from tailscale.com/tstime/rate
tailscale.com/tstime/rate from tailscale.com/wgengine/filter+
tailscale.com/tstime/rate from tailscale.com/wgengine/filter
tailscale.com/types/dnstype from tailscale.com/tailcfg
tailscale.com/types/empty from tailscale.com/ipn
tailscale.com/types/ipproto from tailscale.com/net/flowtrack+
@@ -123,7 +121,6 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
tailscale.com/util/quarantine from tailscale.com/cmd/tailscale/cli
tailscale.com/util/set from tailscale.com/health+
tailscale.com/util/singleflight from tailscale.com/net/dnscache
tailscale.com/util/slicesx from tailscale.com/net/dnscache+
💣 tailscale.com/util/winutil from tailscale.com/hostinfo+
tailscale.com/version from tailscale.com/cmd/tailscale/cli+
tailscale.com/version/distro from tailscale.com/cmd/tailscale/cli+

View File

@@ -6,3 +6,4 @@ package main
//go:generate go run tailscale.com/cmd/mkmanifest amd64 windows-manifest.xml manifest_windows_amd64.syso
//go:generate go run tailscale.com/cmd/mkmanifest 386 windows-manifest.xml manifest_windows_386.syso
//go:generate go run tailscale.com/cmd/mkmanifest arm64 windows-manifest.xml manifest_windows_arm64.syso
//go:generate go run tailscale.com/cmd/mkmanifest arm windows-manifest.xml manifest_windows_arm.syso

Binary file not shown.

View File

@@ -14,19 +14,25 @@ import (
"fmt"
"io"
"log"
"net"
"net/http"
"net/http/httptrace"
"net/netip"
"net/url"
"os"
"strings"
"time"
"tailscale.com/derp/derphttp"
"tailscale.com/envknob"
"tailscale.com/ipn"
"tailscale.com/net/interfaces"
"tailscale.com/net/netmon"
"tailscale.com/net/portmapper"
"tailscale.com/net/tshttpproxy"
"tailscale.com/tailcfg"
"tailscale.com/types/key"
"tailscale.com/types/logger"
"tailscale.com/wgengine/monitor"
)
var debugArgs struct {
@@ -42,7 +48,7 @@ var debugModeFunc = debugMode // so it can be addressable
func debugMode(args []string) error {
fs := flag.NewFlagSet("debug", flag.ExitOnError)
fs.BoolVar(&debugArgs.ifconfig, "ifconfig", false, "If true, print network interface state")
fs.BoolVar(&debugArgs.monitor, "monitor", false, "If true, run network monitor forever. Precludes all other options.")
fs.BoolVar(&debugArgs.monitor, "monitor", false, "If true, run link monitor forever. Precludes all other options.")
fs.BoolVar(&debugArgs.portmap, "portmap", false, "If true, run portmap debugging. Precludes all other options.")
fs.StringVar(&debugArgs.getURL, "get-url", "", "If non-empty, fetch provided URL.")
fs.StringVar(&debugArgs.derpCheck, "derp", "", "if non-empty, test a DERP ping via named region code")
@@ -76,7 +82,7 @@ func runMonitor(ctx context.Context, loop bool) error {
j, _ := json.MarshalIndent(st, "", " ")
os.Stderr.Write(j)
}
mon, err := netmon.New(log.Printf)
mon, err := monitor.New(log.Printf)
if err != nil {
return err
}
@@ -84,10 +90,10 @@ func runMonitor(ctx context.Context, loop bool) error {
mon.RegisterChangeCallback(func(changed bool, st *interfaces.State) {
if !changed {
log.Printf("Network monitor fired; no change")
log.Printf("Link monitor fired; no change")
return
}
log.Printf("Network monitor fired. New state:")
log.Printf("Link monitor fired. New state:")
dump(st)
})
if loop {
@@ -193,8 +199,8 @@ func checkDerp(ctx context.Context, derpRegion string) (err error) {
priv1 := key.NewNode()
priv2 := key.NewNode()
c1 := derphttp.NewRegionClient(priv1, log.Printf, nil, getRegion)
c2 := derphttp.NewRegionClient(priv2, log.Printf, nil, getRegion)
c1 := derphttp.NewRegionClient(priv1, log.Printf, getRegion)
c2 := derphttp.NewRegionClient(priv2, log.Printf, getRegion)
defer func() {
if err != nil {
c1.Close()
@@ -223,5 +229,95 @@ func checkDerp(ctx context.Context, derpRegion string) (err error) {
}
func debugPortmap(ctx context.Context) error {
return fmt.Errorf("this flag has been deprecated in favour of 'tailscale debug portmap'")
ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
defer cancel()
portmapper.VerboseLogs = true
switch envknob.String("TS_DEBUG_PORTMAP_TYPE") {
case "":
case "pmp":
portmapper.DisablePCP = true
portmapper.DisableUPnP = true
case "pcp":
portmapper.DisablePMP = true
portmapper.DisableUPnP = true
case "upnp":
portmapper.DisablePCP = true
portmapper.DisablePMP = true
default:
log.Fatalf("TS_DEBUG_PORTMAP_TYPE must be one of pmp,pcp,upnp")
}
done := make(chan bool, 1)
var c *portmapper.Client
logf := log.Printf
c = portmapper.NewClient(logger.WithPrefix(logf, "portmapper: "), func() {
logf("portmapping changed.")
logf("have mapping: %v", c.HaveMapping())
if ext, ok := c.GetCachedMappingOrStartCreatingOne(); ok {
logf("cb: mapping: %v", ext)
select {
case done <- true:
default:
}
return
}
logf("cb: no mapping")
})
linkMon, err := monitor.New(logger.WithPrefix(logf, "monitor: "))
if err != nil {
return err
}
gatewayAndSelfIP := func() (gw, self netip.Addr, ok bool) {
if v := os.Getenv("TS_DEBUG_GW_SELF"); strings.Contains(v, "/") {
i := strings.Index(v, "/")
gw = netip.MustParseAddr(v[:i])
self = netip.MustParseAddr(v[i+1:])
return gw, self, true
}
return linkMon.GatewayAndSelfIP()
}
c.SetGatewayLookupFunc(gatewayAndSelfIP)
gw, selfIP, ok := gatewayAndSelfIP()
if !ok {
logf("no gateway or self IP; %v", linkMon.InterfaceState())
return nil
}
logf("gw=%v; self=%v", gw, selfIP)
uc, err := net.ListenPacket("udp", "0.0.0.0:0")
if err != nil {
return err
}
defer uc.Close()
c.SetLocalPort(uint16(uc.LocalAddr().(*net.UDPAddr).Port))
res, err := c.Probe(ctx)
if err != nil {
return fmt.Errorf("Probe: %v", err)
}
logf("Probe: %+v", res)
if !res.PCP && !res.PMP && !res.UPnP {
logf("no portmapping services available")
return nil
}
if ext, ok := c.GetCachedMappingOrStartCreatingOne(); ok {
logf("mapping: %v", ext)
} else {
logf("no mapping")
}
select {
case <-done:
return nil
case <-ctx.Done():
return ctx.Err()
}
}

View File

@@ -154,7 +154,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
💣 gvisor.dev/gvisor/pkg/state from gvisor.dev/gvisor/pkg/atomicbitops+
gvisor.dev/gvisor/pkg/state/wire from gvisor.dev/gvisor/pkg/state
💣 gvisor.dev/gvisor/pkg/sync from gvisor.dev/gvisor/pkg/linewriter+
💣 gvisor.dev/gvisor/pkg/sync/locking from gvisor.dev/gvisor/pkg/tcpip/stack
gvisor.dev/gvisor/pkg/tcpip from gvisor.dev/gvisor/pkg/tcpip/header+
gvisor.dev/gvisor/pkg/tcpip/adapters/gonet from tailscale.com/wgengine/netstack
gvisor.dev/gvisor/pkg/tcpip/checksum from gvisor.dev/gvisor/pkg/bufferv2+
@@ -201,7 +200,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/derp/derphttp from tailscale.com/net/netcheck+
tailscale.com/disco from tailscale.com/derp+
tailscale.com/doctor from tailscale.com/ipn/ipnlocal
💣 tailscale.com/doctor/permissions from tailscale.com/ipn/ipnlocal
tailscale.com/doctor/routetable from tailscale.com/ipn/ipnlocal
tailscale.com/envknob from tailscale.com/control/controlclient+
tailscale.com/health from tailscale.com/control/controlclient+
@@ -214,18 +212,17 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/ipn/ipnstate from tailscale.com/control/controlclient+
tailscale.com/ipn/localapi from tailscale.com/ipn/ipnserver
tailscale.com/ipn/policy from tailscale.com/ipn/ipnlocal
tailscale.com/ipn/store from tailscale.com/cmd/tailscaled+
tailscale.com/ipn/store from tailscale.com/cmd/tailscaled
L tailscale.com/ipn/store/awsstore from tailscale.com/ipn/store
L tailscale.com/ipn/store/kubestore from tailscale.com/ipn/store
tailscale.com/ipn/store/mem from tailscale.com/ipn/store+
L tailscale.com/kube from tailscale.com/ipn/store/kubestore
tailscale.com/log/filelogger from tailscale.com/logpolicy
tailscale.com/log/logheap from tailscale.com/control/controlclient
tailscale.com/log/sockstatlog from tailscale.com/ipn/ipnlocal
tailscale.com/logpolicy from tailscale.com/cmd/tailscaled+
tailscale.com/logtail from tailscale.com/control/controlclient+
tailscale.com/logtail/backoff from tailscale.com/control/controlclient+
tailscale.com/logtail/filch from tailscale.com/logpolicy+
tailscale.com/logtail/filch from tailscale.com/logpolicy
tailscale.com/metrics from tailscale.com/derp+
tailscale.com/net/connstats from tailscale.com/net/tstun+
tailscale.com/net/dns from tailscale.com/ipn/ipnlocal+
@@ -240,7 +237,6 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/net/netcheck from tailscale.com/wgengine/magicsock
tailscale.com/net/neterror from tailscale.com/net/dns/resolver+
tailscale.com/net/netknob from tailscale.com/net/netns+
tailscale.com/net/netmon from tailscale.com/cmd/tailscaled+
tailscale.com/net/netns from tailscale.com/derp/derphttp+
💣 tailscale.com/net/netstat from tailscale.com/ipn/ipnauth+
tailscale.com/net/netutil from tailscale.com/ipn/ipnlocal+
@@ -250,14 +246,12 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/net/proxymux from tailscale.com/cmd/tailscaled
tailscale.com/net/routetable from tailscale.com/doctor/routetable
tailscale.com/net/socks5 from tailscale.com/cmd/tailscaled
tailscale.com/net/sockstats from tailscale.com/control/controlclient+
tailscale.com/net/stun from tailscale.com/net/netcheck+
tailscale.com/net/tlsdial from tailscale.com/control/controlclient+
tailscale.com/net/tsaddr from tailscale.com/ipn+
tailscale.com/net/tsdial from tailscale.com/control/controlclient+
💣 tailscale.com/net/tshttpproxy from tailscale.com/control/controlclient+
tailscale.com/net/tstun from tailscale.com/cmd/tailscaled+
tailscale.com/net/tstun/table from tailscale.com/net/tstun
tailscale.com/net/wsconn from tailscale.com/control/controlhttp+
tailscale.com/paths from tailscale.com/ipn/ipnlocal+
💣 tailscale.com/portlist from tailscale.com/ipn/ipnlocal
@@ -266,14 +260,13 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
LD 💣 tailscale.com/ssh/tailssh from tailscale.com/cmd/tailscaled
tailscale.com/syncs from tailscale.com/net/netcheck+
tailscale.com/tailcfg from tailscale.com/client/tailscale/apitype+
💣 tailscale.com/tempfork/device from tailscale.com/net/tstun/table
LD tailscale.com/tempfork/gliderlabs/ssh from tailscale.com/ssh/tailssh
tailscale.com/tka from tailscale.com/ipn/ipnlocal+
W tailscale.com/tsconst from tailscale.com/net/interfaces
tailscale.com/tstime from tailscale.com/wgengine/magicsock
💣 tailscale.com/tstime/mono from tailscale.com/net/tstun+
tailscale.com/tstime/rate from tailscale.com/wgengine/filter+
tailscale.com/tsweb/varz from tailscale.com/cmd/tailscaled
tailscale.com/tstime/rate from tailscale.com/wgengine/filter
tailscale.com/tsweb from tailscale.com/cmd/tailscaled
tailscale.com/types/dnstype from tailscale.com/ipn/ipnlocal+
tailscale.com/types/empty from tailscale.com/control/controlclient+
tailscale.com/types/flagtype from tailscale.com/cmd/tailscaled
@@ -305,19 +298,15 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/util/lineread from tailscale.com/hostinfo+
tailscale.com/util/mak from tailscale.com/control/controlclient+
tailscale.com/util/multierr from tailscale.com/control/controlclient+
tailscale.com/util/must from tailscale.com/logpolicy
tailscale.com/util/osshare from tailscale.com/ipn/ipnlocal+
W tailscale.com/util/pidowner from tailscale.com/ipn/ipnauth
tailscale.com/util/racebuild from tailscale.com/logpolicy
tailscale.com/util/ringbuffer from tailscale.com/wgengine/magicsock
tailscale.com/util/set from tailscale.com/health+
tailscale.com/util/singleflight from tailscale.com/control/controlclient+
tailscale.com/util/slicesx from tailscale.com/net/dnscache+
tailscale.com/util/sysresources from tailscale.com/wgengine/magicsock
tailscale.com/util/systemd from tailscale.com/control/controlclient+
tailscale.com/util/uniq from tailscale.com/wgengine/magicsock+
tailscale.com/util/vizerror from tailscale.com/tsweb
💣 tailscale.com/util/winutil from tailscale.com/control/controlclient+
W tailscale.com/util/winutil/policy from tailscale.com/ipn/ipnlocal
tailscale.com/version from tailscale.com/derp+
tailscale.com/version/distro from tailscale.com/hostinfo+
W tailscale.com/wf from tailscale.com/cmd/tailscaled
@@ -325,6 +314,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
tailscale.com/wgengine/capture from tailscale.com/ipn/ipnlocal+
tailscale.com/wgengine/filter from tailscale.com/control/controlclient+
💣 tailscale.com/wgengine/magicsock from tailscale.com/ipn/ipnlocal+
tailscale.com/wgengine/monitor from tailscale.com/control/controlclient+
tailscale.com/wgengine/netlog from tailscale.com/wgengine
tailscale.com/wgengine/netstack from tailscale.com/cmd/tailscaled
tailscale.com/wgengine/router from tailscale.com/ipn/ipnlocal+
@@ -350,13 +340,13 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
golang.org/x/crypto/poly1305 from github.com/tailscale/golang-x-crypto/ssh+
golang.org/x/crypto/salsa20/salsa from golang.org/x/crypto/nacl/box+
LD golang.org/x/crypto/ssh from tailscale.com/ssh/tailssh+
golang.org/x/exp/constraints from golang.org/x/exp/slices+
golang.org/x/exp/constraints from golang.org/x/exp/slices
golang.org/x/exp/maps from tailscale.com/wgengine
golang.org/x/exp/slices from tailscale.com/ipn/ipnlocal+
golang.org/x/net/bpf from github.com/mdlayher/genetlink+
golang.org/x/net/dns/dnsmessage from net+
golang.org/x/net/http/httpguts from golang.org/x/net/http2+
golang.org/x/net/http/httpproxy from net/http+
golang.org/x/net/http/httpproxy from net/http
golang.org/x/net/http2 from golang.org/x/net/http2/h2c+
golang.org/x/net/http2/h2c from tailscale.com/ipn/ipnlocal
golang.org/x/net/http2/hpack from golang.org/x/net/http2+
@@ -420,7 +410,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
encoding/xml from github.com/tailscale/goupnp+
errors from bufio+
expvar from tailscale.com/derp+
flag from net/http/httptest+
flag from tailscale.com/control/controlclient+
fmt from compress/flate+
hash from crypto+
hash/adler32 from tailscale.com/ipn/ipnlocal

View File

@@ -6,3 +6,4 @@ package main
//go:generate go run tailscale.com/cmd/mkmanifest amd64 windows-manifest.xml manifest_windows_amd64.syso
//go:generate go run tailscale.com/cmd/mkmanifest 386 windows-manifest.xml manifest_windows_386.syso
//go:generate go run tailscale.com/cmd/mkmanifest arm64 windows-manifest.xml manifest_windows_arm64.syso
//go:generate go run tailscale.com/cmd/mkmanifest arm windows-manifest.xml manifest_windows_arm.syso

Binary file not shown.

View File

@@ -39,27 +39,25 @@ import (
"tailscale.com/logtail"
"tailscale.com/net/dns"
"tailscale.com/net/dnsfallback"
"tailscale.com/net/netmon"
"tailscale.com/net/netns"
"tailscale.com/net/proxymux"
"tailscale.com/net/socks5"
"tailscale.com/net/tsdial"
"tailscale.com/net/tshttpproxy"
"tailscale.com/net/tstun"
"tailscale.com/paths"
"tailscale.com/safesocket"
"tailscale.com/smallzstd"
"tailscale.com/syncs"
"tailscale.com/tsweb/varz"
"tailscale.com/tsweb"
"tailscale.com/types/flagtype"
"tailscale.com/types/logger"
"tailscale.com/types/logid"
"tailscale.com/util/clientmetric"
"tailscale.com/util/multierr"
"tailscale.com/util/osshare"
"tailscale.com/version"
"tailscale.com/version/distro"
"tailscale.com/wgengine"
"tailscale.com/wgengine/monitor"
"tailscale.com/wgengine/netstack"
"tailscale.com/wgengine/router"
)
@@ -329,15 +327,7 @@ var logPol *logpolicy.Policy
var debugMux *http.ServeMux
func run() error {
var logf logger.Logf = log.Printf
netMon, err := netmon.New(func(format string, args ...any) {
logf(format, args...)
})
if err != nil {
return fmt.Errorf("netmon.New: %w", err)
}
pol := logpolicy.New(logtail.CollectionNode, netMon)
pol := logpolicy.New(logtail.CollectionNode)
pol.SetVerbosityLevel(args.verbose)
logPol = pol
defer func() {
@@ -361,6 +351,7 @@ func run() error {
return nil
}
var logf logger.Logf = log.Printf
if envknob.Bool("TS_DEBUG_MEMORY") {
logf = logger.RusagePrefixLog(logf)
}
@@ -386,10 +377,11 @@ func run() error {
debugMux = newDebugMux()
}
return startIPNServer(context.Background(), logf, pol.PublicID, netMon)
logid := pol.PublicID.String()
return startIPNServer(context.Background(), logf, logid)
}
func startIPNServer(ctx context.Context, logf logger.Logf, logID logid.PublicID, netMon *netmon.Monitor) error {
func startIPNServer(ctx context.Context, logf logger.Logf, logid string) error {
ln, err := safesocket.Listen(args.socketpath)
if err != nil {
return fmt.Errorf("safesocket.Listen: %v", err)
@@ -415,7 +407,7 @@ func startIPNServer(ctx context.Context, logf logger.Logf, logID logid.PublicID,
}
}()
srv := ipnserver.New(logf, logID, netMon)
srv := ipnserver.New(logf, logid)
if debugMux != nil {
debugMux.HandleFunc("/debug/ipn", srv.ServeHTMLStatus)
}
@@ -433,7 +425,7 @@ func startIPNServer(ctx context.Context, logf logger.Logf, logID logid.PublicID,
return
}
}
lb, err := getLocalBackend(ctx, logf, logID, netMon)
lb, err := getLocalBackend(ctx, logf, logid)
if err == nil {
logf("got LocalBackend in %v", time.Since(t0).Round(time.Millisecond))
srv.SetLocalBackend(lb)
@@ -457,15 +449,19 @@ func startIPNServer(ctx context.Context, logf logger.Logf, logID logid.PublicID,
return nil
}
func getLocalBackend(ctx context.Context, logf logger.Logf, logID logid.PublicID, netMon *netmon.Monitor) (_ *ipnlocal.LocalBackend, retErr error) {
func getLocalBackend(ctx context.Context, logf logger.Logf, logid string) (_ *ipnlocal.LocalBackend, retErr error) {
linkMon, err := monitor.New(logf)
if err != nil {
return nil, fmt.Errorf("monitor.New: %w", err)
}
if logPol != nil {
logPol.Logtail.SetNetMon(netMon)
logPol.Logtail.SetLinkMonitor(linkMon)
}
socksListener, httpProxyListener := mustStartProxyListeners(args.socksAddr, args.httpProxyAddr)
dialer := &tsdial.Dialer{Logf: logf} // mutated below (before used)
e, onlyNetstack, err := createEngine(logf, netMon, dialer)
e, onlyNetstack, err := createEngine(logf, linkMon, dialer)
if err != nil {
return nil, fmt.Errorf("createEngine: %w", err)
}
@@ -498,13 +494,11 @@ func getLocalBackend(ctx context.Context, logf logger.Logf, logID logid.PublicID
}
}
if socksListener != nil || httpProxyListener != nil {
var addrs []string
if httpProxyListener != nil {
hs := &http.Server{Handler: httpProxyHandler(dialer.UserDial)}
go func() {
log.Fatalf("HTTP proxy exited: %v", hs.Serve(httpProxyListener))
}()
addrs = append(addrs, httpProxyListener.Addr().String())
}
if socksListener != nil {
ss := &socks5.Server{
@@ -514,9 +508,7 @@ func getLocalBackend(ctx context.Context, logf logger.Logf, logID logid.PublicID
go func() {
log.Fatalf("SOCKS5 server exited: %v", ss.Serve(socksListener))
}()
addrs = append(addrs, socksListener.Addr().String())
}
tshttpproxy.SetSelfProxy(addrs...)
}
e = wgengine.NewWatchdog(e)
@@ -528,7 +520,7 @@ func getLocalBackend(ctx context.Context, logf logger.Logf, logID logid.PublicID
return nil, fmt.Errorf("store.New: %w", err)
}
lb, err := ipnlocal.NewLocalBackend(logf, logID, store, dialer, e, opts.LoginFlags)
lb, err := ipnlocal.NewLocalBackend(logf, logid, store, dialer, e, opts.LoginFlags)
if err != nil {
return nil, fmt.Errorf("ipnlocal.NewLocalBackend: %w", err)
}
@@ -537,7 +529,7 @@ func getLocalBackend(ctx context.Context, logf logger.Logf, logID logid.PublicID
lb.SetLogFlusher(logPol.Logtail.StartFlush)
}
if root := lb.TailscaleVarRoot(); root != "" {
dnsfallback.SetCachePath(filepath.Join(root, "derpmap.cached.json"), logf)
dnsfallback.SetCachePath(filepath.Join(root, "derpmap.cached.json"))
}
lb.SetDecompressor(func() (controlclient.Decompressor, error) {
return smallzstd.NewDecoder(nil)
@@ -554,14 +546,14 @@ func getLocalBackend(ctx context.Context, logf logger.Logf, logID logid.PublicID
//
// onlyNetstack is true if the user has explicitly requested that we use netstack
// for all networking.
func createEngine(logf logger.Logf, netMon *netmon.Monitor, dialer *tsdial.Dialer) (e wgengine.Engine, onlyNetstack bool, err error) {
func createEngine(logf logger.Logf, linkMon *monitor.Mon, dialer *tsdial.Dialer) (e wgengine.Engine, onlyNetstack bool, err error) {
if args.tunname == "" {
return nil, false, errors.New("no --tun value specified")
}
var errs []error
for _, name := range strings.Split(args.tunname, ",") {
logf("wgengine.NewUserspaceEngine(tun %q) ...", name)
e, onlyNetstack, err = tryEngine(logf, netMon, dialer, name)
e, onlyNetstack, err = tryEngine(logf, linkMon, dialer, name)
if err == nil {
return e, onlyNetstack, nil
}
@@ -593,11 +585,11 @@ func handleSubnetsInNetstack() bool {
var tstunNew = tstun.New
func tryEngine(logf logger.Logf, netMon *netmon.Monitor, dialer *tsdial.Dialer, name string) (e wgengine.Engine, onlyNetstack bool, err error) {
func tryEngine(logf logger.Logf, linkMon *monitor.Mon, dialer *tsdial.Dialer, name string) (e wgengine.Engine, onlyNetstack bool, err error) {
conf := wgengine.Config{
ListenPort: args.port,
NetMon: netMon,
Dialer: dialer,
ListenPort: args.port,
LinkMonitor: linkMon,
Dialer: dialer,
}
onlyNetstack = name == "userspace-networking"
@@ -636,7 +628,7 @@ func tryEngine(logf logger.Logf, netMon *netmon.Monitor, dialer *tsdial.Dialer,
return e, false, err
}
r, err := router.New(logf, dev, netMon)
r, err := router.New(logf, dev, linkMon)
if err != nil {
dev.Close()
return nil, false, fmt.Errorf("creating router: %w", err)
@@ -673,7 +665,7 @@ func newDebugMux() *http.ServeMux {
func servePrometheusMetrics(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain")
varz.Handler(w, r)
tsweb.VarzHandler(w, r)
clientmetric.WritePrometheusExpositionFormat(w)
}

View File

@@ -45,10 +45,8 @@ import (
"tailscale.com/logpolicy"
"tailscale.com/logtail/backoff"
"tailscale.com/net/dns"
"tailscale.com/net/netmon"
"tailscale.com/net/tstun"
"tailscale.com/types/logger"
"tailscale.com/types/logid"
"tailscale.com/util/winutil"
"tailscale.com/version"
"tailscale.com/wf"
@@ -264,13 +262,13 @@ func beWindowsSubprocess() bool {
if len(os.Args) != 3 || os.Args[1] != "/subproc" {
return false
}
logID := os.Args[2]
logid := os.Args[2]
// Remove the date/time prefix; the logtail + file loggers add it.
log.SetFlags(0)
log.Printf("Program starting: v%v: %#v", version.Long(), os.Args)
log.Printf("subproc mode: logid=%v", logID)
log.Printf("subproc mode: logid=%v", logid)
if err := envknob.ApplyDiskConfigError(); err != nil {
log.Printf("Error reading environment config: %v", err)
}
@@ -292,13 +290,7 @@ func beWindowsSubprocess() bool {
}
}()
netMon, err := netmon.New(log.Printf)
if err != nil {
log.Printf("Could not create netMon: %v", err)
netMon = nil
}
publicLogID, _ := logid.ParsePublicID(logID)
err = startIPNServer(ctx, log.Printf, publicLogID, netMon)
err := startIPNServer(ctx, log.Printf, logid)
if err != nil {
log.Fatalf("ipnserver: %v", err)
}

View File

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

View File

@@ -38,31 +38,13 @@ class App extends Component<{}, AppState> {
if (ipnState === "NeedsMachineAuth") {
machineAuthInstructions = (
<div class="container mx-auto px-4 text-center">
An administrator needs to approve this device.
</div>
)
}
const lockedOut = netMap?.lockedOut
let lockedOutInstructions
if (lockedOut) {
lockedOutInstructions = (
<div class="container mx-auto px-4 text-center space-y-4">
<p>This instance of Tailscale Connect needs to be signed, due to
{" "}<a href="https://tailscale.com/kb/1226/tailnet-lock/" class="link">tailnet lock</a>{" "}
being enabled on this domain.
</p>
<p>
Run the following command on a device with a trusted tailnet lock key:
<pre>tailscale lock sign {netMap.self.nodeKey}</pre>
</p>
An administrator needs to authorize this device.
</div>
)
}
let ssh
if (ipn && ipnState === "Running" && netMap && !lockedOut) {
if (ipn && ipnState === "Running" && netMap) {
ssh = <SSH netMap={netMap} ipn={ipn} />
}
@@ -73,7 +55,6 @@ class App extends Component<{}, AppState> {
<div class="flex-grow flex flex-col justify-center overflow-hidden">
{urlDisplay}
{machineAuthInstructions}
{lockedOutInstructions}
{ssh}
</div>
</>

View File

@@ -30,7 +30,7 @@ const STATE_LABELS = {
NoState: "Initializing…",
InUseOtherUser: "In-use by another user",
NeedsLogin: "Needs login",
NeedsMachineAuth: "Needs approval",
NeedsMachineAuth: "Needs authorization",
Stopped: "Stopped",
Starting: "Starting…",
Running: "Running",

View File

@@ -60,11 +60,11 @@ function SSHSession({
function NoSSHPeers() {
return (
<div class="container mx-auto px-4 text-center">
None of your machines have{" "}
None of your machines have
<a href="https://tailscale.com/kb/1193/tailscale-ssh/" class="link">
Tailscale SSH
</a>
{" "}enabled. Give it a try!
enabled. Give it a try!
</div>
)
}

View File

@@ -63,7 +63,6 @@ declare global {
type IPNNetMap = {
self: IPNNetMapSelfNode
peers: IPNNetMapPeerNode[]
lockedOut: boolean
}
type IPNNetMapNode = {

View File

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

View File

@@ -46,7 +46,7 @@ import (
var ControlURL = ipn.DefaultControlURL
func main() {
js.Global().Set("newIPN", js.FuncOf(func(this js.Value, args []js.Value) any {
js.Global().Set("newIPN", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
if len(args) != 1 {
log.Fatal("Usage: newIPN(config)")
return nil
@@ -122,8 +122,8 @@ func newIPN(jsConfig js.Value) map[string]any {
return ns.DialContextTCP(ctx, dst)
}
logid := lpc.PublicID
srv := ipnserver.New(logf, logid, nil /* no netMon */)
logid := lpc.PublicID.String()
srv := ipnserver.New(logf, logid)
lb, err := ipnlocal.NewLocalBackend(logf, logid, store, dialer, eng, controlclient.LoginEphemeral)
if err != nil {
log.Fatalf("ipnlocal.NewLocalBackend: %v", err)
@@ -146,7 +146,7 @@ func newIPN(jsConfig js.Value) map[string]any {
}
return map[string]any{
"run": js.FuncOf(func(this js.Value, args []js.Value) any {
"run": js.FuncOf(func(this js.Value, args []js.Value) interface{} {
if len(args) != 1 {
log.Fatal(`Usage: run({
notifyState(state: int): void,
@@ -159,7 +159,7 @@ func newIPN(jsConfig js.Value) map[string]any {
jsIPN.run(args[0])
return nil
}),
"login": js.FuncOf(func(this js.Value, args []js.Value) any {
"login": js.FuncOf(func(this js.Value, args []js.Value) interface{} {
if len(args) != 0 {
log.Printf("Usage: login()")
return nil
@@ -167,7 +167,7 @@ func newIPN(jsConfig js.Value) map[string]any {
jsIPN.login()
return nil
}),
"logout": js.FuncOf(func(this js.Value, args []js.Value) any {
"logout": js.FuncOf(func(this js.Value, args []js.Value) interface{} {
if len(args) != 0 {
log.Printf("Usage: logout()")
return nil
@@ -175,7 +175,7 @@ func newIPN(jsConfig js.Value) map[string]any {
jsIPN.logout()
return nil
}),
"ssh": js.FuncOf(func(this js.Value, args []js.Value) any {
"ssh": js.FuncOf(func(this js.Value, args []js.Value) interface{} {
if len(args) != 3 {
log.Printf("Usage: ssh(hostname, userName, termConfig)")
return nil
@@ -185,7 +185,7 @@ func newIPN(jsConfig js.Value) map[string]any {
args[1].String(),
args[2])
}),
"fetch": js.FuncOf(func(this js.Value, args []js.Value) any {
"fetch": js.FuncOf(func(this js.Value, args []js.Value) interface{} {
if len(args) != 1 {
log.Printf("Usage: fetch(url)")
return nil
@@ -272,7 +272,6 @@ func (i *jsIPN) run(jsCallbacks js.Value) {
TailscaleSSHEnabled: p.Hostinfo.TailscaleSSHEnabled(),
}
}),
LockedOut: nm.TKAEnabled && len(nm.SelfNode.KeySignature) == 0,
}
if jsonNetMap, err := json.Marshal(jsNetMap); err == nil {
jsCallbacks.Call("notifyNetMap", string(jsonNetMap))
@@ -334,10 +333,10 @@ func (i *jsIPN) ssh(host, username string, termConfig js.Value) map[string]any {
go jsSSHSession.Run()
return map[string]any{
"close": js.FuncOf(func(this js.Value, args []js.Value) any {
"close": js.FuncOf(func(this js.Value, args []js.Value) interface{} {
return jsSSHSession.Close() != nil
}),
"resize": js.FuncOf(func(this js.Value, args []js.Value) any {
"resize": js.FuncOf(func(this js.Value, args []js.Value) interface{} {
rows := args[0].Int()
cols := args[1].Int()
return jsSSHSession.Resize(rows, cols) != nil
@@ -426,7 +425,7 @@ func (s *jsSSHSession) Run() {
session.Stdout = termWriter{writeFn}
session.Stderr = termWriter{writeFn}
setReadFn.Invoke(js.FuncOf(func(this js.Value, args []js.Value) any {
setReadFn.Invoke(js.FuncOf(func(this js.Value, args []js.Value) interface{} {
input := args[0].String()
_, err := stdin.Write([]byte(input))
if err != nil {
@@ -496,7 +495,7 @@ func (i *jsIPN) fetch(url string) js.Value {
return map[string]any{
"status": res.StatusCode,
"statusText": res.Status,
"text": js.FuncOf(func(this js.Value, args []js.Value) any {
"text": js.FuncOf(func(this js.Value, args []js.Value) interface{} {
return makePromise(func() (any, error) {
defer res.Body.Close()
buf := new(bytes.Buffer)
@@ -522,9 +521,8 @@ func (w termWriter) Write(p []byte) (n int, err error) {
}
type jsNetMap struct {
Self jsNetMapSelfNode `json:"self"`
Peers []jsNetMapPeerNode `json:"peers"`
LockedOut bool `json:"lockedOut"`
Self jsNetMapSelfNode `json:"self"`
Peers []jsNetMapPeerNode `json:"peers"`
}
type jsNetMapNode struct {
@@ -602,7 +600,7 @@ func generateHostname() string {
// f is run on a goroutine and its return value is used to resolve the promise
// (or reject it if an error is returned).
func makePromise(f func() (any, error)) js.Value {
handler := js.FuncOf(func(this js.Value, args []js.Value) any {
handler := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
resolve := args[0]
reject := args[1]
go func() {

View File

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

View File

@@ -13,7 +13,6 @@ import (
"tailscale.com/health"
"tailscale.com/logtail/backoff"
"tailscale.com/net/sockstats"
"tailscale.com/tailcfg"
"tailscale.com/types/empty"
"tailscale.com/types/key"
@@ -59,17 +58,15 @@ type Auto struct {
mu sync.Mutex // mutex guards the following fields
paused bool // whether we should stop making HTTP requests
unpauseWaiters []chan struct{}
loggedIn bool // true if currently logged in
loginGoal *LoginGoal // non-nil if some login activity is desired
synced bool // true if our netmap is up-to-date
inPollNetMap bool // true if currently running a PollNetMap
inLiteMapUpdate bool // true if a lite (non-streaming) map request is outstanding
liteMapUpdateCancel context.CancelFunc // cancels a lite map update, may be nil
liteMapUpdateCancels int // how many times we've canceled a lite map update
inSendStatus int // number of sendStatus calls currently in progress
state State
paused bool // whether we should stop making HTTP requests
unpauseWaiters []chan struct{}
loggedIn bool // true if currently logged in
loginGoal *LoginGoal // non-nil if some login activity is desired
synced bool // true if our netmap is up-to-date
inPollNetMap bool // true if currently running a PollNetMap
inLiteMapUpdate bool // true if a lite (non-streaming) map request is outstanding
inSendStatus int // number of sendStatus calls currently in progress
state State
authCtx context.Context // context used for auth requests
mapCtx context.Context // context used for netmap requests
@@ -121,11 +118,7 @@ func NewNoStart(opts Options) (_ *Auto, err error) {
statusFunc: opts.Status,
}
c.authCtx, c.authCancel = context.WithCancel(context.Background())
c.authCtx = sockstats.WithSockStats(c.authCtx, sockstats.LabelControlClientAuto, opts.Logf)
c.mapCtx, c.mapCancel = context.WithCancel(context.Background())
c.mapCtx = sockstats.WithSockStats(c.mapCtx, sockstats.LabelControlClientAuto, opts.Logf)
c.unregisterHealthWatch = health.RegisterWatcher(direct.ReportHealthChange)
return c, nil
@@ -170,56 +163,28 @@ func (c *Auto) Start() {
func (c *Auto) sendNewMapRequest() {
c.mu.Lock()
// If we're not already streaming a netmap, then tear down everything
// and start a new stream (which starts by sending a new map request)
if !c.inPollNetMap || !c.loggedIn {
// If we're not already streaming a netmap, or if we're already stuck
// in a lite update, then tear down everything and start a new stream
// (which starts by sending a new map request)
if !c.inPollNetMap || c.inLiteMapUpdate || !c.loggedIn {
c.mu.Unlock()
c.cancelMapSafely()
return
}
// If we are already in process of doing a LiteMapUpdate, cancel it and
// try a new one. If this is the 10th time we have done this
// cancelation, tear down everything and start again.
const maxLiteMapUpdateAttempts = 10
if c.inLiteMapUpdate {
// Always cancel the in-flight lite map update, regardless of
// whether we cancel the streaming map request or not.
c.liteMapUpdateCancel()
c.inLiteMapUpdate = false
if c.liteMapUpdateCancels >= maxLiteMapUpdateAttempts {
// Not making progress
c.mu.Unlock()
c.cancelMapSafely()
return
}
// Increment our cancel counter and continue below to start a
// new lite update.
c.liteMapUpdateCancels++
}
// Otherwise, send a lite update that doesn't keep a
// long-running stream response.
defer c.mu.Unlock()
c.inLiteMapUpdate = true
ctx, cancel := context.WithTimeout(c.mapCtx, 10*time.Second)
c.liteMapUpdateCancel = cancel
go func() {
defer cancel()
t0 := time.Now()
err := c.direct.SendLiteMapUpdate(ctx)
d := time.Since(t0).Round(time.Millisecond)
c.mu.Lock()
c.inLiteMapUpdate = false
c.liteMapUpdateCancel = nil
if err == nil {
c.liteMapUpdateCancels = 0
}
c.mu.Unlock()
if err == nil {
c.logf("[v1] successful lite map update in %v", d)
return
@@ -227,13 +192,10 @@ func (c *Auto) sendNewMapRequest() {
if ctx.Err() == nil {
c.logf("lite map update after %v: %v", d, err)
}
if !errors.Is(ctx.Err(), context.Canceled) {
// Fall back to restarting the long-polling map
// request (the old heavy way) if the lite update
// failed for reasons other than the context being
// canceled.
c.cancelMapSafely()
}
// Fall back to restarting the long-polling map
// request (the old heavy way) if the lite update
// failed for any reason.
c.cancelMapSafely()
}()
}
@@ -244,7 +206,6 @@ func (c *Auto) cancelAuth() {
}
if !c.closed {
c.authCtx, c.authCancel = context.WithCancel(context.Background())
c.authCtx = sockstats.WithSockStats(c.authCtx, sockstats.LabelControlClientAuto, c.logf)
}
c.mu.Unlock()
}
@@ -255,8 +216,6 @@ func (c *Auto) cancelMapLocked() {
}
if !c.closed {
c.mapCtx, c.mapCancel = context.WithCancel(context.Background())
c.mapCtx = sockstats.WithSockStats(c.mapCtx, sockstats.LabelControlClientAuto, c.logf)
}
}
@@ -270,12 +229,6 @@ func (c *Auto) cancelMapSafely() {
c.mu.Lock()
defer c.mu.Unlock()
// Always reset our lite map cancels counter if we're canceling
// everything, since we're about to restart with a new map update; this
// allows future calls to sendNewMapRequest to retry sending lite
// updates.
c.liteMapUpdateCancels = 0
c.logf("[v1] cancelMapSafely: synced=%v", c.synced)
if c.inPollNetMap {
@@ -407,13 +360,7 @@ func (c *Auto) authRoutine() {
c.mu.Unlock()
c.sendStatus("authRoutine-url", err, url, nil)
if goal.url == url {
// The server sent us the same URL we already tried,
// backoff to avoid a busy loop.
bo.BackOff(ctx, errors.New("login URL not changing"))
} else {
bo.BackOff(ctx, nil)
}
bo.BackOff(ctx, err)
continue
}

View File

@@ -7,11 +7,10 @@ import (
"bufio"
"bytes"
"context"
"crypto/ed25519"
"encoding/base64"
"encoding/binary"
"encoding/json"
"errors"
"flag"
"fmt"
"io"
"log"
@@ -37,7 +36,6 @@ import (
"tailscale.com/net/dnscache"
"tailscale.com/net/dnsfallback"
"tailscale.com/net/interfaces"
"tailscale.com/net/netmon"
"tailscale.com/net/netutil"
"tailscale.com/net/tlsdial"
"tailscale.com/net/tsdial"
@@ -55,6 +53,7 @@ import (
"tailscale.com/util/multierr"
"tailscale.com/util/singleflight"
"tailscale.com/util/systemd"
"tailscale.com/wgengine/monitor"
)
// Direct is the client that connects to a tailcontrol server for a node.
@@ -67,7 +66,7 @@ type Direct struct {
newDecompressor func() (Decompressor, error)
keepAlive bool
logf logger.Logf
netMon *netmon.Monitor // or nil
linkMon *monitor.Mon // or nil
discoPubKey key.DiscoPublic
getMachinePrivKey func() (key.MachinePrivate, error)
debugFlags []string
@@ -88,15 +87,16 @@ type Direct struct {
sfGroup singleflight.Group[struct{}, *NoiseClient] // protects noiseClient creation.
noiseClient *NoiseClient
persist persist.PersistView
authKey string
tryingNewKey key.NodePrivate
expiry *time.Time
hostinfo *tailcfg.Hostinfo // always non-nil
netinfo *tailcfg.NetInfo
endpoints []tailcfg.Endpoint
tkaHead string
lastPingURL string // last PingRequest.URL received, for dup suppression
persist persist.PersistView
authKey string
tryingNewKey key.NodePrivate
expiry *time.Time
hostinfo *tailcfg.Hostinfo // always non-nil
netinfo *tailcfg.NetInfo
endpoints []tailcfg.Endpoint
tkaHead string
everEndpoints bool // whether we've ever had non-empty endpoints
lastPingURL string // last PingRequest.URL received, for dup suppression
}
type Options struct {
@@ -113,7 +113,7 @@ type Options struct {
HTTPTestClient *http.Client // optional HTTP client to use (for tests only)
NoiseTestClient *http.Client // optional HTTP client to use for noise RPCs (tests only)
DebugFlags []string // debug settings to send to control
NetMon *netmon.Monitor // optional network monitor
LinkMonitor *monitor.Mon // optional link monitor
PopBrowserURL func(url string) // optional func to open browser
OnClientVersion func(*tailcfg.ClientVersion) // optional func to inform GUI of client version status
OnControlTime func(time.Time) // optional func to notify callers of new time from control
@@ -211,9 +211,7 @@ func NewDirect(opts Options) (*Direct, error) {
dnsCache := &dnscache.Resolver{
Forward: dnscache.Get().Forward, // use default cache's forwarder
UseLastGood: true,
LookupIPFallback: dnsfallback.MakeLookupFunc(opts.Logf, opts.NetMon),
Logf: opts.Logf,
NetMon: opts.NetMon,
LookupIPFallback: dnsfallback.Lookup,
}
tr := http.DefaultTransport.(*http.Transport).Clone()
tr.Proxy = tshttpproxy.ProxyFromEnvironment
@@ -242,7 +240,7 @@ func NewDirect(opts Options) (*Direct, error) {
discoPubKey: opts.DiscoPublicKey,
debugFlags: opts.DebugFlags,
keepSharerAndUserSplit: opts.KeepSharerAndUserSplit,
netMon: opts.NetMon,
linkMon: opts.LinkMonitor,
skipIPForwardingCheck: opts.SkipIPForwardingCheck,
pinger: opts.Pinger,
popBrowser: opts.PopBrowserURL,
@@ -426,7 +424,7 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
tryingNewKey := c.tryingNewKey
serverKey := c.serverKey
serverNoiseKey := c.serverNoiseKey
authKey, isWrapped, wrappedSig, wrappedKey := decodeWrappedAuthkey(c.authKey, c.logf)
authKey := c.authKey
hi := c.hostInfoLocked()
backendLogID := hi.BackendLogID
expired := c.expiry != nil && !c.expiry.IsZero() && c.expiry.Before(c.timeNow())
@@ -512,22 +510,6 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
if nodeKeySignature, err = resignNKS(persist.NetworkLockKey, tryingNewKey.Public(), opt.OldNodeKeySignature); err != nil {
c.logf("Failed re-signing node-key signature: %v", err)
}
} else if isWrapped {
// We were given a wrapped pre-auth key, which means that in addition
// to being a regular pre-auth key there was a suffix with information to
// generate a tailnet-lock signature.
nk, err := tryingNewKey.Public().MarshalBinary()
if err != nil {
return false, "", nil, fmt.Errorf("marshalling node-key: %w", err)
}
sig := &tka.NodeKeySignature{
SigKind: tka.SigRotation,
Pubkey: nk,
Nested: wrappedSig,
}
sigHash := sig.SigHash()
sig.Signature = ed25519.Sign(wrappedKey, sigHash[:])
nodeKeySignature = sig.Serialize()
}
if backendLogID == "" {
@@ -753,6 +735,9 @@ func (c *Direct) newEndpoints(endpoints []tailcfg.Endpoint) (changed bool) {
}
c.logf("[v2] client.newEndpoints(%v)", epStrs)
c.endpoints = append(c.endpoints[:0], endpoints...)
if len(endpoints) > 0 {
c.everEndpoints = true
}
return true // changed
}
@@ -765,6 +750,8 @@ func (c *Direct) SetEndpoints(endpoints []tailcfg.Endpoint) (changed bool) {
return c.newEndpoints(endpoints)
}
func inTest() bool { return flag.Lookup("test.v") != nil }
// PollNetMap makes a /map request to download the network map, calling cb with
// each new netmap.
func (c *Direct) PollNetMap(ctx context.Context, cb func(*netmap.NetworkMap)) error {
@@ -819,6 +806,7 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, readOnly bool
epStrs = append(epStrs, ep.Addr.String())
epTypes = append(epTypes, ep.Type)
}
everEndpoints := c.everEndpoints
c.mu.Unlock()
machinePrivKey, err := c.getMachinePrivKey()
@@ -859,21 +847,19 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, readOnly bool
OmitPeers: cb == nil,
TKAHead: c.tkaHead,
// Previously we'd set ReadOnly to true if we didn't have any endpoints
// yet as we expected to learn them in a half second and restart the full
// streaming map poll, however as we are trying to reduce the number of
// times we restart the full streaming map poll we now just set ReadOnly
// false when we're doing a full streaming map poll.
//
// TODO(maisem/bradfitz): really ReadOnly should be set to true if for
// all streams and we should only do writes via lite map updates.
// However that requires an audit and a bunch of testing to make sure we
// don't break anything.
ReadOnly: readOnly && !allowStream,
// On initial startup before we know our endpoints, set the ReadOnly flag
// to tell the control server not to distribute out our (empty) endpoints to peers.
// Presumably we'll learn our endpoints in a half second and do another post
// with useful results. The first POST just gets us the DERP map which we
// need to do the STUN queries to discover our endpoints.
// TODO(bradfitz): we skip this optimization in tests, though,
// because the e2e tests are currently hyper-specific about the
// ordering of things. The e2e tests need love.
ReadOnly: readOnly || (len(epStrs) == 0 && !everEndpoints && !inTest()),
}
var extraDebugFlags []string
if hi != nil && c.netMon != nil && !c.skipIPForwardingCheck &&
ipForwardingBroken(hi.RoutableIPs, c.netMon.InterfaceState()) {
if hi != nil && c.linkMon != nil && !c.skipIPForwardingCheck &&
ipForwardingBroken(hi.RoutableIPs, c.linkMon.InterfaceState()) {
extraDebugFlags = append(extraDebugFlags, "warn-ip-forwarding-off")
}
if health.RouterHealth() != nil {
@@ -1509,7 +1495,7 @@ func (c *Direct) getNoiseClient() (*NoiseClient, error) {
return nil, err
}
c.logf("creating new noise client")
nc, err := NewNoiseClient(k, serverNoiseKey, c.serverURL, c.dialer, c.logf, c.netMon, dp)
nc, err := NewNoiseClient(k, serverNoiseKey, c.serverURL, c.dialer, dp)
if err != nil {
return nil, err
}
@@ -1727,43 +1713,6 @@ func (c *Direct) ReportHealthChange(sys health.Subsystem, sysErr error) {
res.Body.Close()
}
// decodeWrappedAuthkey separates wrapping information from an authkey, if any.
// In all cases the authkey is returned, sans wrapping information if any.
//
// If the authkey is wrapped, isWrapped returns true, along with the wrapping signature
// and private key.
func decodeWrappedAuthkey(key string, logf logger.Logf) (authKey string, isWrapped bool, sig *tka.NodeKeySignature, priv ed25519.PrivateKey) {
authKey, suffix, found := strings.Cut(key, "--TL")
if !found {
return key, false, nil, nil
}
sigBytes, privBytes, found := strings.Cut(suffix, "-")
if !found {
logf("decoding wrapped auth-key: did not find delimiter")
return key, false, nil, nil
}
rawSig, err := base64.RawStdEncoding.DecodeString(sigBytes)
if err != nil {
logf("decoding wrapped auth-key: signature decode: %v", err)
return key, false, nil, nil
}
rawPriv, err := base64.RawStdEncoding.DecodeString(privBytes)
if err != nil {
logf("decoding wrapped auth-key: priv decode: %v", err)
return key, false, nil, nil
}
sig = new(tka.NodeKeySignature)
if err := sig.Unserialize([]byte(rawSig)); err != nil {
logf("decoding wrapped auth-key: signature: %v", err)
return key, false, nil, nil
}
priv = ed25519.PrivateKey(rawPriv)
return authKey, true, sig, priv
}
var (
metricMapRequestsActive = clientmetric.NewGauge("controlclient_map_requests_active")

View File

@@ -4,7 +4,6 @@
package controlclient
import (
"crypto/ed25519"
"encoding/json"
"net/http"
"net/http/httptest"
@@ -143,42 +142,3 @@ func TestTsmpPing(t *testing.T) {
t.Fatal(err)
}
}
func TestDecodeWrappedAuthkey(t *testing.T) {
k, isWrapped, sig, priv := decodeWrappedAuthkey("tskey-32mjsdkdsffds9o87dsfkjlh", nil)
if want := "tskey-32mjsdkdsffds9o87dsfkjlh"; k != want {
t.Errorf("decodeWrappedAuthkey(<unwrapped-key>).key = %q, want %q", k, want)
}
if isWrapped {
t.Error("decodeWrappedAuthkey(<unwrapped-key>).isWrapped = true, want false")
}
if sig != nil {
t.Errorf("decodeWrappedAuthkey(<unwrapped-key>).sig = %v, want nil", sig)
}
if priv != nil {
t.Errorf("decodeWrappedAuthkey(<unwrapped-key>).priv = %v, want nil", priv)
}
k, isWrapped, sig, priv = decodeWrappedAuthkey("tskey-auth-k7UagY1CNTRL-ZZZZZ--TLpAEDA1ggnXuw4/fWnNWUwcoOjLemhOvml1juMl5lhLmY5sBUsj8EWEAfL2gdeD9g8VDw5tgcxCiHGlEb67BgU2DlFzZApi4LheLJraA+pYjTGChVhpZz1iyiBPD+U2qxDQAbM3+WFY0EBlggxmVqG53Hu0Rg+KmHJFMlUhfgzo+AQP6+Kk9GzvJJOs4-k36RdoSFqaoARfQo0UncHAV0t3YTqrkD5r/z2jTrE43GZWobnce7RGD4qYckUyVSF+DOj4BA/r4qT0bO8kk6zg", nil)
if want := "tskey-auth-k7UagY1CNTRL-ZZZZZ"; k != want {
t.Errorf("decodeWrappedAuthkey(<wrapped-key>).key = %q, want %q", k, want)
}
if !isWrapped {
t.Error("decodeWrappedAuthkey(<wrapped-key>).isWrapped = false, want true")
}
if sig == nil {
t.Fatal("decodeWrappedAuthkey(<wrapped-key>).sig = nil, want non-nil signature")
}
sigHash := sig.SigHash()
if !ed25519.Verify(sig.KeyID, sigHash[:], sig.Signature) {
t.Error("signature failed to verify")
}
// Make sure the private is correct by using it.
someSig := ed25519.Sign(priv, []byte{1, 2, 3, 4})
if !ed25519.Verify(sig.WrappingPubkey, []byte{1, 2, 3, 4}, someSig) {
t.Error("failed to use priv")
}
}

View File

@@ -13,7 +13,6 @@ import (
"go4.org/mem"
"tailscale.com/tailcfg"
"tailscale.com/tstest"
"tailscale.com/types/key"
"tailscale.com/types/netmap"
"tailscale.com/types/opt"
@@ -22,11 +21,12 @@ import (
)
func TestUndeltaPeers(t *testing.T) {
var curTime time.Time
tstest.Replace(t, &clockNow, func() time.Time {
return curTime
})
defer func(old func() time.Time) { clockNow = old }(clockNow)
var curTime time.Time
clockNow = func() time.Time {
return curTime
}
online := func(v bool) func(*tailcfg.Node) {
return func(n *tailcfg.Node) {
n.Online = &v

View File

@@ -19,11 +19,9 @@ import (
"golang.org/x/net/http2"
"tailscale.com/control/controlbase"
"tailscale.com/control/controlhttp"
"tailscale.com/net/netmon"
"tailscale.com/net/tsdial"
"tailscale.com/tailcfg"
"tailscale.com/types/key"
"tailscale.com/types/logger"
"tailscale.com/util/mak"
"tailscale.com/util/multierr"
"tailscale.com/util/singleflight"
@@ -169,9 +167,6 @@ type NoiseClient struct {
// be nil.
dialPlan func() *tailcfg.ControlDialPlan
logf logger.Logf
netMon *netmon.Monitor
// mu only protects the following variables.
mu sync.Mutex
last *noiseConn // or nil
@@ -182,9 +177,8 @@ type NoiseClient struct {
// 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(privKey key.MachinePrivate, serverPubKey key.MachinePublic, serverURL string, dialer *tsdial.Dialer, logf logger.Logf, netMon *netmon.Monitor, dialPlan func() *tailcfg.ControlDialPlan) (*NoiseClient, error) {
func NewNoiseClient(privKey key.MachinePrivate, serverPubKey key.MachinePublic, serverURL string, dialer *tsdial.Dialer, dialPlan func() *tailcfg.ControlDialPlan) (*NoiseClient, error) {
u, err := url.Parse(serverURL)
if err != nil {
return nil, err
@@ -213,8 +207,6 @@ func NewNoiseClient(privKey key.MachinePrivate, serverPubKey key.MachinePublic,
httpsPort: httpsPort,
dialer: dialer,
dialPlan: dialPlan,
logf: logf,
netMon: netMon,
}
// Create the HTTP/2 Transport using a net/http.Transport
@@ -374,8 +366,6 @@ func (nc *NoiseClient) dial() (*noiseConn, error) {
ProtocolVersion: uint16(tailcfg.CurrentCapabilityVersion),
Dialer: nc.dialer.SystemDial,
DialPlan: dialPlan,
Logf: nc.logf,
NetMon: nc.netMon,
}).Dial(ctx)
if err != nil {
return nil, err

View File

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

View File

@@ -41,7 +41,6 @@ import (
"tailscale.com/net/dnscache"
"tailscale.com/net/dnsfallback"
"tailscale.com/net/netutil"
"tailscale.com/net/sockstats"
"tailscale.com/net/tlsdial"
"tailscale.com/net/tshttpproxy"
"tailscale.com/tailcfg"
@@ -273,8 +272,6 @@ func (a *Dialer) dialHost(ctx context.Context, addr netip.Addr) (*ClientConn, er
ctx, cancel := context.WithCancel(ctx)
defer cancel()
ctx = sockstats.WithSockStats(ctx, sockstats.LabelControlClientDialer, a.logf)
// u80 and u443 are the URLs we'll try to hit over HTTP or HTTPS,
// respectively, in order to do the HTTP upgrade to a net.Conn over which
// we'll speak Noise.
@@ -388,16 +385,12 @@ func (a *Dialer) tryURLUpgrade(ctx context.Context, u *url.URL, addr netip.Addr,
dns = &dnscache.Resolver{
SingleHostStaticResult: []netip.Addr{addr},
SingleHost: u.Hostname(),
Logf: a.Logf, // not a.logf method; we want to propagate nil-ness
NetMon: a.NetMon,
}
} else {
dns = &dnscache.Resolver{
Forward: dnscache.Get().Forward,
LookupIPFallback: dnsfallback.MakeLookupFunc(a.logf, a.NetMon),
LookupIPFallback: dnsfallback.Lookup,
UseLastGood: true,
Logf: a.Logf, // not a.logf method; we want to propagate nil-ness
NetMon: a.NetMon,
}
}

View File

@@ -9,7 +9,6 @@ import (
"time"
"tailscale.com/net/dnscache"
"tailscale.com/net/netmon"
"tailscale.com/tailcfg"
"tailscale.com/types/key"
"tailscale.com/types/logger"
@@ -71,8 +70,6 @@ type Dialer struct {
// dropped.
Logf logger.Logf
NetMon *netmon.Monitor
// DialPlan, if set, contains instructions from the control server on
// how to connect to it. If present, we will try the methods in this
// plan before falling back to DNS.

View File

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

View File

@@ -348,12 +348,9 @@ type ReceivedPacket struct {
func (ReceivedPacket) msg() {}
// PeerGoneMessage is a ReceivedMessage that indicates that the client
// identified by the underlying public key is not connected to this
// server.
type PeerGoneMessage struct {
Peer key.NodePublic
Reason PeerGoneReasonType
}
// identified by the underlying public key had previously sent you a
// packet but has now disconnected from the server.
type PeerGoneMessage key.NodePublic
func (PeerGoneMessage) msg() {}
@@ -527,15 +524,7 @@ func (c *Client) recvTimeout(timeout time.Duration) (m ReceivedMessage, err erro
c.logf("[unexpected] dropping short peerGone frame from DERP server")
continue
}
// Backward compatibility for the older peerGone without reason byte
reason := PeerGoneReasonDisconnected
if n > keyLen {
reason = PeerGoneReasonType(b[keyLen])
}
pg := PeerGoneMessage{
Peer: key.NodePublicFromRaw32(mem.B(b[:keyLen])),
Reason: reason,
}
pg := PeerGoneMessage(key.NodePublicFromRaw32(mem.B(b[:keyLen])))
return pg, nil
case framePeerPresent:

View File

@@ -34,12 +34,12 @@ import (
"go4.org/mem"
"golang.org/x/sync/errgroup"
"golang.org/x/time/rate"
"tailscale.com/client/tailscale"
"tailscale.com/disco"
"tailscale.com/envknob"
"tailscale.com/metrics"
"tailscale.com/syncs"
"tailscale.com/tstime/rate"
"tailscale.com/types/key"
"tailscale.com/types/logger"
"tailscale.com/version"
@@ -122,8 +122,7 @@ type Server struct {
_ align64
packetsForwardedOut expvar.Int
packetsForwardedIn expvar.Int
peerGoneDisconnectedFrames expvar.Int // number of peer disconnected frames sent
peerGoneNotHereFrames expvar.Int // number of peer not here frames sent
peerGoneFrames expvar.Int // number of peer gone frames sent
gotPing expvar.Int // number of ping frames from client
sentPong expvar.Int // number of pong frames enqueued to client
accepts expvar.Int
@@ -280,7 +279,6 @@ func (s *dupClientSet) removeClient(c *sclient) bool {
// public key gets more than one PacketForwarder registered for it.
type PacketForwarder interface {
ForwardPacket(src, dst key.NodePublic, payload []byte) error
String() string
}
// Conn is the subset of the underlying net.Conn the DERP Server needs.
@@ -325,8 +323,7 @@ func NewServer(privateKey key.NodePrivate, logf logger.Logf) *Server {
s.packetsDroppedReasonCounters = []*expvar.Int{
s.packetsDroppedReason.Get("unknown_dest"),
s.packetsDroppedReason.Get("unknown_dest_on_fwd"),
s.packetsDroppedReason.Get("gone_disconnected"),
s.packetsDroppedReason.Get("gone_not_here"),
s.packetsDroppedReason.Get("gone"),
s.packetsDroppedReason.Get("queue_head"),
s.packetsDroppedReason.Get("queue_tail"),
s.packetsDroppedReason.Get("write_error"),
@@ -498,7 +495,6 @@ func (s *Server) registerClient(c *sclient) {
switch set := set.(type) {
case nil:
s.clients[c.key] = singleClient{c}
c.debug("register single client")
case singleClient:
s.dupClientKeys.Add(1)
s.dupClientConns.Add(2) // both old and new count
@@ -514,7 +510,6 @@ func (s *Server) registerClient(c *sclient) {
},
sendHistory: []*sclient{old},
}
c.debug("register duplicate client")
case *dupClientSet:
s.dupClientConns.Add(1) // the gauge
s.dupClientConnTotal.Add(1) // the counter
@@ -522,7 +517,6 @@ func (s *Server) registerClient(c *sclient) {
set.set[c] = true
set.last = c
set.sendHistory = append(set.sendHistory, c)
c.debug("register another duplicate client")
}
if _, ok := s.clientsMesh[c.key]; !ok {
@@ -555,7 +549,7 @@ func (s *Server) unregisterClient(c *sclient) {
case nil:
c.logf("[unexpected]; clients map is empty")
case singleClient:
c.logf("removed connection")
c.logf("removing connection")
delete(s.clients, c.key)
if v, ok := s.clientsMesh[c.key]; ok && v == nil {
delete(s.clientsMesh, c.key)
@@ -563,7 +557,6 @@ func (s *Server) unregisterClient(c *sclient) {
}
s.broadcastPeerStateChangeLocked(c.key, false)
case *dupClientSet:
c.debug("removed duplicate client")
if set.removeClient(c) {
s.dupClientConns.Add(-1)
} else {
@@ -617,26 +610,13 @@ func (s *Server) notePeerGoneFromRegionLocked(key key.NodePublic) {
}
set.ForeachClient(func(peer *sclient) {
if peer.connNum == connNum {
go peer.requestPeerGoneWrite(key, PeerGoneReasonDisconnected)
go peer.requestPeerGoneWrite(key)
}
})
}
delete(s.sentTo, key)
}
// requestPeerGoneWriteLimited sends a request to write a "peer gone"
// frame, but only in reply to a disco packet, and only if we haven't
// sent one recently.
func (c *sclient) requestPeerGoneWriteLimited(peer key.NodePublic, contents []byte, reason PeerGoneReasonType) {
if disco.LooksLikeDiscoWrapper(contents) != true {
return
}
if c.peerGoneLim.Allow() {
go c.requestPeerGoneWrite(peer, reason)
}
}
func (s *Server) addWatcher(c *sclient) {
if !c.canMesh {
panic("invariant: addWatcher called without permissions")
@@ -693,7 +673,7 @@ func (s *Server) accept(ctx context.Context, nc Conn, brw *bufio.ReadWriter, rem
nc: nc,
br: br,
bw: bw,
logf: logger.WithPrefix(s.logf, fmt.Sprintf("derp client %v%s: ", remoteAddr, clientKey.ShortString())),
logf: logger.WithPrefix(s.logf, fmt.Sprintf("derp client %v/%x: ", remoteAddr, clientKey)),
done: ctx.Done(),
remoteAddr: remoteAddr,
remoteIPPort: remoteIPPort,
@@ -701,9 +681,8 @@ func (s *Server) accept(ctx context.Context, nc Conn, brw *bufio.ReadWriter, rem
sendQueue: make(chan pkt, perClientSendQueueDepth),
discoSendQueue: make(chan pkt, perClientSendQueueDepth),
sendPongCh: make(chan [8]byte, 1),
peerGone: make(chan peerGoneMsg),
peerGone: make(chan key.NodePublic),
canMesh: clientInfo.MeshKey != "" && clientInfo.MeshKey == s.meshKey,
peerGoneLim: rate.NewLimiter(rate.Every(time.Second), 3),
}
if c.canMesh {
@@ -711,9 +690,6 @@ func (s *Server) accept(ctx context.Context, nc Conn, brw *bufio.ReadWriter, rem
}
if clientInfo != nil {
c.info = *clientInfo
if envknob.Bool("DERP_PROBER_DEBUG_LOGS") && clientInfo.IsProber {
c.debugLogging = true
}
}
s.registerClient(c)
@@ -750,7 +726,6 @@ func (c *sclient) run(ctx context.Context) error {
for {
ft, fl, err := readFrameHeader(c.br)
c.debug("read frame type %d len %d err %v", ft, fl, err)
if err != nil {
if errors.Is(err, io.EOF) {
c.logf("read EOF")
@@ -760,7 +735,7 @@ func (c *sclient) run(ctx context.Context) error {
c.logf("closing; server closed")
return nil
}
return fmt.Errorf("client %s: readFrameHeader: %w", c.key.ShortString(), err)
return fmt.Errorf("client %x: readFrameHeader: %w", c.key, err)
}
c.s.noteClientActivity(c)
switch ft {
@@ -903,15 +878,11 @@ func (c *sclient) handleFrameForwardPacket(ft frameType, fl uint32) error {
reason := dropReasonUnknownDestOnFwd
if dstLen > 1 {
reason = dropReasonDupClient
} else {
c.requestPeerGoneWriteLimited(dstKey, contents, PeerGoneReasonNotHere)
}
s.recordDrop(contents, srcKey, dstKey, reason)
return nil
}
dst.debug("received forwarded packet from %s via %s", srcKey.ShortString(), c.key.ShortString())
return c.sendPkt(dst, pkt{
bs: contents,
enqueuedAt: time.Now(),
@@ -959,9 +930,7 @@ func (c *sclient) handleFrameSendPacket(ft frameType, fl uint32) error {
if dst == nil {
if fwd != nil {
s.packetsForwardedOut.Add(1)
err := fwd.ForwardPacket(c.key, dstKey, contents)
c.debug("SendPacket for %s, forwarding via %s: %v", dstKey.ShortString(), fwd, err)
if err != nil {
if err := fwd.ForwardPacket(c.key, dstKey, contents); err != nil {
// TODO:
return nil
}
@@ -970,14 +939,10 @@ func (c *sclient) handleFrameSendPacket(ft frameType, fl uint32) error {
reason := dropReasonUnknownDest
if dstLen > 1 {
reason = dropReasonDupClient
} else {
c.requestPeerGoneWriteLimited(dstKey, contents, PeerGoneReasonNotHere)
}
s.recordDrop(contents, c.key, dstKey, reason)
c.debug("SendPacket for %s, dropping with reason=%s", dstKey.ShortString(), reason)
return nil
}
c.debug("SendPacket for %s, sending directly", dstKey.ShortString())
p := pkt{
bs: contents,
@@ -987,12 +952,6 @@ func (c *sclient) handleFrameSendPacket(ft frameType, fl uint32) error {
return c.sendPkt(dst, p)
}
func (c *sclient) debug(format string, v ...any) {
if c.debugLogging {
c.logf(format, v...)
}
}
// dropReason is why we dropped a DERP frame.
type dropReason int
@@ -1001,7 +960,7 @@ type dropReason int
const (
dropReasonUnknownDest dropReason = iota // unknown destination pubkey
dropReasonUnknownDestOnFwd // unknown destination pubkey on a derp-forwarded packet
dropReasonGoneDisconnected // destination tailscaled disconnected before we could send
dropReasonGone // destination tailscaled disconnected before we could send
dropReasonQueueHead // destination queue is full, dropped packet at queue head
dropReasonQueueTail // destination queue is full, dropped packet at queue tail
dropReasonWriteError // OS write() failed
@@ -1043,14 +1002,12 @@ func (c *sclient) sendPkt(dst *sclient, p pkt) error {
for attempt := 0; attempt < 3; attempt++ {
select {
case <-dst.done:
s.recordDrop(p.bs, c.key, dstKey, dropReasonGoneDisconnected)
dst.debug("sendPkt attempt %d dropped, dst gone", attempt)
s.recordDrop(p.bs, c.key, dstKey, dropReasonGone)
return nil
default:
}
select {
case sendQueue <- p:
dst.debug("sendPkt attempt %d enqueued", attempt)
return nil
default:
}
@@ -1066,20 +1023,16 @@ func (c *sclient) sendPkt(dst *sclient, p pkt) error {
// contended queue with racing writers. Give up and tail-drop in
// this case to keep reader unblocked.
s.recordDrop(p.bs, c.key, dstKey, dropReasonQueueTail)
dst.debug("sendPkt attempt %d dropped, queue full")
return nil
}
// requestPeerGoneWrite sends a request to write a "peer gone" frame
// with an explanation of why it is gone. It blocks until either the
// that the provided peer has disconnected. It blocks until either the
// write request is scheduled, or the client has closed.
func (c *sclient) requestPeerGoneWrite(peer key.NodePublic, reason PeerGoneReasonType) {
func (c *sclient) requestPeerGoneWrite(peer key.NodePublic) {
select {
case c.peerGone <- peerGoneMsg{
peer: peer,
reason: reason,
}:
case c.peerGone <- peer:
case <-c.done:
}
}
@@ -1293,19 +1246,22 @@ type sclient struct {
key key.NodePublic
info clientInfo
logf logger.Logf
done <-chan struct{} // closed when connection closes
remoteAddr string // usually ip:port from net.Conn.RemoteAddr().String()
remoteIPPort netip.AddrPort // zero if remoteAddr is not ip:port.
sendQueue chan pkt // packets queued to this client; never closed
discoSendQueue chan pkt // important packets queued to this client; never closed
sendPongCh chan [8]byte // pong replies to send to the client; never closed
peerGone chan peerGoneMsg // write request that a peer is not at this server (not used by mesh peers)
meshUpdate chan struct{} // write request to write peerStateChange
canMesh bool // clientInfo had correct mesh token for inter-region routing
isDup atomic.Bool // whether more than 1 sclient for key is connected
isDisabled atomic.Bool // whether sends to this peer are disabled due to active/active dups
done <-chan struct{} // closed when connection closes
remoteAddr string // usually ip:port from net.Conn.RemoteAddr().String()
remoteIPPort netip.AddrPort // zero if remoteAddr is not ip:port.
sendQueue chan pkt // packets queued to this client; never closed
discoSendQueue chan pkt // important packets queued to this client; never closed
sendPongCh chan [8]byte // pong replies to send to the client; never closed
peerGone chan key.NodePublic // write request that a previous sender has disconnected (not used by mesh peers)
meshUpdate chan struct{} // write request to write peerStateChange
canMesh bool // clientInfo had correct mesh token for inter-region routing
isDup atomic.Bool // whether more than 1 sclient for key is connected
isDisabled atomic.Bool // whether sends to this peer are disabled due to active/active dups
debugLogging bool
// replaceLimiter controls how quickly two connections with
// the same client key can kick each other off the server by
// taking over ownership of a key.
replaceLimiter *rate.Limiter
// Owned by run, not thread-safe.
br *bufio.Reader
@@ -1322,11 +1278,6 @@ type sclient struct {
// the client for them to update their map of who's connected
// to this node.
peerStateChange []peerConnState
// peerGoneLimiter limits how often the server will inform a
// client that it's trying to establish a direct connection
// through us with a peer we have no record of.
peerGoneLim *rate.Limiter
}
// peerConnState represents whether a peer is connected to the server
@@ -1350,12 +1301,6 @@ type pkt struct {
bs []byte
}
// peerGoneMsg is a request to write a peerGone frame to an sclient
type peerGoneMsg struct {
peer key.NodePublic
reason PeerGoneReasonType
}
func (c *sclient) setPreferred(v bool) {
if c.preferred == v {
return
@@ -1410,9 +1355,9 @@ func (c *sclient) sendLoop(ctx context.Context) error {
for {
select {
case pkt := <-c.sendQueue:
c.s.recordDrop(pkt.bs, pkt.src, c.key, dropReasonGoneDisconnected)
c.s.recordDrop(pkt.bs, pkt.src, c.key, dropReasonGone)
case pkt := <-c.discoSendQueue:
c.s.recordDrop(pkt.bs, pkt.src, c.key, dropReasonGoneDisconnected)
c.s.recordDrop(pkt.bs, pkt.src, c.key, dropReasonGone)
default:
return
}
@@ -1433,8 +1378,8 @@ func (c *sclient) sendLoop(ctx context.Context) error {
select {
case <-ctx.Done():
return nil
case msg := <-c.peerGone:
werr = c.sendPeerGone(msg.peer, msg.reason)
case peer := <-c.peerGone:
werr = c.sendPeerGone(peer)
continue
case <-c.meshUpdate:
werr = c.sendMeshUpdates()
@@ -1465,8 +1410,8 @@ func (c *sclient) sendLoop(ctx context.Context) error {
select {
case <-ctx.Done():
return nil
case msg := <-c.peerGone:
werr = c.sendPeerGone(msg.peer, msg.reason)
case peer := <-c.peerGone:
werr = c.sendPeerGone(peer)
case <-c.meshUpdate:
werr = c.sendMeshUpdates()
continue
@@ -1507,22 +1452,13 @@ func (c *sclient) sendPong(data [8]byte) error {
}
// sendPeerGone sends a peerGone frame, without flushing.
func (c *sclient) sendPeerGone(peer key.NodePublic, reason PeerGoneReasonType) error {
switch reason {
case PeerGoneReasonDisconnected:
c.s.peerGoneDisconnectedFrames.Add(1)
case PeerGoneReasonNotHere:
c.s.peerGoneNotHereFrames.Add(1)
}
func (c *sclient) sendPeerGone(peer key.NodePublic) error {
c.s.peerGoneFrames.Add(1)
c.setWriteDeadline()
data := make([]byte, 0, keyLen+1)
data = peer.AppendTo(data)
data = append(data, byte(reason))
if err := writeFrameHeader(c.bw.bw(), framePeerGone, uint32(len(data))); err != nil {
if err := writeFrameHeader(c.bw.bw(), framePeerGone, keyLen); err != nil {
return err
}
_, err := c.bw.Write(data)
_, err := c.bw.Write(peer.AppendTo(nil))
return err
}
@@ -1553,7 +1489,7 @@ func (c *sclient) sendMeshUpdates() error {
if pcs.present {
err = c.sendPeerPresent(pcs.peer)
} else {
err = c.sendPeerGone(pcs.peer, PeerGoneReasonDisconnected)
err = c.sendPeerGone(pcs.peer)
}
if err != nil {
// Shouldn't happen, though, as we're writing
@@ -1593,7 +1529,6 @@ func (c *sclient) sendPacket(srcKey key.NodePublic, contents []byte) (err error)
c.s.packetsSent.Add(1)
c.s.bytesSent.Add(int64(len(contents)))
}
c.debug("sendPacket from %s: %v", srcKey.ShortString(), err)
}()
c.setWriteDeadline()
@@ -1754,10 +1689,6 @@ func (f *multiForwarder) ForwardPacket(src, dst key.NodePublic, payload []byte)
return f.fwd.Load().ForwardPacket(src, dst, payload)
}
func (f *multiForwarder) String() string {
return fmt.Sprintf("<MultiForwarder fwd=%s total=%d>", f.fwd.Load(), len(f.all))
}
func (s *Server) expVarFunc(f func() any) expvar.Func {
return expvar.Func(func() any {
s.mu.Lock()
@@ -1794,8 +1725,7 @@ func (s *Server) ExpVar() expvar.Var {
m.Set("home_moves_out", &s.homeMovesOut)
m.Set("got_ping", &s.gotPing)
m.Set("sent_pong", &s.sentPong)
m.Set("peer_gone_disconnected_frames", &s.peerGoneDisconnectedFrames)
m.Set("peer_gone_not_here_frames", &s.peerGoneNotHereFrames)
m.Set("peer_gone_frames", &s.peerGoneFrames)
m.Set("packets_forwarded_out", &s.packetsForwardedOut)
m.Set("packets_forwarded_in", &s.packetsForwardedIn)
m.Set("multiforwarder_created", &s.multiForwarderCreated)

View File

@@ -25,7 +25,6 @@ import (
"go4.org/mem"
"golang.org/x/time/rate"
"tailscale.com/disco"
"tailscale.com/net/memnet"
"tailscale.com/types/key"
"tailscale.com/types/logger"
@@ -106,8 +105,7 @@ func TestSendRecv(t *testing.T) {
t.Logf("Connected client %d.", i)
}
var peerGoneCountDisconnected expvar.Int
var peerGoneCountNotHere expvar.Int
var peerGoneCount expvar.Int
t.Logf("Starting read loops")
for i := 0; i < numClients; i++ {
@@ -123,14 +121,7 @@ func TestSendRecv(t *testing.T) {
t.Errorf("unexpected message type %T", m)
continue
case PeerGoneMessage:
switch m.Reason {
case PeerGoneReasonDisconnected:
peerGoneCountDisconnected.Add(1)
case PeerGoneReasonNotHere:
peerGoneCountNotHere.Add(1)
default:
t.Errorf("unexpected PeerGone reason %v", m.Reason)
}
peerGoneCount.Add(1)
case ReceivedPacket:
if m.Source.IsZero() {
t.Errorf("zero Source address in ReceivedPacket")
@@ -180,19 +171,7 @@ func TestSendRecv(t *testing.T) {
var got int64
dl := time.Now().Add(5 * time.Second)
for time.Now().Before(dl) {
if got = peerGoneCountDisconnected.Value(); got == want {
return
}
}
t.Errorf("peer gone count = %v; want %v", got, want)
}
wantUnknownPeers := func(want int64) {
t.Helper()
var got int64
dl := time.Now().Add(5 * time.Second)
for time.Now().Before(dl) {
if got = peerGoneCountNotHere.Value(); got == want {
if got = peerGoneCount.Value(); got == want {
return
}
}
@@ -215,30 +194,6 @@ func TestSendRecv(t *testing.T) {
recvNothing(0)
recvNothing(1)
// Send messages to a non-existent node
neKey := key.NewNode().Public()
msg4 := []byte("not a CallMeMaybe->unknown destination\n")
if err := clients[1].Send(neKey, msg4); err != nil {
t.Fatal(err)
}
wantUnknownPeers(0)
callMe := neKey.AppendTo([]byte(disco.Magic))
callMeHeader := make([]byte, disco.NonceLen)
callMe = append(callMe, callMeHeader...)
if err := clients[1].Send(neKey, callMe); err != nil {
t.Fatal(err)
}
wantUnknownPeers(1)
// PeerGoneNotHere is rate-limited to 3 times a second
for i := 0; i < 5; i++ {
if err := clients[1].Send(neKey, callMe); err != nil {
t.Fatal(err)
}
}
wantUnknownPeers(3)
wantActive(3, 0)
clients[0].NotePreferred(true)
wantActive(3, 1)
@@ -640,14 +595,10 @@ func (tc *testClient) wantGone(t *testing.T, peer key.NodePublic) {
}
switch m := m.(type) {
case PeerGoneMessage:
got := key.NodePublic(m.Peer)
got := key.NodePublic(m)
if peer != got {
t.Errorf("got gone message for %v; want gone for %v", tc.ts.keyName(got), tc.ts.keyName(peer))
}
reason := m.Reason
if reason != PeerGoneReasonDisconnected {
t.Errorf("got gone message for reason %v; wanted %v", reason, PeerGoneReasonDisconnected)
}
default:
t.Fatalf("unexpected message type %T", m)
}
@@ -709,9 +660,6 @@ type testFwd int
func (testFwd) ForwardPacket(key.NodePublic, key.NodePublic, []byte) error {
panic("not called in tests")
}
func (testFwd) String() string {
panic("not called in tests")
}
func pubAll(b byte) (ret key.NodePublic) {
var bs [32]byte
@@ -839,7 +787,6 @@ type channelFwd struct {
c chan []byte
}
func (f channelFwd) String() string { return "" }
func (f channelFwd) ForwardPacket(_ key.NodePublic, _ key.NodePublic, packet []byte) error {
f.c <- packet
return nil

View File

@@ -31,9 +31,7 @@ import (
"tailscale.com/derp"
"tailscale.com/envknob"
"tailscale.com/net/dnscache"
"tailscale.com/net/netmon"
"tailscale.com/net/netns"
"tailscale.com/net/sockstats"
"tailscale.com/net/tlsdial"
"tailscale.com/net/tshttpproxy"
"tailscale.com/syncs"
@@ -56,7 +54,6 @@ type Client struct {
privateKey key.NodePrivate
logf logger.Logf
netMon *netmon.Monitor // optional; nil means interfaces will be looked up on-demand
dialer func(ctx context.Context, network, addr string) (net.Conn, error)
// Either url or getRegion is non-nil:
@@ -84,19 +81,13 @@ type Client struct {
pingOut map[derp.PingMessage]chan<- bool // chan to send to on pong
}
func (c *Client) String() string {
return fmt.Sprintf("<derphttp_client.Client %s url=%s>", c.serverPubKey.ShortString(), c.url)
}
// NewRegionClient returns a new DERP-over-HTTP client. It connects lazily.
// To trigger a connection, use Connect.
// The netMon parameter is optional; if non-nil it's used to do faster interface lookups.
func NewRegionClient(privateKey key.NodePrivate, logf logger.Logf, netMon *netmon.Monitor, getRegion func() *tailcfg.DERPRegion) *Client {
func NewRegionClient(privateKey key.NodePrivate, logf logger.Logf, getRegion func() *tailcfg.DERPRegion) *Client {
ctx, cancel := context.WithCancel(context.Background())
c := &Client{
privateKey: privateKey,
logf: logf,
netMon: netMon,
getRegion: getRegion,
ctx: ctx,
cancelCtx: cancel,
@@ -178,10 +169,6 @@ func urlPort(u *url.URL) string {
return ""
}
// debugDERPUseHTTP tells clients to connect to DERP via HTTP on port
// 3340 instead of HTTPS on 443.
var debugUseDERPHTTP = envknob.RegisterBool("TS_DEBUG_USE_DERP_HTTP")
func (c *Client) targetString(reg *tailcfg.DERPRegion) string {
if c.url != nil {
return c.url.String()
@@ -193,10 +180,6 @@ func (c *Client) useHTTPS() bool {
if c.url != nil && c.url.Scheme == "http" {
return false
}
if debugUseDERPHTTP() {
return false
}
return true
}
@@ -212,11 +195,7 @@ func (c *Client) urlString(node *tailcfg.DERPNode) string {
if c.url != nil {
return c.url.String()
}
proto := "https"
if debugUseDERPHTTP() {
proto = "http"
}
return fmt.Sprintf("%s://%s/derp", proto, node.HostName)
return fmt.Sprintf("https://%s/derp", node.HostName)
}
// AddressFamilySelector decides whether IPv6 is preferred for
@@ -341,7 +320,7 @@ func (c *Client) connect(ctx context.Context, caller string) (client *derp.Clien
}
c.serverPubKey = derpClient.ServerPublicKey()
c.client = derpClient
c.netConn = conn
c.netConn = tcpConn
c.connGen++
return c.client, c.connGen, nil
case c.url != nil:
@@ -496,7 +475,7 @@ func (c *Client) dialURL(ctx context.Context) (net.Conn, error) {
return c.dialer(ctx, "tcp", net.JoinHostPort(host, urlPort(c.url)))
}
hostOrIP := host
dialer := netns.NewDialer(c.logf, c.netMon)
dialer := netns.NewDialer(c.logf)
if c.DNSCache != nil {
ip, _, _, err := c.DNSCache.LookupIP(ctx, host)
@@ -591,7 +570,7 @@ func (c *Client) DialRegionTLS(ctx context.Context, reg *tailcfg.DERPRegion) (tl
}
func (c *Client) dialContext(ctx context.Context, proto, addr string) (net.Conn, error) {
return netns.NewDialer(c.logf, c.netMon).DialContext(ctx, proto, addr)
return netns.NewDialer(c.logf).DialContext(ctx, proto, addr)
}
// shouldDialProto reports whether an explicitly provided IPv4 or IPv6
@@ -636,8 +615,6 @@ func (c *Client) dialNode(ctx context.Context, n *tailcfg.DERPNode) (net.Conn, e
ctx, cancel := context.WithTimeout(ctx, dialNodeTimeout)
defer cancel()
ctx = sockstats.WithSockStats(ctx, sockstats.LabelDERPHTTPClient, c.logf)
nwait := 0
startDial := func(dstPrimary, proto string) {
nwait++

View File

@@ -128,17 +128,7 @@ func (c *Client) RunWatchConnectionLoop(ctx context.Context, ignoreServerKey key
case derp.PeerPresentMessage:
updatePeer(key.NodePublic(m), true)
case derp.PeerGoneMessage:
switch m.Reason {
case derp.PeerGoneReasonDisconnected:
// Normal case, log nothing
case derp.PeerGoneReasonNotHere:
logf("Recv: peer %s not connected to %s",
key.NodePublic(m.Peer).ShortString(), c.ServerPublicKey().ShortString())
default:
logf("Recv: peer %s not at server %s for unknown reason %v",
key.NodePublic(m.Peer).ShortString(), c.ServerPublicKey().ShortString(), m.Reason)
}
updatePeer(key.NodePublic(m.Peer), false)
updatePeer(key.NodePublic(m), false)
default:
continue
}

View File

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

View File

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

View File

@@ -1,23 +0,0 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build darwin || freebsd || openbsd
package permissions
import (
"golang.org/x/sys/unix"
"tailscale.com/types/logger"
)
func permissionsImpl(logf logger.Logf) error {
groups, _ := unix.Getgroups()
logf("uid=%s euid=%s gid=%s egid=%s groups=%s",
formatUserID(unix.Getuid()),
formatUserID(unix.Geteuid()),
formatGroupID(unix.Getgid()),
formatGroupID(unix.Getegid()),
formatGroups(groups),
)
return nil
}

View File

@@ -1,62 +0,0 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build linux
package permissions
import (
"fmt"
"strings"
"unsafe"
"golang.org/x/sys/unix"
"tailscale.com/types/logger"
)
func permissionsImpl(logf logger.Logf) error {
// NOTE: getresuid and getresgid never fail unless passed an
// invalid address.
var ruid, euid, suid uint64
unix.Syscall(unix.SYS_GETRESUID,
uintptr(unsafe.Pointer(&ruid)),
uintptr(unsafe.Pointer(&euid)),
uintptr(unsafe.Pointer(&suid)),
)
var rgid, egid, sgid uint64
unix.Syscall(unix.SYS_GETRESGID,
uintptr(unsafe.Pointer(&rgid)),
uintptr(unsafe.Pointer(&egid)),
uintptr(unsafe.Pointer(&sgid)),
)
groups, _ := unix.Getgroups()
var buf strings.Builder
fmt.Fprintf(&buf, "ruid=%s euid=%s suid=%s rgid=%s egid=%s sgid=%s groups=%s",
formatUserID(ruid), formatUserID(euid), formatUserID(suid),
formatGroupID(rgid), formatGroupID(egid), formatGroupID(sgid),
formatGroups(groups),
)
// Get process capabilities
var (
capHeader = unix.CapUserHeader{
Version: unix.LINUX_CAPABILITY_VERSION_3,
Pid: 0, // 0 means 'ourselves'
}
capData unix.CapUserData
)
if err := unix.Capget(&capHeader, &capData); err != nil {
fmt.Fprintf(&buf, " caperr=%v", err)
} else {
fmt.Fprintf(&buf, " cap_effective=%08x cap_permitted=%08x cap_inheritable=%08x",
capData.Effective, capData.Permitted, capData.Inheritable,
)
}
logf("%s", buf.String())
return nil
}

View File

@@ -1,17 +0,0 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
//go:build !(linux || darwin || freebsd || openbsd)
package permissions
import (
"runtime"
"tailscale.com/types/logger"
)
func permissionsImpl(logf logger.Logf) error {
logf("unsupported on %s/%s", runtime.GOOS, runtime.GOARCH)
return nil
}

View File

@@ -1,12 +0,0 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package permissions
import "testing"
func TestPermissionsImpl(t *testing.T) {
if err := permissionsImpl(t.Logf); err != nil {
t.Error(err)
}
}

View File

@@ -42,7 +42,6 @@ var (
regBool = map[string]*bool{}
regOptBool = map[string]*opt.Bool{}
regDuration = map[string]*time.Duration{}
regInt = map[string]*int{}
)
func noteEnv(k, v string) {
@@ -183,25 +182,6 @@ func RegisterDuration(envVar string) func() time.Duration {
return func() time.Duration { return *p }
}
// RegisterInt returns a func that gets the named environment variable as an
// integer, without a map lookup per call. It assumes that any mutations happen
// via envknob.Setenv.
func RegisterInt(envVar string) func() int {
mu.Lock()
defer mu.Unlock()
p, ok := regInt[envVar]
if !ok {
val := os.Getenv(envVar)
if val != "" {
noteEnvLocked(envVar, val)
}
p = new(int)
setIntLocked(p, envVar, val)
regInt[envVar] = p
}
return func() int { return *p }
}
func setBoolLocked(p *bool, envVar, val string) {
noteEnvLocked(envVar, val)
if val == "" {
@@ -241,19 +221,6 @@ func setDurationLocked(p *time.Duration, envVar, val string) {
}
}
func setIntLocked(p *int, envVar, val string) {
noteEnvLocked(envVar, val)
if val == "" {
*p = 0
return
}
var err error
*p, err = strconv.Atoi(val)
if err != nil {
log.Fatalf("invalid int environment variable %s value %q", envVar, val)
}
}
// Bool returns the boolean value of the named environment variable.
// If the variable is not set, it returns false.
// An invalid value exits the binary with a failure.
@@ -330,46 +297,6 @@ func LookupInt(envVar string) (v int, ok bool) {
panic("unreachable")
}
// LookupIntSized returns the integer value of the named environment value
// parsed in base and with a maximum bit size bitSize.
// The ok result is whether a value was set.
// If the value isn't a valid int, it exits the program with a failure.
func LookupIntSized(envVar string, base, bitSize int) (v int, ok bool) {
assertNotInInit()
val := os.Getenv(envVar)
if val == "" {
return 0, false
}
i, err := strconv.ParseInt(val, base, bitSize)
if err == nil {
v = int(i)
noteEnv(envVar, val)
return v, true
}
log.Fatalf("invalid integer environment variable %s: %v", envVar, val)
panic("unreachable")
}
// LookupUintSized returns the unsigned integer value of the named environment
// value parsed in base and with a maximum bit size bitSize.
// The ok result is whether a value was set.
// If the value isn't a valid int, it exits the program with a failure.
func LookupUintSized(envVar string, base, bitSize int) (v uint, ok bool) {
assertNotInInit()
val := os.Getenv(envVar)
if val == "" {
return 0, false
}
i, err := strconv.ParseUint(val, base, bitSize)
if err == nil {
v = uint(i)
noteEnv(envVar, val)
return v, true
}
log.Fatalf("invalid unsigned integer environment variable %s: %v", envVar, val)
panic("unreachable")
}
// UseWIPCode is whether TAILSCALE_USE_WIP_CODE is set to permit use
// of Work-In-Progress code.
func UseWIPCode() bool { return Bool("TAILSCALE_USE_WIP_CODE") }

View File

@@ -1,84 +0,0 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
// Package logknob provides a helpful wrapper that allows enabling logging
// based on either an envknob or other methods of enablement.
package logknob
import (
"sync/atomic"
"golang.org/x/exp/slices"
"tailscale.com/envknob"
"tailscale.com/types/logger"
)
// TODO(andrew-d): should we have a package-global registry of logknobs? It
// would allow us to update from a netmap in a central location, which might be
// reason enough to do it...
// LogKnob allows configuring verbose logging, with multiple ways to enable. It
// supports enabling logging via envknob, via atomic boolean (for use in e.g.
// c2n log level changes), and via capabilities from a NetMap (so users can
// enable logging via the ACL JSON).
type LogKnob struct {
capName string
cap atomic.Bool
env func() bool
manual atomic.Bool
}
// NewLogKnob creates a new LogKnob, with the provided environment variable
// name and/or NetMap capability.
func NewLogKnob(env, cap string) *LogKnob {
if env == "" && cap == "" {
panic("must provide either an environment variable or capability")
}
lk := &LogKnob{
capName: cap,
}
if env != "" {
lk.env = envknob.RegisterBool(env)
} else {
lk.env = func() bool { return false }
}
return lk
}
// Set will cause logs to be printed when called with Set(true). When called
// with Set(false), logs will not be printed due to an earlier call of
// Set(true), but may be printed due to either the envknob and/or capability of
// this LogKnob.
func (lk *LogKnob) Set(v bool) {
lk.manual.Store(v)
}
// NetMap is an interface for the parts of netmap.NetworkMap that we care
// about; we use this rather than a concrete type to avoid a circular
// dependency.
type NetMap interface {
SelfCapabilities() []string
}
// UpdateFromNetMap will enable logging if the SelfNode in the provided NetMap
// contains the capability provided for this LogKnob.
func (lk *LogKnob) UpdateFromNetMap(nm NetMap) {
if lk.capName == "" {
return
}
lk.cap.Store(slices.Contains(nm.SelfCapabilities(), lk.capName))
}
// Do will call log with the provided format and arguments if any of the
// configured methods for enabling logging are true.
func (lk *LogKnob) Do(log logger.Logf, format string, args ...any) {
if lk.shouldLog() {
log(format, args...)
}
}
func (lk *LogKnob) shouldLog() bool {
return lk.manual.Load() || lk.env() || lk.cap.Load()
}

View File

@@ -1,102 +0,0 @@
// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause
package logknob
import (
"bytes"
"fmt"
"testing"
"tailscale.com/envknob"
"tailscale.com/tailcfg"
"tailscale.com/types/netmap"
)
var testKnob = NewLogKnob(
"TS_TEST_LOGKNOB",
"https://tailscale.com/cap/testing",
)
// Static type assertion for our interface type.
var _ NetMap = &netmap.NetworkMap{}
func TestLogKnob(t *testing.T) {
t.Run("Default", func(t *testing.T) {
if testKnob.shouldLog() {
t.Errorf("expected default shouldLog()=false")
}
assertNoLogs(t)
})
t.Run("Manual", func(t *testing.T) {
t.Cleanup(func() { testKnob.Set(false) })
assertNoLogs(t)
testKnob.Set(true)
if !testKnob.shouldLog() {
t.Errorf("expected shouldLog()=true")
}
assertLogs(t)
})
t.Run("Env", func(t *testing.T) {
t.Cleanup(func() {
envknob.Setenv("TS_TEST_LOGKNOB", "")
})
assertNoLogs(t)
if testKnob.shouldLog() {
t.Errorf("expected default shouldLog()=false")
}
envknob.Setenv("TS_TEST_LOGKNOB", "true")
if !testKnob.shouldLog() {
t.Errorf("expected shouldLog()=true")
}
assertLogs(t)
})
t.Run("NetMap", func(t *testing.T) {
t.Cleanup(func() { testKnob.cap.Store(false) })
assertNoLogs(t)
if testKnob.shouldLog() {
t.Errorf("expected default shouldLog()=false")
}
testKnob.UpdateFromNetMap(&netmap.NetworkMap{
SelfNode: &tailcfg.Node{
Capabilities: []string{
"https://tailscale.com/cap/testing",
},
},
})
if !testKnob.shouldLog() {
t.Errorf("expected shouldLog()=true")
}
assertLogs(t)
})
}
func assertLogs(t *testing.T) {
var buf bytes.Buffer
logf := func(format string, args ...any) {
fmt.Fprintf(&buf, format, args...)
}
testKnob.Do(logf, "hello %s", "world")
const want = "hello world"
if got := buf.String(); got != want {
t.Errorf("got %q, want %q", got, want)
}
}
func assertNoLogs(t *testing.T) {
var buf bytes.Buffer
logf := func(format string, args ...any) {
fmt.Fprintf(&buf, format, args...)
}
testKnob.Do(logf, "hello %s", "world")
if got := buf.String(); got != "" {
t.Errorf("expected no logs, but got: %q", got)
}
}

View File

@@ -108,11 +108,10 @@
graphviz
perl
go_1_20
yarn
];
};
};
in
flake-utils.lib.eachDefaultSystem (system: flakeForSystem nixpkgs system);
}
# nix-direnv cache busting line: sha256-lirn07XE3JOS6oiwZBMwxzywkbXHowOJUMWWLrZtccY=
# nix-direnv cache busting line: sha256-zyyqBRFPNPzPYCMgnbnOy5rb3fkn4XEHZlTlJvwqunM=

230
go.mod
View File

@@ -21,14 +21,13 @@ require (
github.com/dblohm7/wingoes v0.0.0-20221124203957-6ac47ab19aa5
github.com/dsnet/try v0.0.3
github.com/evanw/esbuild v0.14.53
github.com/frankban/quicktest v1.14.3
github.com/frankban/quicktest v1.14.0
github.com/fxamacker/cbor/v2 v2.4.0
github.com/go-json-experiment/json v0.0.0-20221017203807-c5ed296b8c92
github.com/go-logr/zapr v1.2.3
github.com/go-ole/go-ole v1.2.6
github.com/godbus/dbus/v5 v5.0.6
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da
github.com/golangci/golangci-lint v1.52.2
github.com/google/go-cmp v0.5.9
github.com/google/go-containerregistry v0.9.0
github.com/google/nftables v0.1.1-0.20230115205135-9aa6fdf5a28c
@@ -43,8 +42,8 @@ require (
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
github.com/klauspost/compress v1.15.4
github.com/kortschak/wol v0.0.0-20200729010619-da482cc4850a
github.com/mattn/go-colorable v0.1.13
github.com/mattn/go-isatty v0.0.17
github.com/mattn/go-colorable v0.1.12
github.com/mattn/go-isatty v0.0.14
github.com/mdlayher/genetlink v1.2.0
github.com/mdlayher/netlink v1.7.1
github.com/mdlayher/sdnotify v1.0.0
@@ -53,8 +52,6 @@ require (
github.com/peterbourgon/ff/v3 v3.1.2
github.com/pkg/errors v0.9.1
github.com/pkg/sftp v1.13.4
github.com/prometheus/client_golang v1.14.0
github.com/prometheus/common v0.41.0
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
github.com/tailscale/certstore v0.1.1-0.20220316223106-78d6e1c49d8d
github.com/tailscale/depaware v0.0.0-20210622194025-720c4b409502
@@ -64,31 +61,30 @@ require (
github.com/tailscale/hujson v0.0.0-20220630195928-54599719472f
github.com/tailscale/mkctr v0.0.0-20220601142259-c0b937af2e89
github.com/tailscale/netlink v1.1.1-0.20211101221916-cabfb018fe85
github.com/tailscale/wireguard-go v0.0.0-20230410165232-af172621b4dd
github.com/tailscale/wireguard-go v0.0.0-20221219190806-4fa124729667
github.com/tc-hib/winres v0.1.6
github.com/tcnksm/go-httpstat v0.2.0
github.com/toqueteos/webbrowser v1.2.0
github.com/u-root/u-root v0.9.1-0.20230109201855-948a78c969ad
github.com/vishvananda/netlink v1.1.1-0.20211118161826-650dca95af54
go.uber.org/zap v1.24.0
go.uber.org/zap v1.21.0
go4.org/mem v0.0.0-20210711025021-927187094b94
go4.org/netipx v0.0.0-20220725152314-7e7bdc8411bf
golang.org/x/crypto v0.6.0
golang.org/x/exp v0.0.0-20221205204356-47842c84f3db
golang.org/x/mod v0.9.0
golang.org/x/net v0.8.0
golang.org/x/oauth2 v0.5.0
golang.org/x/mod v0.7.0
golang.org/x/net v0.7.0
golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5
golang.org/x/sync v0.1.0
golang.org/x/sys v0.6.0
golang.org/x/term v0.6.0
golang.org/x/sys v0.5.0
golang.org/x/term v0.5.0
golang.org/x/time v0.0.0-20220609170525-579cf78fd858
golang.org/x/tools v0.7.0
golang.org/x/tools v0.4.1-0.20221208213631-3f74d914ae6d
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2
golang.zx2c4.com/wireguard/windows v0.5.3
gvisor.dev/gvisor v0.0.0-20230328175328-162ed5ef888d
honnef.co/go/tools v0.4.3
gvisor.dev/gvisor v0.0.0-20221203005347-703fd9b7fbc0
honnef.co/go/tools v0.4.0-0.dev.0.20230130122044-c30b15588105
inet.af/peercred v0.0.0-20210906144145-0893ea02156a
inet.af/tcpproxy v0.0.0-20221017015627-91f861402626
inet.af/wf v0.0.0-20220728202103-50d96caab2f6
k8s.io/api v0.25.0
k8s.io/apimachinery v0.25.0
@@ -100,28 +96,24 @@ require (
)
require (
4d63.com/gocheckcompilerdirectives v1.2.1 // indirect
4d63.com/gochecknoglobals v0.2.1 // indirect
4d63.com/gochecknoglobals v0.1.0 // indirect
filippo.io/edwards25519 v1.0.0-rc.1 // indirect
github.com/Abirdcfly/dupword v0.0.11 // indirect
github.com/Antonboom/errname v0.1.9 // indirect
github.com/Antonboom/nilnil v0.1.3 // indirect
github.com/Antonboom/errname v0.1.5 // indirect
github.com/Antonboom/nilnil v0.1.0 // indirect
github.com/BurntSushi/toml v1.2.1 // indirect
github.com/Djarvur/go-err113 v0.1.0 // indirect
github.com/GaijinEntertainment/go-exhaustruct/v2 v2.3.0 // indirect
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver v1.5.0 // indirect
github.com/Masterminds/semver/v3 v3.1.1 // indirect
github.com/Masterminds/sprig v2.22.0+incompatible // indirect
github.com/OpenPeeDeeP/depguard v1.1.1 // indirect
github.com/OpenPeeDeeP/depguard v1.0.1 // indirect
github.com/ProtonMail/go-crypto v0.0.0-20221026131551-cf6655e29de4 // indirect
github.com/PuerkitoBio/purell v1.1.1 // indirect
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
github.com/acomagu/bufpipe v1.0.3 // indirect
github.com/alexkohler/prealloc v1.0.0 // indirect
github.com/alingse/asasalint v0.0.11 // indirect
github.com/ashanbrown/forbidigo v1.5.1 // indirect
github.com/ashanbrown/makezero v1.1.1 // indirect
github.com/ashanbrown/forbidigo v1.2.0 // indirect
github.com/ashanbrown/makezero v0.0.0-20210520155254-b6261585ddde // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.0.0 // indirect
github.com/aws/aws-sdk-go-v2/credentials v1.6.4 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.8.2 // indirect
@@ -137,38 +129,35 @@ require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/bkielbasa/cyclop v1.2.0 // indirect
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb // indirect
github.com/blizzy78/varnamelen v0.8.0 // indirect
github.com/bombsimon/wsl/v3 v3.4.0 // indirect
github.com/breml/bidichk v0.2.4 // indirect
github.com/breml/errchkjson v0.3.1 // indirect
github.com/blizzy78/varnamelen v0.5.0 // indirect
github.com/bombsimon/wsl/v3 v3.3.0 // indirect
github.com/breml/bidichk v0.2.1 // indirect
github.com/butuzov/ireturn v0.1.1 // indirect
github.com/cavaliercoder/go-cpio v0.0.0-20180626203310-925f9528c45e // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/charithe/durationcheck v0.0.10 // indirect
github.com/chavacava/garif v0.0.0-20230227094218-b8c73b2037b8 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/charithe/durationcheck v0.0.9 // indirect
github.com/chavacava/garif v0.0.0-20210405164556-e8a0a408d6af // indirect
github.com/cloudflare/circl v1.1.0 // indirect
github.com/containerd/stargz-snapshotter/estargz v0.11.4 // indirect
github.com/curioswitch/go-reassign v0.2.0 // indirect
github.com/daixiang0/gci v0.10.1 // indirect
github.com/daixiang0/gci v0.2.9 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/denis-tingaikin/go-header v0.4.3 // indirect
github.com/denis-tingajkin/go-header v0.4.2 // indirect
github.com/docker/cli v20.10.16+incompatible // indirect
github.com/docker/distribution v2.8.1+incompatible // indirect
github.com/docker/docker v20.10.16+incompatible // indirect
github.com/docker/docker-credential-helpers v0.6.4 // indirect
github.com/emicklei/go-restful/v3 v3.8.0 // indirect
github.com/emirpasic/gods v1.12.0 // indirect
github.com/esimonov/ifshort v1.0.4 // indirect
github.com/esimonov/ifshort v1.0.3 // indirect
github.com/ettle/strcase v0.1.1 // indirect
github.com/evanphx/json-patch v4.12.0+incompatible // indirect
github.com/evanphx/json-patch/v5 v5.6.0 // indirect
github.com/fatih/color v1.15.0 // indirect
github.com/fatih/color v1.13.0 // indirect
github.com/fatih/structtag v1.2.0 // indirect
github.com/firefart/nonamedreturns v1.0.4 // indirect
github.com/fsnotify/fsnotify v1.5.4 // indirect
github.com/fzipp/gocyclo v0.6.0 // indirect
github.com/fzipp/gocyclo v0.3.1 // indirect
github.com/gliderlabs/ssh v0.3.3 // indirect
github.com/go-critic/go-critic v0.7.0 // indirect
github.com/go-critic/go-critic v0.6.1 // indirect
github.com/go-git/gcfg v1.5.0 // indirect
github.com/go-git/go-billy/v5 v5.3.1 // indirect
github.com/go-git/go-git/v5 v5.4.2 // indirect
@@ -176,33 +165,34 @@ require (
github.com/go-openapi/jsonpointer v0.19.5 // indirect
github.com/go-openapi/jsonreference v0.19.5 // indirect
github.com/go-openapi/swag v0.19.14 // indirect
github.com/go-toolsmith/astcast v1.1.0 // indirect
github.com/go-toolsmith/astcopy v1.1.0 // indirect
github.com/go-toolsmith/astequal v1.1.0 // indirect
github.com/go-toolsmith/astfmt v1.1.0 // indirect
github.com/go-toolsmith/astp v1.1.0 // indirect
github.com/go-toolsmith/strparse v1.1.0 // indirect
github.com/go-toolsmith/typep v1.1.0 // indirect
github.com/go-xmlfmt/xmlfmt v1.1.2 // indirect
github.com/go-toolsmith/astcast v1.0.0 // indirect
github.com/go-toolsmith/astcopy v1.0.0 // indirect
github.com/go-toolsmith/astequal v1.0.1 // indirect
github.com/go-toolsmith/astfmt v1.0.0 // indirect
github.com/go-toolsmith/astp v1.0.0 // indirect
github.com/go-toolsmith/strparse v1.0.0 // indirect
github.com/go-toolsmith/typep v1.0.2 // indirect
github.com/go-xmlfmt/xmlfmt v0.0.0-20211206191508-7fd73a941850 // indirect
github.com/gobwas/glob v0.2.3 // indirect
github.com/gofrs/flock v0.8.1 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/golangci/check v0.0.0-20180506172741-cfe4005ccda2 // indirect
github.com/golangci/dupl v0.0.0-20180902072040-3e9179ac440a // indirect
github.com/golangci/go-misc v0.0.0-20220329215616-d24fe342adfe // indirect
github.com/golangci/gofmt v0.0.0-20220901101216-f2edd75033f2 // indirect
github.com/golangci/go-misc v0.0.0-20180628070357-927a3d87b613 // indirect
github.com/golangci/gofmt v0.0.0-20190930125516-244bba706f1a // indirect
github.com/golangci/golangci-lint v1.43.0 // indirect
github.com/golangci/lint-1 v0.0.0-20191013205115-297bf364a8e0 // indirect
github.com/golangci/maligned v0.0.0-20180506175553-b1d89398deca // indirect
github.com/golangci/misspell v0.4.0 // indirect
github.com/golangci/revgrep v0.0.0-20220804021717-745bb2f7c2e6 // indirect
github.com/golangci/misspell v0.3.5 // indirect
github.com/golangci/revgrep v0.0.0-20210930125155-c22e5001d4f2 // indirect
github.com/golangci/unconvert v0.0.0-20180507085042-28b1c447d1f4 // indirect
github.com/google/btree v1.0.1 // indirect
github.com/google/gnostic v0.5.7-v3refs // indirect
github.com/google/gofuzz v1.1.0 // indirect
github.com/google/goterm v0.0.0-20200907032337-555d40f16ae2 // indirect
github.com/google/rpmpack v0.0.0-20201206194719-59e495f2b7e1 // indirect
github.com/gordonklaus/ineffassign v0.0.0-20230107090616-13ace0543b28 // indirect
github.com/gordonklaus/ineffassign v0.0.0-20210914165742-4cc7213b9bc8 // indirect
github.com/goreleaser/chglog v0.1.2 // indirect
github.com/goreleaser/fileglob v0.3.1 // indirect
github.com/gostaticanalysis/analysisutil v0.7.1 // indirect
@@ -211,12 +201,10 @@ require (
github.com/gostaticanalysis/nilerr v0.1.1 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/go-version v1.6.0 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/hexops/gotextdiff v1.0.3 // indirect
github.com/huandu/xstrings v1.3.2 // indirect
github.com/imdario/mergo v0.3.12 // indirect
github.com/inconshreveable/mousetrap v1.0.1 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
github.com/jgautheron/goconst v1.5.1 // indirect
github.com/jingyugao/rowserrcheck v1.1.1 // indirect
@@ -224,117 +212,105 @@ require (
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/julz/importas v0.1.0 // indirect
github.com/junk1tm/musttag v0.5.0 // indirect
github.com/julz/importas v0.0.0-20210922140945-27e0a5d4dee2 // indirect
github.com/kevinburke/ssh_config v1.1.0 // indirect
github.com/kisielk/errcheck v1.6.3 // indirect
github.com/kisielk/errcheck v1.6.0 // indirect
github.com/kisielk/gotool v1.0.0 // indirect
github.com/kkHAIKE/contextcheck v1.1.4 // indirect
github.com/kr/fs v0.1.0 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/kr/pretty v0.3.0 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/kulti/thelper v0.6.3 // indirect
github.com/kunwardeep/paralleltest v1.0.6 // indirect
github.com/kyoh86/exportloopref v0.1.11 // indirect
github.com/ldez/gomoddirectives v0.2.3 // indirect
github.com/ldez/tagliatelle v0.4.0 // indirect
github.com/leonklingele/grouper v1.1.1 // indirect
github.com/lufeee/execinquery v1.2.1 // indirect
github.com/magiconair/properties v1.8.6 // indirect
github.com/kulti/thelper v0.4.0 // indirect
github.com/kunwardeep/paralleltest v1.0.3 // indirect
github.com/kyoh86/exportloopref v0.1.8 // indirect
github.com/ldez/gomoddirectives v0.2.2 // indirect
github.com/ldez/tagliatelle v0.2.0 // indirect
github.com/magiconair/properties v1.8.5 // indirect
github.com/mailru/easyjson v0.7.6 // indirect
github.com/maratori/testableexamples v1.0.0 // indirect
github.com/maratori/testpackage v1.1.1 // indirect
github.com/matoous/godox v0.0.0-20230222163458-006bad1f9d26 // indirect
github.com/maratori/testpackage v1.0.1 // indirect
github.com/matoous/godox v0.0.0-20210227103229-6504466cf951 // indirect
github.com/mattn/go-runewidth v0.0.13 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect
github.com/mbilski/exhaustivestruct v1.2.0 // indirect
github.com/mdlayher/socket v0.4.0 // indirect
github.com/mgechev/revive v1.3.1 // indirect
github.com/mgechev/dots v0.0.0-20210922191527-e955255bf517 // indirect
github.com/mgechev/revive v1.1.2 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/mitchellh/mapstructure v1.4.3 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/moricho/tparallel v0.3.1 // indirect
github.com/moricho/tparallel v0.2.1 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/nakabonne/nestif v0.3.1 // indirect
github.com/nbutton23/zxcvbn-go v0.0.0-20210217022336-fa2cb2858354 // indirect
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect
github.com/nishanths/exhaustive v0.9.5 // indirect
github.com/nishanths/predeclared v0.2.2 // indirect
github.com/nunnatsa/ginkgolinter v0.9.0 // indirect
github.com/nishanths/exhaustive v0.7.11 // indirect
github.com/nishanths/predeclared v0.2.1 // indirect
github.com/olekukonko/tablewriter v0.0.5 // indirect
github.com/onsi/gomega v1.20.1 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.0.3-0.20220114050600-8b9d41f48198 // indirect
github.com/pelletier/go-toml v1.9.5 // indirect
github.com/pelletier/go-toml/v2 v2.0.5 // indirect
github.com/pelletier/go-toml v1.9.4 // indirect
github.com/phayes/checkstyle v0.0.0-20170904204023-bfd46e6a821d // indirect
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/polyfloyd/go-errorlint v1.4.0 // indirect
github.com/prometheus/client_model v0.3.0 // indirect
github.com/prometheus/procfs v0.9.0 // indirect
github.com/quasilyte/go-ruleguard v0.3.19 // indirect
github.com/quasilyte/gogrep v0.5.0 // indirect
github.com/polyfloyd/go-errorlint v0.0.0-20211125173453-6d6d39c5bb8b // indirect
github.com/prometheus/client_golang v1.12.2 // indirect
github.com/prometheus/client_model v0.2.0 // indirect
github.com/prometheus/common v0.32.1 // indirect
github.com/prometheus/procfs v0.7.3 // indirect
github.com/quasilyte/go-ruleguard v0.3.13 // indirect
github.com/quasilyte/regex/syntax v0.0.0-20210819130434-b3f0c404a727 // indirect
github.com/quasilyte/stdinfo v0.0.0-20220114132959-f7386bf02567 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/rogpeppe/go-internal v1.9.0 // indirect
github.com/ryancurrah/gomodguard v1.3.0 // indirect
github.com/ryanrolds/sqlclosecheck v0.4.0 // indirect
github.com/rogpeppe/go-internal v1.8.1-0.20211023094830-115ce09fd6b4 // indirect
github.com/ryancurrah/gomodguard v1.2.3 // indirect
github.com/ryanrolds/sqlclosecheck v0.3.0 // indirect
github.com/sanposhiho/wastedassign/v2 v2.0.7 // indirect
github.com/sashamelentyev/interfacebloat v1.1.0 // indirect
github.com/sashamelentyev/usestdlibvars v1.23.0 // indirect
github.com/sassoftware/go-rpmutils v0.1.0 // indirect
github.com/securego/gosec/v2 v2.15.0 // indirect
github.com/securego/gosec/v2 v2.9.3 // indirect
github.com/sergi/go-diff v1.2.0 // indirect
github.com/shazow/go-diff v0.0.0-20160112020656-b6b7b6733b8c // indirect
github.com/sirupsen/logrus v1.9.0 // indirect
github.com/sivchari/containedctx v1.0.2 // indirect
github.com/sivchari/nosnakecase v1.7.0 // indirect
github.com/sivchari/tenv v1.7.1 // indirect
github.com/sonatard/noctx v0.0.2 // indirect
github.com/sourcegraph/go-diff v0.7.0 // indirect
github.com/spf13/afero v1.8.2 // indirect
github.com/spf13/cast v1.5.0 // indirect
github.com/spf13/cobra v1.6.1 // indirect
github.com/sirupsen/logrus v1.8.1 // indirect
github.com/sivchari/tenv v1.4.7 // indirect
github.com/sonatard/noctx v0.0.1 // indirect
github.com/sourcegraph/go-diff v0.6.1 // indirect
github.com/spf13/afero v1.6.0 // indirect
github.com/spf13/cast v1.4.1 // indirect
github.com/spf13/cobra v1.4.0 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/spf13/viper v1.12.0 // indirect
github.com/spf13/viper v1.9.0 // indirect
github.com/ssgreg/nlreturn/v2 v2.2.1 // indirect
github.com/stbenjam/no-sprintf-host-port v0.1.1 // indirect
github.com/stretchr/objx v0.5.0 // indirect
github.com/stretchr/testify v1.8.2 // indirect
github.com/subosito/gotenv v1.4.1 // indirect
github.com/t-yuki/gocover-cobertura v0.0.0-20180217150009-aaee18c8195c // indirect
github.com/tdakkota/asciicheck v0.2.0 // indirect
github.com/stretchr/objx v0.4.0 // indirect
github.com/stretchr/testify v1.8.0 // indirect
github.com/subosito/gotenv v1.2.0 // indirect
github.com/sylvia7788/contextcheck v1.0.4 // indirect
github.com/tdakkota/asciicheck v0.1.1 // indirect
github.com/tetafro/godot v1.4.11 // indirect
github.com/timakin/bodyclose v0.0.0-20221125081123-e39cf3fc478e // indirect
github.com/timonwong/loggercheck v0.9.4 // indirect
github.com/tomarrell/wrapcheck/v2 v2.8.1 // indirect
github.com/tommy-muehle/go-mnd/v2 v2.5.1 // indirect
github.com/timakin/bodyclose v0.0.0-20210704033933-f49887972144 // indirect
github.com/tomarrell/wrapcheck/v2 v2.4.0 // indirect
github.com/tommy-muehle/go-mnd/v2 v2.4.0 // indirect
github.com/u-root/uio v0.0.0-20221213070652-c3537552635f // indirect
github.com/ulikunitz/xz v0.5.10 // indirect
github.com/ultraware/funlen v0.0.3 // indirect
github.com/ultraware/whitespace v0.0.5 // indirect
github.com/uudashr/gocognit v1.0.6 // indirect
github.com/ultraware/whitespace v0.0.4 // indirect
github.com/uudashr/gocognit v1.0.5 // indirect
github.com/vbatts/tar-split v0.11.2 // indirect
github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74 // indirect
github.com/x448/float16 v0.8.4 // indirect
github.com/xanzy/ssh-agent v0.3.1 // indirect
github.com/yagipy/maintidx v1.0.0 // indirect
github.com/yeya24/promlinter v0.2.0 // indirect
gitlab.com/bosi/decorder v0.2.3 // indirect
github.com/yeya24/promlinter v0.1.0 // indirect
go.uber.org/atomic v1.7.0 // indirect
go.uber.org/multierr v1.6.0 // indirect
golang.org/x/exp/typeparams v0.0.0-20230224173230-c95f2b4c22f2 // indirect
golang.org/x/image v0.5.0 // indirect
golang.org/x/text v0.8.0 // indirect
golang.org/x/exp/typeparams v0.0.0-20221208152030-732eee02a75a // indirect
golang.org/x/image v0.0.0-20201208152932-35266b937fa6 // indirect
golang.org/x/text v0.7.0 // indirect
gomodules.xyz/jsonpatch/v2 v2.2.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/protobuf v1.30.0 // indirect
google.golang.org/protobuf v1.28.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/ini.v1 v1.66.2 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
@@ -344,10 +320,10 @@ require (
k8s.io/klog/v2 v2.70.1 // indirect
k8s.io/kube-openapi v0.0.0-20220803162953-67bda5d908f1 // indirect
k8s.io/utils v0.0.0-20220728103510-ee6ede2d64ed // indirect
mvdan.cc/gofumpt v0.4.0 // indirect
mvdan.cc/gofumpt v0.2.0 // indirect
mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed // indirect
mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b // indirect
mvdan.cc/unparam v0.0.0-20221223090309-7455f1af531d // indirect
mvdan.cc/unparam v0.0.0-20211002134041-24922b6997ca // indirect
sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect
)

View File

@@ -1 +1 @@
sha256-lirn07XE3JOS6oiwZBMwxzywkbXHowOJUMWWLrZtccY=
sha256-zyyqBRFPNPzPYCMgnbnOy5rb3fkn4XEHZlTlJvwqunM=

461
go.sum

File diff suppressed because it is too large Load Diff

View File

@@ -1 +1 @@
ddff070c02790cb571006e820e58cce9627569cf
ec180cbca39fcb5dc420399b37583e53fcf382c9

View File

@@ -36,7 +36,6 @@ func New() *tailcfg.Hostinfo {
return &tailcfg.Hostinfo{
IPNVersion: version.Long(),
Hostname: hostname,
App: appTypeCached(),
OS: version.OS(),
OSVersion: GetOSVersion(),
Container: lazyInContainer.Get(),
@@ -113,13 +112,6 @@ func GetOSVersion() string {
return ""
}
func appTypeCached() string {
if v, ok := appType.Load().(string); ok {
return v
}
return ""
}
func packageTypeCached() string {
if v, _ := packagingType.Load().(string); v != "" {
return v
@@ -167,7 +159,6 @@ var (
osVersionAtomic atomic.Value // of string
desktopAtomic atomic.Value // of opt.Bool
packagingType atomic.Value // of string
appType atomic.Value // of string
)
// SetPushDeviceToken sets the device token for use in Hostinfo updates.
@@ -185,11 +176,6 @@ func SetOSVersion(v string) { osVersionAtomic.Store(v) }
// F-Droid build) and tsnet (set to "tsnet").
func SetPackage(v string) { packagingType.Store(v) }
// SetApp sets the app type for the app.
// It is used by tsnet to specify what app is using it such as "golinks"
// and "k8s-operator".
func SetApp(v string) { appType.Store(v) }
func deviceModel() string {
s, _ := deviceModelAtomic.Load().(string)
return s
@@ -405,7 +391,7 @@ func DisabledEtcAptSource() bool {
return false
}
mod := fi.ModTime()
if c, ok := etcAptSrcCache.Load().(etcAptSrcResult); ok && c.mod.Equal(mod) {
if c, ok := etcAptSrcCache.Load().(etcAptSrcResult); ok && c.mod == mod {
return c.disabled
}
f, err := os.Open(path)

View File

@@ -6,7 +6,6 @@
package tooldeps
import (
_ "github.com/golangci/golangci-lint/cmd/golangci-lint"
_ "github.com/tailscale/depaware/depaware"
_ "golang.org/x/tools/cmd/goimports"
)

View File

@@ -17,7 +17,6 @@ import (
"time"
"tailscale.com/envknob"
"tailscale.com/net/sockstats"
"tailscale.com/tailcfg"
"tailscale.com/util/clientmetric"
"tailscale.com/util/goroutines"
@@ -84,19 +83,6 @@ func (b *LocalBackend) handleC2N(w http.ResponseWriter, r *http.Request) {
return
}
writeJSON(res)
case "/sockstats":
if r.Method != "POST" {
http.Error(w, "bad method", http.StatusMethodNotAllowed)
return
}
w.Header().Set("Content-Type", "text/plain")
if b.sockstatLogger == nil {
http.Error(w, "no sockstatLogger", http.StatusInternalServerError)
return
}
b.sockstatLogger.Flush()
fmt.Fprintf(w, "logid: %s\n", b.sockstatLogger.LogID())
fmt.Fprintf(w, "debug info: %v\n", sockstats.DebugInfo())
default:
http.Error(w, "unknown c2n path", http.StatusBadRequest)
}

View File

@@ -31,13 +31,10 @@ import (
"time"
"golang.org/x/crypto/acme"
"tailscale.com/atomicfile"
"tailscale.com/envknob"
"tailscale.com/hostinfo"
"tailscale.com/ipn"
"tailscale.com/ipn/ipnstate"
"tailscale.com/ipn/store"
"tailscale.com/ipn/store/mem"
"tailscale.com/types/logger"
"tailscale.com/version"
"tailscale.com/version/distro"
@@ -85,6 +82,11 @@ func (b *LocalBackend) GetCertPEM(ctx context.Context, domain string) (*TLSCertK
return nil, errors.New("invalid domain")
}
logf := logger.WithPrefix(b.logf, fmt.Sprintf("cert(%q): ", domain))
dir, err := b.certDir()
if err != nil {
logf("failed to get certDir: %v", err)
return nil, err
}
now := time.Now()
traceACME := func(v any) {
if !acmeDebug() {
@@ -94,22 +96,17 @@ func (b *LocalBackend) GetCertPEM(ctx context.Context, domain string) (*TLSCertK
log.Printf("acme %T: %s", v, j)
}
cs, err := b.getCertStore()
if err != nil {
return nil, err
}
if pair, err := getCertPEMCached(cs, domain, now); err == nil {
if pair, err := b.getCertPEMCached(dir, domain, now); err == nil {
future := now.AddDate(0, 0, 14)
if b.shouldStartDomainRenewal(cs, domain, future) {
if b.shouldStartDomainRenewal(dir, domain, future) {
logf("starting async renewal")
// Start renewal in the background.
go b.getCertPEM(context.Background(), cs, logf, traceACME, domain, future)
go b.getCertPEM(context.Background(), logf, traceACME, dir, domain, future)
}
return pair, nil
}
pair, err := b.getCertPEM(ctx, cs, logf, traceACME, domain, now)
pair, err := b.getCertPEM(ctx, logf, traceACME, dir, domain, now)
if err != nil {
logf("getCertPEM: %v", err)
return nil, err
@@ -117,7 +114,7 @@ func (b *LocalBackend) GetCertPEM(ctx context.Context, domain string) (*TLSCertK
return pair, nil
}
func (b *LocalBackend) shouldStartDomainRenewal(cs certStore, domain string, future time.Time) bool {
func (b *LocalBackend) shouldStartDomainRenewal(dir, domain string, future time.Time) bool {
renewMu.Lock()
defer renewMu.Unlock()
now := time.Now()
@@ -127,7 +124,7 @@ func (b *LocalBackend) shouldStartDomainRenewal(cs certStore, domain string, fut
return false
}
lastRenewCheck[domain] = now
_, err := getCertPEMCached(cs, domain, future)
_, err := b.getCertPEMCached(dir, domain, future)
return errors.Is(err, errCertExpired)
}
@@ -143,32 +140,15 @@ type certStore interface {
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
}
var errCertExpired = errors.New("cert expired")
func (b *LocalBackend) getCertStore() (certStore, error) {
switch b.store.(type) {
case *store.FileStore:
case *mem.Store:
default:
if hostinfo.GetEnvType() == hostinfo.Kubernetes {
// We're running in Kubernetes with a custom StateStore,
// use that instead of the cert directory.
// TODO(maisem): expand this to other environments?
return certStateStore{StateStore: b.store}, nil
}
func (b *LocalBackend) getCertStore(dir string) certStore {
if hostinfo.GetEnvType() == hostinfo.Kubernetes && dir == "/tmp" {
return certStateStore{StateStore: b.store}
}
dir, err := b.certDir()
if err != nil {
return nil, err
}
return certFileStore{dir: dir}, nil
return certFileStore{dir: dir}
}
// certFileStore implements certStore by storing the cert & key files in the named directory.
@@ -180,25 +160,6 @@ type certFileStore struct {
testRoots *x509.CertPool
}
const acmePEMName = "acme-account.key.pem"
func (f certFileStore) ACMEKey() ([]byte, error) {
pemName := filepath.Join(f.dir, acmePEMName)
v, err := os.ReadFile(pemName)
if err != nil {
if os.IsNotExist(err) {
return nil, ipn.ErrStateNotExist
}
return nil, err
}
return v, nil
}
func (f certFileStore) WriteACMEKey(b []byte) error {
pemName := filepath.Join(f.dir, acmePEMName)
return atomicfile.WriteFile(pemName, b, 0600)
}
func (f certFileStore) Read(domain string, now time.Time) (*TLSCertKeyPair, error) {
certPEM, err := os.ReadFile(certFile(f.dir, domain))
if err != nil {
@@ -221,11 +182,11 @@ func (f certFileStore) Read(domain string, now time.Time) (*TLSCertKeyPair, erro
}
func (f certFileStore) WriteCert(domain string, cert []byte) error {
return atomicfile.WriteFile(certFile(f.dir, domain), cert, 0644)
return os.WriteFile(certFile(f.dir, domain), cert, 0644)
}
func (f certFileStore) WriteKey(domain string, key []byte) error {
return atomicfile.WriteFile(keyFile(f.dir, domain), key, 0600)
return os.WriteFile(keyFile(f.dir, domain), key, 0600)
}
// certStateStore implements certStore by storing the cert & key files in an ipn.StateStore.
@@ -260,14 +221,6 @@ func (s certStateStore) WriteKey(domain string, key []byte) error {
return s.WriteState(ipn.StateKey(domain+".key"), key)
}
func (s certStateStore) ACMEKey() ([]byte, error) {
return s.ReadState(ipn.StateKey(acmePEMName))
}
func (s certStateStore) WriteACMEKey(key []byte) error {
return s.WriteState(ipn.StateKey(acmePEMName), key)
}
// TLSCertKeyPair is a TLS public and private key, and whether they were obtained
// from cache or freshly obtained.
type TLSCertKeyPair struct {
@@ -283,26 +236,26 @@ func certFile(dir, domain string) string { return filepath.Join(dir, domain+".cr
// domain exists on disk in dir that is valid at the provided now time.
// If the keypair is expired, it returns errCertExpired.
// If the keypair doesn't exist, it returns ipn.ErrStateNotExist.
func getCertPEMCached(cs certStore, domain string, now time.Time) (p *TLSCertKeyPair, err error) {
func (b *LocalBackend) getCertPEMCached(dir, domain string, now time.Time) (p *TLSCertKeyPair, err error) {
if !validLookingCertDomain(domain) {
// Before we read files from disk using it, validate it's halfway
// reasonable looking.
return nil, fmt.Errorf("invalid domain %q", domain)
}
return cs.Read(domain, now)
return b.getCertStore(dir).Read(domain, now)
}
func (b *LocalBackend) getCertPEM(ctx context.Context, cs certStore, logf logger.Logf, traceACME func(any), domain string, now time.Time) (*TLSCertKeyPair, error) {
func (b *LocalBackend) getCertPEM(ctx context.Context, logf logger.Logf, traceACME func(any), dir, domain string, now time.Time) (*TLSCertKeyPair, error) {
acmeMu.Lock()
defer acmeMu.Unlock()
if p, err := getCertPEMCached(cs, domain, now); err == nil {
if p, err := b.getCertPEMCached(dir, domain, now); err == nil {
return p, nil
} else if !errors.Is(err, ipn.ErrStateNotExist) && !errors.Is(err, errCertExpired) {
return nil, err
}
key, err := acmeKey(cs)
key, err := acmeKey(dir)
if err != nil {
return nil, fmt.Errorf("acmeKey: %w", err)
}
@@ -413,7 +366,8 @@ 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 {
certStore := b.getCertStore(dir)
if err := certStore.WriteKey(domain, privPEM.Bytes()); err != nil {
return nil, err
}
@@ -436,7 +390,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 := certStore.WriteCert(domain, certPEM.Bytes()); err != nil {
return nil, err
}
@@ -490,15 +444,14 @@ func parsePrivateKey(der []byte) (crypto.Signer, error) {
return nil, errors.New("acme/autocert: failed to parse private key")
}
func acmeKey(cs certStore) (crypto.Signer, error) {
if v, err := cs.ACMEKey(); err == nil {
func acmeKey(dir string) (crypto.Signer, error) {
pemName := filepath.Join(dir, "acme-account.key.pem")
if v, err := os.ReadFile(pemName); err == nil {
priv, _ := pem.Decode(v)
if priv == nil || !strings.Contains(priv.Type, "PRIVATE") {
return nil, errors.New("acme/autocert: invalid account key found in cache")
}
return parsePrivateKey(priv.Bytes)
} else if err != nil && !errors.Is(err, ipn.ErrStateNotExist) {
return nil, err
}
privKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
@@ -509,7 +462,7 @@ func acmeKey(cs certStore) (crypto.Signer, error) {
if err := encodeECDSAKey(&pemBuf, privKey); err != nil {
return nil, err
}
if err := cs.WriteACMEKey(pemBuf.Bytes()); err != nil {
if err := os.WriteFile(pemName, pemBuf.Bytes(), 0600); err != nil {
return nil, err
}
return privKey, nil

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