Compare commits
144 Commits
tom/tka6
...
marwan/tmp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
98a708c099 | ||
|
|
7a530173bd | ||
|
|
f58751eb2b | ||
|
|
ce11c82d51 | ||
|
|
90ba26cea1 | ||
|
|
7778d708a6 | ||
|
|
f66ddb544c | ||
|
|
e3b2250e26 | ||
|
|
6f521c138d | ||
|
|
04a3118d45 | ||
|
|
c791e64881 | ||
|
|
7330aa593e | ||
|
|
7f17e04a5a | ||
|
|
4722f7e322 | ||
|
|
3ede3aafe4 | ||
|
|
f844791e15 | ||
|
|
cd35a79136 | ||
|
|
f85dc6f97c | ||
|
|
5acc7c4b1e | ||
|
|
c328770184 | ||
|
|
588a234fdc | ||
|
|
c3ef6fb4ee | ||
|
|
85de580455 | ||
|
|
d0906cda97 | ||
|
|
7c386ca6d2 | ||
|
|
7f057d7489 | ||
|
|
c7cea825ae | ||
|
|
280255acae | ||
|
|
ff1b35ec6c | ||
|
|
9a655a1d58 | ||
|
|
28cb1221ba | ||
|
|
d5a870b4dc | ||
|
|
162488a775 | ||
|
|
c5150eae67 | ||
|
|
80b138f0df | ||
|
|
4b49ca4a12 | ||
|
|
10f1c90f4d | ||
|
|
29f7df9d8f | ||
|
|
83c41f3697 | ||
|
|
20f17d6e7b | ||
|
|
bd0c32ca21 | ||
|
|
b7f51a1468 | ||
|
|
f352f8a0e6 | ||
|
|
8dec1a8724 | ||
|
|
4ecc7fdf5f | ||
|
|
6866aaeab3 | ||
|
|
c889254b42 | ||
|
|
228d0c6aea | ||
|
|
64bbf1738e | ||
|
|
a5fd51ebdc | ||
|
|
a7c910e361 | ||
|
|
edb02b63f8 | ||
|
|
782ccb5655 | ||
|
|
bb34589748 | ||
|
|
9e50da321b | ||
|
|
bdc7a61c24 | ||
|
|
33b006cacf | ||
|
|
e5d272f445 | ||
|
|
7c95734907 | ||
|
|
8546ff98fb | ||
|
|
c153e6ae2f | ||
|
|
11e6247d2a | ||
|
|
690446c784 | ||
|
|
cef0a474f8 | ||
|
|
03b2c44a21 | ||
|
|
1bec2cbbd5 | ||
|
|
f571536598 | ||
|
|
e09c434e5d | ||
|
|
e1b71c83ac | ||
|
|
a257b2f88b | ||
|
|
fb18af5564 | ||
|
|
c573bef0aa | ||
|
|
6cfcb3cae4 | ||
|
|
e978299bf0 | ||
|
|
22680a11ae | ||
|
|
75784e10e2 | ||
|
|
6a627e5a33 | ||
|
|
92459a9248 | ||
|
|
7012bf7981 | ||
|
|
07b29f13dc | ||
|
|
f49b9f75b8 | ||
|
|
c0e0a5458f | ||
|
|
81fd00a6b7 | ||
|
|
d42d570066 | ||
|
|
2c0bda6e2e | ||
|
|
3d29da105c | ||
|
|
765d3253f3 | ||
|
|
ba4e58f429 | ||
|
|
7bfb7744b7 | ||
|
|
f475e5550c | ||
|
|
45138fcfba | ||
|
|
b0ed863d55 | ||
|
|
4d1b3bc26f | ||
|
|
6d5c3c1637 | ||
|
|
5a3da3cd7f | ||
|
|
90fd04cbde | ||
|
|
e3cb8cc88d | ||
|
|
8d3acc9235 | ||
|
|
483109b8fc | ||
|
|
59879e5770 | ||
|
|
1bf65e4760 | ||
|
|
38bbb30aaf | ||
|
|
f4da995940 | ||
|
|
02582083d5 | ||
|
|
40fa2a420c | ||
|
|
8ed4fd1dbc | ||
|
|
3b39ca9017 | ||
|
|
e0d291ab8a | ||
|
|
2b00d6922f | ||
|
|
7b4e85aa78 | ||
|
|
e99c7c3ee5 | ||
|
|
38e4d303a2 | ||
|
|
62a1e9a44f | ||
|
|
985535aebc | ||
|
|
d1d5d52b2c | ||
|
|
2522b0615f | ||
|
|
c98652c333 | ||
|
|
524f53de89 | ||
|
|
8c2b755b2e | ||
|
|
a31e43f760 | ||
|
|
c628132b34 | ||
|
|
e04acabfde | ||
|
|
cb960d6cdd | ||
|
|
27e37cf9b3 | ||
|
|
946451b43e | ||
|
|
840d69e1eb | ||
|
|
3ba9f8dd04 | ||
|
|
7c99210e68 | ||
|
|
920ec69241 | ||
|
|
2a933c1903 | ||
|
|
43f7ec48ca | ||
|
|
3177ccabe5 | ||
|
|
7908b6d616 | ||
|
|
ed10a1769b | ||
|
|
5ba57e4661 | ||
|
|
d5abdd915e | ||
|
|
74eb99aed1 | ||
|
|
09d0b632d4 | ||
|
|
d39a5e4417 | ||
|
|
d2fd101eb4 | ||
|
|
8ac5976897 | ||
|
|
7300b908fb | ||
|
|
ca19cf13e9 | ||
|
|
33b359642e |
2
.github/workflows/go-licenses.yml
vendored
2
.github/workflows/go-licenses.yml
vendored
@@ -50,7 +50,7 @@ jobs:
|
||||
private_key: ${{ secrets.LICENSING_APP_PRIVATE_KEY }}
|
||||
|
||||
- name: Send pull request
|
||||
uses: peter-evans/create-pull-request@38e0b6e68b4c852a5500a94740f0e535e0d7ba54 #v4.2.4
|
||||
uses: peter-evans/create-pull-request@5b4a9f6a9e2af26e5f02351490b90d01eb8ec1e5 #v5.0.0
|
||||
with:
|
||||
token: ${{ steps.generate-token.outputs.token }}
|
||||
author: License Updater <noreply@tailscale.com>
|
||||
|
||||
40
.github/workflows/golangci-lint.yml
vendored
Normal file
40
.github/workflows/golangci-lint.yml
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
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
|
||||
88
.github/workflows/test.yml
vendored
88
.github/workflows/test.yml
vendored
@@ -46,14 +46,30 @@ jobs:
|
||||
include:
|
||||
- goarch: amd64
|
||||
- goarch: amd64
|
||||
variant: race
|
||||
buildflags: "-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 ./...
|
||||
run: ./tool/go build ${{matrix.buildflags}} ./...
|
||||
env:
|
||||
GOARCH: ${{ matrix.goarch }}
|
||||
- name: build variant CLIs
|
||||
@@ -73,13 +89,7 @@ jobs:
|
||||
- name: build test wrapper
|
||||
run: ./tool/go build -o /tmp/testwrapper ./cmd/testwrapper
|
||||
- name: test all
|
||||
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 ./...
|
||||
run: ./tool/go test ${{matrix.buildflags}} -exec=/tmp/testwrapper -bench=. -benchtime=1x ./...
|
||||
env:
|
||||
GOARCH: ${{ matrix.goarch }}
|
||||
- name: check that no tracked files changed
|
||||
@@ -101,6 +111,13 @@ 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:
|
||||
@@ -109,17 +126,19 @@ 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).
|
||||
# TODO(raggi): add a go version here.
|
||||
key: ${{ runner.os }}-go-2-${{ hashFiles('**/go.sum') }}
|
||||
key: ${{ github.job }}-${{ runner.os }}-go-2-${{ hashFiles('**/go.sum') }}
|
||||
restore-keys: |
|
||||
${{ github.job }}-${{ runner.os }}-go-2-
|
||||
- name: test
|
||||
# Don't use -bench=. -benchtime=1x.
|
||||
# Somewhere in the layers (powershell?)
|
||||
# the equals signs cause great confusion.
|
||||
run: ./tool/go test -bench . -benchtime 1x ./...
|
||||
run: go test -bench . -benchtime 1x ./...
|
||||
|
||||
vm:
|
||||
runs-on: ["self-hosted", "linux", "vm"]
|
||||
@@ -174,6 +193,22 @@ 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:
|
||||
@@ -223,6 +258,22 @@ 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:
|
||||
@@ -235,6 +286,15 @@ 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
|
||||
@@ -372,6 +432,7 @@ jobs:
|
||||
- cross
|
||||
- ios
|
||||
- wasm
|
||||
- tailscale_go
|
||||
- fuzz
|
||||
- depaware
|
||||
- go_generate
|
||||
@@ -389,7 +450,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.0.0
|
||||
uses: ruby/action-slack@v3.2.1
|
||||
with:
|
||||
payload: |
|
||||
{
|
||||
@@ -416,6 +477,7 @@ jobs:
|
||||
- cross
|
||||
- ios
|
||||
- wasm
|
||||
- tailscale_go
|
||||
- fuzz
|
||||
- depaware
|
||||
- go_generate
|
||||
|
||||
31
.github/workflows/tsconnect-pkg-publish.yml
vendored
31
.github/workflows/tsconnect-pkg-publish.yml
vendored
@@ -1,31 +0,0 @@
|
||||
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
|
||||
2
.github/workflows/update-flake.yml
vendored
2
.github/workflows/update-flake.yml
vendored
@@ -35,7 +35,7 @@ jobs:
|
||||
private_key: ${{ secrets.LICENSING_APP_PRIVATE_KEY }}
|
||||
|
||||
- name: Send pull request
|
||||
uses: peter-evans/create-pull-request@38e0b6e68b4c852a5500a94740f0e535e0d7ba54 #v4.2.4
|
||||
uses: peter-evans/create-pull-request@5b4a9f6a9e2af26e5f02351490b90d01eb8ec1e5 #v5.0.0
|
||||
with:
|
||||
token: ${{ steps.generate-token.outputs.token }}
|
||||
author: Flakes Updater <noreply@tailscale.com>
|
||||
|
||||
61
.golangci.yml
Normal file
61
.golangci.yml
Normal file
@@ -0,0 +1,61 @@
|
||||
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
|
||||
6
api.md
6
api.md
@@ -1336,8 +1336,8 @@ It holds the capabilities specified in the request and can no longer be retrieve
|
||||
|
||||
``` jsonc
|
||||
{
|
||||
"id": "XXXX456CNTRL",
|
||||
"key": "tskey-k123456CNTRL-abcdefghijklmnopqrstuvwxyz",
|
||||
"id": "k123456CNTRL",
|
||||
"key": "tskey-auth-k123456CNTRL-abcdefghijklmnopqrstuvwxyz",
|
||||
"created": "2021-12-09T23:22:39Z",
|
||||
"expires": "2022-03-09T23:22:39Z",
|
||||
"revoked": "2022-03-12T23:22:39Z",
|
||||
@@ -1348,9 +1348,9 @@ It holds the capabilities specified in the request and can no longer be retrieve
|
||||
"ephemeral": false,
|
||||
"preauthorized": false,
|
||||
"tags": [ "tag:example" ]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -8,14 +8,20 @@
|
||||
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.
|
||||
// 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.
|
||||
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
|
||||
|
||||
47
atomicfile/atomicfile_test.go
Normal file
47
atomicfile/atomicfile_test.go
Normal file
@@ -0,0 +1,47 @@
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
@@ -436,7 +436,7 @@ func (c *Client) ValidateACLJSON(ctx context.Context, source, dest string) (test
|
||||
}
|
||||
}()
|
||||
|
||||
tests := []ACLTest{ACLTest{User: source, Allow: []string{dest}}}
|
||||
tests := []ACLTest{{User: source, Allow: []string{dest}}}
|
||||
postData, err := json.Marshal(tests)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -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 interface{}) ([]byte, error) {
|
||||
func (c *Client) dnsPOSTRequest(ctx context.Context, endpoint string, postData any) ([]byte, error) {
|
||||
path := fmt.Sprintf("%s/api/v2/tailnet/%s/dns/%s", c.baseURL(), c.tailnet, endpoint)
|
||||
data, err := json.Marshal(&postData)
|
||||
if err != nil {
|
||||
|
||||
@@ -96,8 +96,9 @@ 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", "localhost:"+strconv.Itoa(port))
|
||||
return d.DialContext(ctx, "tcp", "127.0.0.1:"+strconv.Itoa(port))
|
||||
}
|
||||
}
|
||||
s := safesocket.DefaultConnectionStrategy(lc.socket())
|
||||
@@ -1101,7 +1102,6 @@ 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 {
|
||||
|
||||
@@ -8,21 +8,63 @@ 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
|
||||
@@ -44,6 +86,7 @@ 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
|
||||
@@ -60,8 +103,10 @@ 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+
|
||||
@@ -85,7 +130,7 @@ 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
|
||||
@@ -112,7 +157,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
|
||||
@@ -169,8 +214,10 @@ 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+
|
||||
@@ -188,7 +235,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+
|
||||
@@ -201,6 +248,7 @@ 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+
|
||||
|
||||
@@ -36,8 +36,8 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
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.")
|
||||
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.")
|
||||
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")
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"expvar"
|
||||
"flag"
|
||||
"fmt"
|
||||
"html"
|
||||
@@ -30,7 +29,7 @@ var (
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
p := prober.New().WithSpread(*spread).WithOnce(*probeOnce)
|
||||
p := prober.New().WithSpread(*spread).WithOnce(*probeOnce).WithMetricNamespace("derpprobe")
|
||||
dp, err := prober.DERP(p, *derpMapURL, *interval, *interval, *interval)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
@@ -53,7 +52,6 @@ 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))
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ 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"
|
||||
@@ -670,11 +669,11 @@ func expectedSTS(stsName, secretName, hostname string) *appsv1.StatefulSet {
|
||||
},
|
||||
},
|
||||
},
|
||||
Containers: []v1.Container{
|
||||
Containers: []corev1.Container{
|
||||
{
|
||||
Name: "tailscale",
|
||||
Image: "tailscale/tailscale",
|
||||
Env: []v1.EnvVar{
|
||||
Env: []corev1.EnvVar{
|
||||
{Name: "TS_USERSPACE", Value: "false"},
|
||||
{Name: "TS_AUTH_ONCE", Value: "true"},
|
||||
{Name: "TS_DEST_IP", Value: "10.20.30.40"},
|
||||
|
||||
@@ -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 acknowldge start-ssl, said %q", buf[0])
|
||||
return fmt.Errorf("upstream didn't acknowledge start-ssl, said %q", buf[0])
|
||||
}
|
||||
tlsConf := &tls.Config{
|
||||
ServerName: p.upstreamHost,
|
||||
|
||||
@@ -120,6 +120,8 @@ change in the future.
|
||||
pingCmd,
|
||||
ncCmd,
|
||||
sshCmd,
|
||||
funnelCmd,
|
||||
serveCmd,
|
||||
versionCmd,
|
||||
webCmd,
|
||||
fileCmd,
|
||||
@@ -147,10 +149,6 @@ change in the future.
|
||||
switch {
|
||||
case slices.Contains(args, "debug"):
|
||||
rootCmd.Subcommands = append(rootCmd.Subcommands, debugCmd)
|
||||
case slices.Contains(args, "funnel"):
|
||||
rootCmd.Subcommands = append(rootCmd.Subcommands, funnelCmd)
|
||||
case slices.Contains(args, "serve"):
|
||||
rootCmd.Subcommands = append(rootCmd.Subcommands, serveCmd)
|
||||
case slices.Contains(args, "update"):
|
||||
rootCmd.Subcommands = append(rootCmd.Subcommands, updateCmd)
|
||||
}
|
||||
|
||||
@@ -621,9 +621,16 @@ func TestPrefsFromUpArgs(t *testing.T) {
|
||||
{
|
||||
name: "error_long_hostname",
|
||||
args: upArgsT{
|
||||
hostname: strings.Repeat("a", 300),
|
||||
hostname: strings.Repeat(strings.Repeat("a", 63)+".", 4),
|
||||
},
|
||||
wantErr: `hostname too long: 300 bytes (max 256)`,
|
||||
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`,
|
||||
},
|
||||
{
|
||||
name: "error_linux_netfilter_empty",
|
||||
|
||||
@@ -29,7 +29,7 @@ var funnelCmd = newFunnelCommand(&serveEnv{lc: &localClient})
|
||||
func newFunnelCommand(e *serveEnv) *ffcli.Command {
|
||||
return &ffcli.Command{
|
||||
Name: "funnel",
|
||||
ShortHelp: "[ALPHA] turn Tailscale Funnel on or off",
|
||||
ShortHelp: "Turn on/off Funnel service",
|
||||
ShortUsage: strings.TrimSpace(`
|
||||
funnel <serve-port> {on|off}
|
||||
funnel status [--json]
|
||||
|
||||
@@ -19,6 +19,7 @@ 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"
|
||||
@@ -45,9 +46,15 @@ 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(logger.WithPrefix(log.Printf, "portmap: "), nil, nil),
|
||||
PortMapper: portmapper.NewClient(logf, netMon, nil, nil),
|
||||
UseDNSCache: false, // always resolve, don't cache
|
||||
}
|
||||
if netcheckArgs.verbose {
|
||||
c.Logf = logger.WithPrefix(log.Printf, "netcheck: ")
|
||||
@@ -96,7 +103,6 @@ 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":
|
||||
|
||||
@@ -33,7 +33,7 @@ var serveCmd = newServeCommand(&serveEnv{lc: &localClient})
|
||||
func newServeCommand(e *serveEnv) *ffcli.Command {
|
||||
return &ffcli.Command{
|
||||
Name: "serve",
|
||||
ShortHelp: "[ALPHA] Serve from your Tailscale node",
|
||||
ShortHelp: "Serve content and local servers",
|
||||
ShortUsage: strings.TrimSpace(`
|
||||
serve https:<port> <mount-point> <source> [off]
|
||||
serve tcp:<port> tcp://localhost:<local-port> [off]
|
||||
@@ -41,7 +41,7 @@ serve https:<port> <mount-point> <source> [off]
|
||||
serve status [--json]
|
||||
`),
|
||||
LongHelp: strings.TrimSpace(`
|
||||
*** ALPHA; all of this is subject to change ***
|
||||
*** BETA; 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
|
||||
@@ -453,6 +453,7 @@ func expandProxyTarget(source string) (string, error) {
|
||||
if u.Port() != "" {
|
||||
url += ":" + u.Port()
|
||||
}
|
||||
url += u.Path
|
||||
return url, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -262,6 +262,18 @@ 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})
|
||||
|
||||
@@ -34,6 +34,7 @@ import (
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/preftype"
|
||||
"tailscale.com/util/dnsname"
|
||||
"tailscale.com/version"
|
||||
"tailscale.com/version/distro"
|
||||
)
|
||||
@@ -320,8 +321,8 @@ func prefsFromUpArgs(upArgs upArgsT, warnf logger.Logf, st *ipnstate.Status, goo
|
||||
}
|
||||
}
|
||||
|
||||
if len(upArgs.hostname) > 256 {
|
||||
return nil, fmt.Errorf("hostname too long: %d bytes (max 256)", len(upArgs.hostname))
|
||||
if err := dnsname.ValidHostname(upArgs.hostname); upArgs.hostname != "" && err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
prefs := ipn.NewPrefs()
|
||||
|
||||
@@ -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.LoginName }}
|
||||
{{ with .Profile }}
|
||||
<div class="text-right w-full leading-4">
|
||||
<h4 class="truncate leading-normal">{{.}}</h4>
|
||||
<h4 class="truncate leading-normal">{{.LoginName}}</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="#"
|
||||
|
||||
@@ -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,9 +74,10 @@ 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+
|
||||
@@ -92,7 +93,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+
|
||||
|
||||
@@ -23,10 +23,10 @@ import (
|
||||
"tailscale.com/derp/derphttp"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/net/interfaces"
|
||||
"tailscale.com/net/netmon"
|
||||
"tailscale.com/net/tshttpproxy"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/wgengine/monitor"
|
||||
)
|
||||
|
||||
var debugArgs struct {
|
||||
@@ -42,7 +42,7 @@ var debugModeFunc = debugMode // so it can be addressable
|
||||
func debugMode(args []string) error {
|
||||
fs := flag.NewFlagSet("debug", flag.ExitOnError)
|
||||
fs.BoolVar(&debugArgs.ifconfig, "ifconfig", false, "If true, print network interface state")
|
||||
fs.BoolVar(&debugArgs.monitor, "monitor", false, "If true, run link monitor forever. Precludes all other options.")
|
||||
fs.BoolVar(&debugArgs.monitor, "monitor", false, "If true, run network monitor forever. Precludes all other options.")
|
||||
fs.BoolVar(&debugArgs.portmap, "portmap", false, "If true, run portmap debugging. Precludes all other options.")
|
||||
fs.StringVar(&debugArgs.getURL, "get-url", "", "If non-empty, fetch provided URL.")
|
||||
fs.StringVar(&debugArgs.derpCheck, "derp", "", "if non-empty, test a DERP ping via named region code")
|
||||
@@ -76,7 +76,7 @@ func runMonitor(ctx context.Context, loop bool) error {
|
||||
j, _ := json.MarshalIndent(st, "", " ")
|
||||
os.Stderr.Write(j)
|
||||
}
|
||||
mon, err := monitor.New(log.Printf)
|
||||
mon, err := netmon.New(log.Printf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -84,10 +84,10 @@ func runMonitor(ctx context.Context, loop bool) error {
|
||||
|
||||
mon.RegisterChangeCallback(func(changed bool, st *interfaces.State) {
|
||||
if !changed {
|
||||
log.Printf("Link monitor fired; no change")
|
||||
log.Printf("Network monitor fired; no change")
|
||||
return
|
||||
}
|
||||
log.Printf("Link monitor fired. New state:")
|
||||
log.Printf("Network monitor fired. New state:")
|
||||
dump(st)
|
||||
})
|
||||
if loop {
|
||||
@@ -193,8 +193,8 @@ func checkDerp(ctx context.Context, derpRegion string) (err error) {
|
||||
priv1 := key.NewNode()
|
||||
priv2 := key.NewNode()
|
||||
|
||||
c1 := derphttp.NewRegionClient(priv1, log.Printf, getRegion)
|
||||
c2 := derphttp.NewRegionClient(priv2, log.Printf, getRegion)
|
||||
c1 := derphttp.NewRegionClient(priv1, log.Printf, nil, getRegion)
|
||||
c2 := derphttp.NewRegionClient(priv2, log.Printf, nil, getRegion)
|
||||
defer func() {
|
||||
if err != nil {
|
||||
c1.Close()
|
||||
|
||||
@@ -154,6 +154,7 @@ 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+
|
||||
@@ -200,6 +201,7 @@ 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+
|
||||
@@ -238,6 +240,7 @@ 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+
|
||||
@@ -254,6 +257,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
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
|
||||
@@ -262,13 +266,14 @@ 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 from tailscale.com/cmd/tailscaled
|
||||
tailscale.com/tstime/rate from tailscale.com/wgengine/filter+
|
||||
tailscale.com/tsweb/varz 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
|
||||
@@ -308,10 +313,11 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/util/set from tailscale.com/health+
|
||||
tailscale.com/util/singleflight from tailscale.com/control/controlclient+
|
||||
tailscale.com/util/slicesx from tailscale.com/net/dnscache+
|
||||
tailscale.com/util/sysresources from tailscale.com/wgengine/magicsock
|
||||
tailscale.com/util/systemd from tailscale.com/control/controlclient+
|
||||
tailscale.com/util/uniq from tailscale.com/wgengine/magicsock+
|
||||
tailscale.com/util/vizerror from tailscale.com/tsweb
|
||||
💣 tailscale.com/util/winutil from tailscale.com/control/controlclient+
|
||||
W tailscale.com/util/winutil/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
|
||||
@@ -319,7 +325,6 @@ 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+
|
||||
@@ -345,13 +350,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+
|
||||
|
||||
@@ -39,16 +39,18 @@ 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"
|
||||
"tailscale.com/tsweb/varz"
|
||||
"tailscale.com/types/flagtype"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/logid"
|
||||
@@ -58,7 +60,6 @@ import (
|
||||
"tailscale.com/version"
|
||||
"tailscale.com/version/distro"
|
||||
"tailscale.com/wgengine"
|
||||
"tailscale.com/wgengine/monitor"
|
||||
"tailscale.com/wgengine/netstack"
|
||||
"tailscale.com/wgengine/router"
|
||||
)
|
||||
@@ -328,7 +329,15 @@ var logPol *logpolicy.Policy
|
||||
var debugMux *http.ServeMux
|
||||
|
||||
func run() error {
|
||||
pol := logpolicy.New(logtail.CollectionNode)
|
||||
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.SetVerbosityLevel(args.verbose)
|
||||
logPol = pol
|
||||
defer func() {
|
||||
@@ -352,7 +361,6 @@ func run() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
var logf logger.Logf = log.Printf
|
||||
if envknob.Bool("TS_DEBUG_MEMORY") {
|
||||
logf = logger.RusagePrefixLog(logf)
|
||||
}
|
||||
@@ -378,10 +386,10 @@ func run() error {
|
||||
debugMux = newDebugMux()
|
||||
}
|
||||
|
||||
return startIPNServer(context.Background(), logf, pol.PublicID)
|
||||
return startIPNServer(context.Background(), logf, pol.PublicID, netMon)
|
||||
}
|
||||
|
||||
func startIPNServer(ctx context.Context, logf logger.Logf, logID logid.PublicID) error {
|
||||
func startIPNServer(ctx context.Context, logf logger.Logf, logID logid.PublicID, netMon *netmon.Monitor) error {
|
||||
ln, err := safesocket.Listen(args.socketpath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("safesocket.Listen: %v", err)
|
||||
@@ -407,7 +415,7 @@ func startIPNServer(ctx context.Context, logf logger.Logf, logID logid.PublicID)
|
||||
}
|
||||
}()
|
||||
|
||||
srv := ipnserver.New(logf, logID)
|
||||
srv := ipnserver.New(logf, logID, netMon)
|
||||
if debugMux != nil {
|
||||
debugMux.HandleFunc("/debug/ipn", srv.ServeHTMLStatus)
|
||||
}
|
||||
@@ -425,7 +433,7 @@ func startIPNServer(ctx context.Context, logf logger.Logf, logID logid.PublicID)
|
||||
return
|
||||
}
|
||||
}
|
||||
lb, err := getLocalBackend(ctx, logf, logID)
|
||||
lb, err := getLocalBackend(ctx, logf, logID, netMon)
|
||||
if err == nil {
|
||||
logf("got LocalBackend in %v", time.Since(t0).Round(time.Millisecond))
|
||||
srv.SetLocalBackend(lb)
|
||||
@@ -449,19 +457,15 @@ func startIPNServer(ctx context.Context, logf logger.Logf, logID logid.PublicID)
|
||||
return nil
|
||||
}
|
||||
|
||||
func getLocalBackend(ctx context.Context, logf logger.Logf, logID logid.PublicID) (_ *ipnlocal.LocalBackend, retErr error) {
|
||||
linkMon, err := monitor.New(logf)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("monitor.New: %w", err)
|
||||
}
|
||||
func getLocalBackend(ctx context.Context, logf logger.Logf, logID logid.PublicID, netMon *netmon.Monitor) (_ *ipnlocal.LocalBackend, retErr error) {
|
||||
if logPol != nil {
|
||||
logPol.Logtail.SetLinkMonitor(linkMon)
|
||||
logPol.Logtail.SetNetMon(netMon)
|
||||
}
|
||||
|
||||
socksListener, httpProxyListener := mustStartProxyListeners(args.socksAddr, args.httpProxyAddr)
|
||||
|
||||
dialer := &tsdial.Dialer{Logf: logf} // mutated below (before used)
|
||||
e, onlyNetstack, err := createEngine(logf, linkMon, dialer)
|
||||
e, onlyNetstack, err := createEngine(logf, netMon, dialer)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("createEngine: %w", err)
|
||||
}
|
||||
@@ -494,11 +498,13 @@ 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{
|
||||
@@ -508,7 +514,9 @@ 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)
|
||||
@@ -529,7 +537,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"))
|
||||
dnsfallback.SetCachePath(filepath.Join(root, "derpmap.cached.json"), logf)
|
||||
}
|
||||
lb.SetDecompressor(func() (controlclient.Decompressor, error) {
|
||||
return smallzstd.NewDecoder(nil)
|
||||
@@ -546,14 +554,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, linkMon *monitor.Mon, dialer *tsdial.Dialer) (e wgengine.Engine, onlyNetstack bool, err error) {
|
||||
func createEngine(logf logger.Logf, netMon *netmon.Monitor, 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, linkMon, dialer, name)
|
||||
e, onlyNetstack, err = tryEngine(logf, netMon, dialer, name)
|
||||
if err == nil {
|
||||
return e, onlyNetstack, nil
|
||||
}
|
||||
@@ -585,11 +593,11 @@ func handleSubnetsInNetstack() bool {
|
||||
|
||||
var tstunNew = tstun.New
|
||||
|
||||
func tryEngine(logf logger.Logf, linkMon *monitor.Mon, dialer *tsdial.Dialer, name string) (e wgengine.Engine, onlyNetstack bool, err error) {
|
||||
func tryEngine(logf logger.Logf, netMon *netmon.Monitor, dialer *tsdial.Dialer, name string) (e wgengine.Engine, onlyNetstack bool, err error) {
|
||||
conf := wgengine.Config{
|
||||
ListenPort: args.port,
|
||||
LinkMonitor: linkMon,
|
||||
Dialer: dialer,
|
||||
ListenPort: args.port,
|
||||
NetMon: netMon,
|
||||
Dialer: dialer,
|
||||
}
|
||||
|
||||
onlyNetstack = name == "userspace-networking"
|
||||
@@ -628,7 +636,7 @@ func tryEngine(logf logger.Logf, linkMon *monitor.Mon, dialer *tsdial.Dialer, na
|
||||
return e, false, err
|
||||
}
|
||||
|
||||
r, err := router.New(logf, dev, linkMon)
|
||||
r, err := router.New(logf, dev, netMon)
|
||||
if err != nil {
|
||||
dev.Close()
|
||||
return nil, false, fmt.Errorf("creating router: %w", err)
|
||||
@@ -665,7 +673,7 @@ func newDebugMux() *http.ServeMux {
|
||||
|
||||
func servePrometheusMetrics(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
tsweb.VarzHandler(w, r)
|
||||
varz.Handler(w, r)
|
||||
clientmetric.WritePrometheusExpositionFormat(w)
|
||||
}
|
||||
|
||||
|
||||
@@ -45,6 +45,7 @@ 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"
|
||||
@@ -291,8 +292,13 @@ 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)
|
||||
err = startIPNServer(ctx, log.Printf, publicLogID, netMon)
|
||||
if err != nil {
|
||||
log.Fatalf("ipnserver: %v", err)
|
||||
}
|
||||
|
||||
@@ -71,6 +71,9 @@ 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
|
||||
|
||||
@@ -23,6 +23,7 @@ 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() {
|
||||
|
||||
@@ -46,7 +46,7 @@ import (
|
||||
var ControlURL = ipn.DefaultControlURL
|
||||
|
||||
func main() {
|
||||
js.Global().Set("newIPN", js.FuncOf(func(this js.Value, args []js.Value) interface{} {
|
||||
js.Global().Set("newIPN", js.FuncOf(func(this js.Value, args []js.Value) any {
|
||||
if len(args) != 1 {
|
||||
log.Fatal("Usage: newIPN(config)")
|
||||
return nil
|
||||
@@ -123,7 +123,7 @@ func newIPN(jsConfig js.Value) map[string]any {
|
||||
}
|
||||
|
||||
logid := lpc.PublicID
|
||||
srv := ipnserver.New(logf, logid)
|
||||
srv := ipnserver.New(logf, logid, nil /* no netMon */)
|
||||
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) interface{} {
|
||||
"run": js.FuncOf(func(this js.Value, args []js.Value) any {
|
||||
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) interface{} {
|
||||
"login": js.FuncOf(func(this js.Value, args []js.Value) any {
|
||||
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) interface{} {
|
||||
"logout": js.FuncOf(func(this js.Value, args []js.Value) any {
|
||||
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) interface{} {
|
||||
"ssh": js.FuncOf(func(this js.Value, args []js.Value) any {
|
||||
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) interface{} {
|
||||
"fetch": js.FuncOf(func(this js.Value, args []js.Value) any {
|
||||
if len(args) != 1 {
|
||||
log.Printf("Usage: fetch(url)")
|
||||
return nil
|
||||
@@ -334,10 +334,10 @@ func (i *jsIPN) ssh(host, username string, termConfig js.Value) map[string]any {
|
||||
go jsSSHSession.Run()
|
||||
|
||||
return map[string]any{
|
||||
"close": js.FuncOf(func(this js.Value, args []js.Value) interface{} {
|
||||
"close": js.FuncOf(func(this js.Value, args []js.Value) any {
|
||||
return jsSSHSession.Close() != nil
|
||||
}),
|
||||
"resize": js.FuncOf(func(this js.Value, args []js.Value) interface{} {
|
||||
"resize": js.FuncOf(func(this js.Value, args []js.Value) any {
|
||||
rows := args[0].Int()
|
||||
cols := args[1].Int()
|
||||
return jsSSHSession.Resize(rows, cols) != nil
|
||||
@@ -426,7 +426,7 @@ func (s *jsSSHSession) Run() {
|
||||
session.Stdout = termWriter{writeFn}
|
||||
session.Stderr = termWriter{writeFn}
|
||||
|
||||
setReadFn.Invoke(js.FuncOf(func(this js.Value, args []js.Value) interface{} {
|
||||
setReadFn.Invoke(js.FuncOf(func(this js.Value, args []js.Value) any {
|
||||
input := args[0].String()
|
||||
_, err := stdin.Write([]byte(input))
|
||||
if err != nil {
|
||||
@@ -496,7 +496,7 @@ func (i *jsIPN) fetch(url string) js.Value {
|
||||
return map[string]any{
|
||||
"status": res.StatusCode,
|
||||
"statusText": res.Status,
|
||||
"text": js.FuncOf(func(this js.Value, args []js.Value) interface{} {
|
||||
"text": js.FuncOf(func(this js.Value, args []js.Value) any {
|
||||
return makePromise(func() (any, error) {
|
||||
defer res.Body.Close()
|
||||
buf := new(bytes.Buffer)
|
||||
@@ -602,7 +602,7 @@ func generateHostname() string {
|
||||
// f is run on a goroutine and its return value is used to resolve the promise
|
||||
// (or reject it if an error is returned).
|
||||
func makePromise(f func() (any, error)) js.Value {
|
||||
handler := js.FuncOf(func(this js.Value, args []js.Value) interface{} {
|
||||
handler := js.FuncOf(func(this js.Value, args []js.Value) any {
|
||||
resolve := args[0]
|
||||
reject := args[1]
|
||||
go func() {
|
||||
|
||||
@@ -398,7 +398,7 @@ type maxMsgBuffer [maxMessageSize]byte
|
||||
|
||||
// bufPool holds the temporary buffers for Conn.Read & Write.
|
||||
var bufPool = &sync.Pool{
|
||||
New: func() interface{} {
|
||||
New: func() any {
|
||||
return new(maxMsgBuffer)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -121,10 +121,10 @@ func NewNoStart(opts Options) (_ *Auto, err error) {
|
||||
statusFunc: opts.Status,
|
||||
}
|
||||
c.authCtx, c.authCancel = context.WithCancel(context.Background())
|
||||
c.authCtx = sockstats.WithSockStats(c.authCtx, sockstats.LabelControlClientAuto)
|
||||
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)
|
||||
c.mapCtx = sockstats.WithSockStats(c.mapCtx, sockstats.LabelControlClientAuto, opts.Logf)
|
||||
|
||||
c.unregisterHealthWatch = health.RegisterWatcher(direct.ReportHealthChange)
|
||||
return c, nil
|
||||
@@ -244,7 +244,7 @@ func (c *Auto) cancelAuth() {
|
||||
}
|
||||
if !c.closed {
|
||||
c.authCtx, c.authCancel = context.WithCancel(context.Background())
|
||||
c.authCtx = sockstats.WithSockStats(c.authCtx, sockstats.LabelControlClientAuto)
|
||||
c.authCtx = sockstats.WithSockStats(c.authCtx, sockstats.LabelControlClientAuto, c.logf)
|
||||
}
|
||||
c.mu.Unlock()
|
||||
}
|
||||
@@ -255,7 +255,7 @@ func (c *Auto) cancelMapLocked() {
|
||||
}
|
||||
if !c.closed {
|
||||
c.mapCtx, c.mapCancel = context.WithCancel(context.Background())
|
||||
c.mapCtx = sockstats.WithSockStats(c.mapCtx, sockstats.LabelControlClientAuto)
|
||||
c.mapCtx = sockstats.WithSockStats(c.mapCtx, sockstats.LabelControlClientAuto, c.logf)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@ 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"
|
||||
@@ -54,7 +55,6 @@ 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 +67,7 @@ type Direct struct {
|
||||
newDecompressor func() (Decompressor, error)
|
||||
keepAlive bool
|
||||
logf logger.Logf
|
||||
linkMon *monitor.Mon // or nil
|
||||
netMon *netmon.Monitor // or nil
|
||||
discoPubKey key.DiscoPublic
|
||||
getMachinePrivKey func() (key.MachinePrivate, error)
|
||||
debugFlags []string
|
||||
@@ -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
|
||||
LinkMonitor *monitor.Mon // optional link monitor
|
||||
NetMon *netmon.Monitor // optional network 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,8 +211,9 @@ func NewDirect(opts Options) (*Direct, error) {
|
||||
dnsCache := &dnscache.Resolver{
|
||||
Forward: dnscache.Get().Forward, // use default cache's forwarder
|
||||
UseLastGood: true,
|
||||
LookupIPFallback: dnsfallback.Lookup,
|
||||
LookupIPFallback: dnsfallback.MakeLookupFunc(opts.Logf, opts.NetMon),
|
||||
Logf: opts.Logf,
|
||||
NetMon: opts.NetMon,
|
||||
}
|
||||
tr := http.DefaultTransport.(*http.Transport).Clone()
|
||||
tr.Proxy = tshttpproxy.ProxyFromEnvironment
|
||||
@@ -241,7 +242,7 @@ func NewDirect(opts Options) (*Direct, error) {
|
||||
discoPubKey: opts.DiscoPublicKey,
|
||||
debugFlags: opts.DebugFlags,
|
||||
keepSharerAndUserSplit: opts.KeepSharerAndUserSplit,
|
||||
linkMon: opts.LinkMonitor,
|
||||
netMon: opts.NetMon,
|
||||
skipIPForwardingCheck: opts.SkipIPForwardingCheck,
|
||||
pinger: opts.Pinger,
|
||||
popBrowser: opts.PopBrowserURL,
|
||||
@@ -871,8 +872,8 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, readOnly bool
|
||||
ReadOnly: readOnly && !allowStream,
|
||||
}
|
||||
var extraDebugFlags []string
|
||||
if hi != nil && c.linkMon != nil && !c.skipIPForwardingCheck &&
|
||||
ipForwardingBroken(hi.RoutableIPs, c.linkMon.InterfaceState()) {
|
||||
if hi != nil && c.netMon != nil && !c.skipIPForwardingCheck &&
|
||||
ipForwardingBroken(hi.RoutableIPs, c.netMon.InterfaceState()) {
|
||||
extraDebugFlags = append(extraDebugFlags, "warn-ip-forwarding-off")
|
||||
}
|
||||
if health.RouterHealth() != nil {
|
||||
@@ -1508,7 +1509,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, dp)
|
||||
nc, err := NewNoiseClient(k, serverNoiseKey, c.serverURL, c.dialer, c.logf, c.netMon, dp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -19,9 +19,11 @@ 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"
|
||||
@@ -167,6 +169,9 @@ 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
|
||||
@@ -177,8 +182,9 @@ 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, dialPlan func() *tailcfg.ControlDialPlan) (*NoiseClient, error) {
|
||||
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) {
|
||||
u, err := url.Parse(serverURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -207,6 +213,8 @@ 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
|
||||
@@ -366,6 +374,8 @@ 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
|
||||
|
||||
@@ -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)
|
||||
nc, err := NewNoiseClient(clientPrivate, serverPrivate.Public(), hs.URL, dialer, nil, nil, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -273,7 +273,7 @@ func (a *Dialer) dialHost(ctx context.Context, addr netip.Addr) (*ClientConn, er
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
|
||||
ctx = sockstats.WithSockStats(ctx, sockstats.LabelControlClientDialer)
|
||||
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
|
||||
@@ -389,13 +389,15 @@ func (a *Dialer) tryURLUpgrade(ctx context.Context, u *url.URL, addr netip.Addr,
|
||||
SingleHostStaticResult: []netip.Addr{addr},
|
||||
SingleHost: u.Hostname(),
|
||||
Logf: a.Logf, // not a.logf method; we want to propagate nil-ness
|
||||
NetMon: a.NetMon,
|
||||
}
|
||||
} else {
|
||||
dns = &dnscache.Resolver{
|
||||
Forward: dnscache.Get().Forward,
|
||||
LookupIPFallback: dnsfallback.Lookup,
|
||||
LookupIPFallback: dnsfallback.MakeLookupFunc(a.logf, a.NetMon),
|
||||
UseLastGood: true,
|
||||
Logf: a.Logf, // not a.logf method; we want to propagate nil-ness
|
||||
NetMon: a.NetMon,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"time"
|
||||
|
||||
"tailscale.com/net/dnscache"
|
||||
"tailscale.com/net/netmon"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/logger"
|
||||
@@ -70,6 +71,8 @@ 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.
|
||||
|
||||
16
derp/derp.go
16
derp/derp.go
@@ -77,8 +77,11 @@ 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.
|
||||
framePeerGone = frameType(0x08) // 32B pub key of peer that's gone
|
||||
// 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
|
||||
|
||||
// framePeerPresent is like framePeerGone, but for other
|
||||
// members of the DERP region when they're meshed up together.
|
||||
@@ -116,6 +119,15 @@ 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 {
|
||||
|
||||
@@ -348,9 +348,12 @@ type ReceivedPacket struct {
|
||||
func (ReceivedPacket) msg() {}
|
||||
|
||||
// PeerGoneMessage is a ReceivedMessage that indicates that the client
|
||||
// identified by the underlying public key had previously sent you a
|
||||
// packet but has now disconnected from the server.
|
||||
type PeerGoneMessage key.NodePublic
|
||||
// identified by the underlying public key is not connected to this
|
||||
// server.
|
||||
type PeerGoneMessage struct {
|
||||
Peer key.NodePublic
|
||||
Reason PeerGoneReasonType
|
||||
}
|
||||
|
||||
func (PeerGoneMessage) msg() {}
|
||||
|
||||
@@ -524,7 +527,15 @@ func (c *Client) recvTimeout(timeout time.Duration) (m ReceivedMessage, err erro
|
||||
c.logf("[unexpected] dropping short peerGone frame from DERP server")
|
||||
continue
|
||||
}
|
||||
pg := PeerGoneMessage(key.NodePublicFromRaw32(mem.B(b[:keyLen])))
|
||||
// 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,
|
||||
}
|
||||
return pg, nil
|
||||
|
||||
case framePeerPresent:
|
||||
|
||||
@@ -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,7 +122,8 @@ type Server struct {
|
||||
_ align64
|
||||
packetsForwardedOut expvar.Int
|
||||
packetsForwardedIn expvar.Int
|
||||
peerGoneFrames expvar.Int // number of peer gone frames sent
|
||||
peerGoneDisconnectedFrames expvar.Int // number of peer disconnected frames sent
|
||||
peerGoneNotHereFrames expvar.Int // number of peer not here frames sent
|
||||
gotPing expvar.Int // number of ping frames from client
|
||||
sentPong expvar.Int // number of pong frames enqueued to client
|
||||
accepts expvar.Int
|
||||
@@ -324,7 +325,8 @@ 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"),
|
||||
s.packetsDroppedReason.Get("gone_disconnected"),
|
||||
s.packetsDroppedReason.Get("gone_not_here"),
|
||||
s.packetsDroppedReason.Get("queue_head"),
|
||||
s.packetsDroppedReason.Get("queue_tail"),
|
||||
s.packetsDroppedReason.Get("write_error"),
|
||||
@@ -615,13 +617,26 @@ func (s *Server) notePeerGoneFromRegionLocked(key key.NodePublic) {
|
||||
}
|
||||
set.ForeachClient(func(peer *sclient) {
|
||||
if peer.connNum == connNum {
|
||||
go peer.requestPeerGoneWrite(key)
|
||||
go peer.requestPeerGoneWrite(key, PeerGoneReasonDisconnected)
|
||||
}
|
||||
})
|
||||
}
|
||||
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")
|
||||
@@ -686,8 +701,9 @@ 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 key.NodePublic),
|
||||
peerGone: make(chan peerGoneMsg),
|
||||
canMesh: clientInfo.MeshKey != "" && clientInfo.MeshKey == s.meshKey,
|
||||
peerGoneLim: rate.NewLimiter(rate.Every(time.Second), 3),
|
||||
}
|
||||
|
||||
if c.canMesh {
|
||||
@@ -887,6 +903,8 @@ 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
|
||||
@@ -952,6 +970,8 @@ 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)
|
||||
@@ -981,7 +1001,7 @@ type dropReason int
|
||||
const (
|
||||
dropReasonUnknownDest dropReason = iota // unknown destination pubkey
|
||||
dropReasonUnknownDestOnFwd // unknown destination pubkey on a derp-forwarded packet
|
||||
dropReasonGone // destination tailscaled disconnected before we could send
|
||||
dropReasonGoneDisconnected // destination tailscaled disconnected before we could send
|
||||
dropReasonQueueHead // destination queue is full, dropped packet at queue head
|
||||
dropReasonQueueTail // destination queue is full, dropped packet at queue tail
|
||||
dropReasonWriteError // OS write() failed
|
||||
@@ -1023,7 +1043,7 @@ 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, dropReasonGone)
|
||||
s.recordDrop(p.bs, c.key, dstKey, dropReasonGoneDisconnected)
|
||||
dst.debug("sendPkt attempt %d dropped, dst gone", attempt)
|
||||
return nil
|
||||
default:
|
||||
@@ -1052,11 +1072,14 @@ func (c *sclient) sendPkt(dst *sclient, p pkt) error {
|
||||
}
|
||||
|
||||
// requestPeerGoneWrite sends a request to write a "peer gone" frame
|
||||
// that the provided peer has disconnected. It blocks until either the
|
||||
// with an explanation of why it is gone. It blocks until either the
|
||||
// write request is scheduled, or the client has closed.
|
||||
func (c *sclient) requestPeerGoneWrite(peer key.NodePublic) {
|
||||
func (c *sclient) requestPeerGoneWrite(peer key.NodePublic, reason PeerGoneReasonType) {
|
||||
select {
|
||||
case c.peerGone <- peer:
|
||||
case c.peerGone <- peerGoneMsg{
|
||||
peer: peer,
|
||||
reason: reason,
|
||||
}:
|
||||
case <-c.done:
|
||||
}
|
||||
}
|
||||
@@ -1270,25 +1293,20 @@ 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 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
|
||||
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
|
||||
|
||||
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
|
||||
connectedAt time.Time
|
||||
@@ -1304,6 +1322,11 @@ 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
|
||||
@@ -1327,6 +1350,12 @@ 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
|
||||
@@ -1381,9 +1410,9 @@ func (c *sclient) sendLoop(ctx context.Context) error {
|
||||
for {
|
||||
select {
|
||||
case pkt := <-c.sendQueue:
|
||||
c.s.recordDrop(pkt.bs, pkt.src, c.key, dropReasonGone)
|
||||
c.s.recordDrop(pkt.bs, pkt.src, c.key, dropReasonGoneDisconnected)
|
||||
case pkt := <-c.discoSendQueue:
|
||||
c.s.recordDrop(pkt.bs, pkt.src, c.key, dropReasonGone)
|
||||
c.s.recordDrop(pkt.bs, pkt.src, c.key, dropReasonGoneDisconnected)
|
||||
default:
|
||||
return
|
||||
}
|
||||
@@ -1404,8 +1433,8 @@ func (c *sclient) sendLoop(ctx context.Context) error {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
case peer := <-c.peerGone:
|
||||
werr = c.sendPeerGone(peer)
|
||||
case msg := <-c.peerGone:
|
||||
werr = c.sendPeerGone(msg.peer, msg.reason)
|
||||
continue
|
||||
case <-c.meshUpdate:
|
||||
werr = c.sendMeshUpdates()
|
||||
@@ -1436,8 +1465,8 @@ func (c *sclient) sendLoop(ctx context.Context) error {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
case peer := <-c.peerGone:
|
||||
werr = c.sendPeerGone(peer)
|
||||
case msg := <-c.peerGone:
|
||||
werr = c.sendPeerGone(msg.peer, msg.reason)
|
||||
case <-c.meshUpdate:
|
||||
werr = c.sendMeshUpdates()
|
||||
continue
|
||||
@@ -1478,13 +1507,22 @@ func (c *sclient) sendPong(data [8]byte) error {
|
||||
}
|
||||
|
||||
// sendPeerGone sends a peerGone frame, without flushing.
|
||||
func (c *sclient) sendPeerGone(peer key.NodePublic) error {
|
||||
c.s.peerGoneFrames.Add(1)
|
||||
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)
|
||||
}
|
||||
c.setWriteDeadline()
|
||||
if err := writeFrameHeader(c.bw.bw(), framePeerGone, keyLen); err != nil {
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
_, err := c.bw.Write(peer.AppendTo(nil))
|
||||
|
||||
_, err := c.bw.Write(data)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1515,7 +1553,7 @@ func (c *sclient) sendMeshUpdates() error {
|
||||
if pcs.present {
|
||||
err = c.sendPeerPresent(pcs.peer)
|
||||
} else {
|
||||
err = c.sendPeerGone(pcs.peer)
|
||||
err = c.sendPeerGone(pcs.peer, PeerGoneReasonDisconnected)
|
||||
}
|
||||
if err != nil {
|
||||
// Shouldn't happen, though, as we're writing
|
||||
@@ -1756,7 +1794,8 @@ 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_frames", &s.peerGoneFrames)
|
||||
m.Set("peer_gone_disconnected_frames", &s.peerGoneDisconnectedFrames)
|
||||
m.Set("peer_gone_not_here_frames", &s.peerGoneNotHereFrames)
|
||||
m.Set("packets_forwarded_out", &s.packetsForwardedOut)
|
||||
m.Set("packets_forwarded_in", &s.packetsForwardedIn)
|
||||
m.Set("multiforwarder_created", &s.multiForwarderCreated)
|
||||
|
||||
@@ -25,6 +25,7 @@ import (
|
||||
|
||||
"go4.org/mem"
|
||||
"golang.org/x/time/rate"
|
||||
"tailscale.com/disco"
|
||||
"tailscale.com/net/memnet"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/logger"
|
||||
@@ -105,7 +106,8 @@ func TestSendRecv(t *testing.T) {
|
||||
t.Logf("Connected client %d.", i)
|
||||
}
|
||||
|
||||
var peerGoneCount expvar.Int
|
||||
var peerGoneCountDisconnected expvar.Int
|
||||
var peerGoneCountNotHere expvar.Int
|
||||
|
||||
t.Logf("Starting read loops")
|
||||
for i := 0; i < numClients; i++ {
|
||||
@@ -121,7 +123,14 @@ func TestSendRecv(t *testing.T) {
|
||||
t.Errorf("unexpected message type %T", m)
|
||||
continue
|
||||
case PeerGoneMessage:
|
||||
peerGoneCount.Add(1)
|
||||
switch m.Reason {
|
||||
case PeerGoneReasonDisconnected:
|
||||
peerGoneCountDisconnected.Add(1)
|
||||
case PeerGoneReasonNotHere:
|
||||
peerGoneCountNotHere.Add(1)
|
||||
default:
|
||||
t.Errorf("unexpected PeerGone reason %v", m.Reason)
|
||||
}
|
||||
case ReceivedPacket:
|
||||
if m.Source.IsZero() {
|
||||
t.Errorf("zero Source address in ReceivedPacket")
|
||||
@@ -171,7 +180,19 @@ func TestSendRecv(t *testing.T) {
|
||||
var got int64
|
||||
dl := time.Now().Add(5 * time.Second)
|
||||
for time.Now().Before(dl) {
|
||||
if got = peerGoneCount.Value(); got == want {
|
||||
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 {
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -194,6 +215,30 @@ 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)
|
||||
@@ -595,10 +640,14 @@ func (tc *testClient) wantGone(t *testing.T, peer key.NodePublic) {
|
||||
}
|
||||
switch m := m.(type) {
|
||||
case PeerGoneMessage:
|
||||
got := key.NodePublic(m)
|
||||
got := key.NodePublic(m.Peer)
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -31,6 +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"
|
||||
@@ -55,6 +56,7 @@ 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:
|
||||
@@ -88,11 +90,13 @@ func (c *Client) String() string {
|
||||
|
||||
// NewRegionClient returns a new DERP-over-HTTP client. It connects lazily.
|
||||
// To trigger a connection, use Connect.
|
||||
func NewRegionClient(privateKey key.NodePrivate, logf logger.Logf, getRegion func() *tailcfg.DERPRegion) *Client {
|
||||
// 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 {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
c := &Client{
|
||||
privateKey: privateKey,
|
||||
logf: logf,
|
||||
netMon: netMon,
|
||||
getRegion: getRegion,
|
||||
ctx: ctx,
|
||||
cancelCtx: cancel,
|
||||
@@ -174,6 +178,10 @@ 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()
|
||||
@@ -185,6 +193,10 @@ func (c *Client) useHTTPS() bool {
|
||||
if c.url != nil && c.url.Scheme == "http" {
|
||||
return false
|
||||
}
|
||||
if debugUseDERPHTTP() {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -200,7 +212,11 @@ func (c *Client) urlString(node *tailcfg.DERPNode) string {
|
||||
if c.url != nil {
|
||||
return c.url.String()
|
||||
}
|
||||
return fmt.Sprintf("https://%s/derp", node.HostName)
|
||||
proto := "https"
|
||||
if debugUseDERPHTTP() {
|
||||
proto = "http"
|
||||
}
|
||||
return fmt.Sprintf("%s://%s/derp", proto, node.HostName)
|
||||
}
|
||||
|
||||
// AddressFamilySelector decides whether IPv6 is preferred for
|
||||
@@ -480,7 +496,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)
|
||||
dialer := netns.NewDialer(c.logf, c.netMon)
|
||||
|
||||
if c.DNSCache != nil {
|
||||
ip, _, _, err := c.DNSCache.LookupIP(ctx, host)
|
||||
@@ -575,7 +591,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).DialContext(ctx, proto, addr)
|
||||
return netns.NewDialer(c.logf, c.netMon).DialContext(ctx, proto, addr)
|
||||
}
|
||||
|
||||
// shouldDialProto reports whether an explicitly provided IPv4 or IPv6
|
||||
@@ -620,7 +636,7 @@ 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)
|
||||
ctx = sockstats.WithSockStats(ctx, sockstats.LabelDERPHTTPClient, c.logf)
|
||||
|
||||
nwait := 0
|
||||
startDial := func(dstPrimary, proto string) {
|
||||
|
||||
@@ -128,7 +128,17 @@ func (c *Client) RunWatchConnectionLoop(ctx context.Context, ignoreServerKey key
|
||||
case derp.PeerPresentMessage:
|
||||
updatePeer(key.NodePublic(m), true)
|
||||
case derp.PeerGoneMessage:
|
||||
updatePeer(key.NodePublic(m), false)
|
||||
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)
|
||||
default:
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -13,16 +13,16 @@ func _() {
|
||||
var x [1]struct{}
|
||||
_ = x[dropReasonUnknownDest-0]
|
||||
_ = x[dropReasonUnknownDestOnFwd-1]
|
||||
_ = x[dropReasonGone-2]
|
||||
_ = x[dropReasonGoneDisconnected-2]
|
||||
_ = x[dropReasonQueueHead-3]
|
||||
_ = x[dropReasonQueueTail-4]
|
||||
_ = x[dropReasonWriteError-5]
|
||||
_ = x[dropReasonDupClient-6]
|
||||
}
|
||||
|
||||
const _dropReason_name = "UnknownDestUnknownDestOnFwdGoneQueueHeadQueueTailWriteErrorDupClient"
|
||||
const _dropReason_name = "UnknownDestUnknownDestOnFwdGoneDisconnectedQueueHeadQueueTailWriteErrorDupClient"
|
||||
|
||||
var _dropReason_index = [...]uint8{0, 11, 27, 31, 40, 49, 59, 68}
|
||||
var _dropReason_index = [...]uint8{0, 11, 27, 43, 52, 61, 71, 80}
|
||||
|
||||
func (i dropReason) String() string {
|
||||
if i < 0 || i >= dropReason(len(_dropReason_index)-1) {
|
||||
|
||||
56
doctor/permissions/permissions.go
Normal file
56
doctor/permissions/permissions.go
Normal file
@@ -0,0 +1,56 @@
|
||||
// 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()
|
||||
}
|
||||
23
doctor/permissions/permissions_bsd.go
Normal file
23
doctor/permissions/permissions_bsd.go
Normal file
@@ -0,0 +1,23 @@
|
||||
// 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
|
||||
}
|
||||
62
doctor/permissions/permissions_linux.go
Normal file
62
doctor/permissions/permissions_linux.go
Normal file
@@ -0,0 +1,62 @@
|
||||
// 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
|
||||
}
|
||||
17
doctor/permissions/permissions_other.go
Normal file
17
doctor/permissions/permissions_other.go
Normal file
@@ -0,0 +1,17 @@
|
||||
// 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
|
||||
}
|
||||
12
doctor/permissions/permissions_test.go
Normal file
12
doctor/permissions/permissions_test.go
Normal file
@@ -0,0 +1,12 @@
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
@@ -330,6 +330,46 @@ 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") }
|
||||
|
||||
84
envknob/logknob/logknob.go
Normal file
84
envknob/logknob/logknob.go
Normal file
@@ -0,0 +1,84 @@
|
||||
// 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()
|
||||
}
|
||||
102
envknob/logknob/logknob_test.go
Normal file
102
envknob/logknob/logknob_test.go
Normal file
@@ -0,0 +1,102 @@
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
@@ -115,4 +115,4 @@
|
||||
in
|
||||
flake-utils.lib.eachDefaultSystem (system: flakeForSystem nixpkgs system);
|
||||
}
|
||||
# nix-direnv cache busting line: sha256-LIvaxSo+4LuHUk8DIZ27IaRQwaDnjW6Jwm5AEc/V95A=
|
||||
# nix-direnv cache busting line: sha256-lirn07XE3JOS6oiwZBMwxzywkbXHowOJUMWWLrZtccY=
|
||||
|
||||
227
go.mod
227
go.mod
@@ -21,13 +21,14 @@ 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.0
|
||||
github.com/frankban/quicktest v1.14.3
|
||||
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
|
||||
@@ -42,8 +43,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.12
|
||||
github.com/mattn/go-isatty v0.0.14
|
||||
github.com/mattn/go-colorable v0.1.13
|
||||
github.com/mattn/go-isatty v0.0.17
|
||||
github.com/mdlayher/genetlink v1.2.0
|
||||
github.com/mdlayher/netlink v1.7.1
|
||||
github.com/mdlayher/sdnotify v1.0.0
|
||||
@@ -52,6 +53,8 @@ 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
|
||||
@@ -61,29 +64,29 @@ 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-20221219190806-4fa124729667
|
||||
github.com/tailscale/wireguard-go v0.0.0-20230410165232-af172621b4dd
|
||||
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.21.0
|
||||
go.uber.org/zap v1.24.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.7.0
|
||||
golang.org/x/net v0.7.0
|
||||
golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5
|
||||
golang.org/x/mod v0.9.0
|
||||
golang.org/x/net v0.8.0
|
||||
golang.org/x/oauth2 v0.5.0
|
||||
golang.org/x/sync v0.1.0
|
||||
golang.org/x/sys v0.5.0
|
||||
golang.org/x/term v0.5.0
|
||||
golang.org/x/sys v0.6.0
|
||||
golang.org/x/term v0.6.0
|
||||
golang.org/x/time v0.0.0-20220609170525-579cf78fd858
|
||||
golang.org/x/tools v0.4.1-0.20221208213631-3f74d914ae6d
|
||||
golang.org/x/tools v0.7.0
|
||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2
|
||||
golang.zx2c4.com/wireguard/windows v0.5.3
|
||||
gvisor.dev/gvisor v0.0.0-20221203005347-703fd9b7fbc0
|
||||
honnef.co/go/tools v0.4.0-0.dev.0.20230130122044-c30b15588105
|
||||
gvisor.dev/gvisor v0.0.0-20230328175328-162ed5ef888d
|
||||
honnef.co/go/tools v0.4.3
|
||||
inet.af/peercred v0.0.0-20210906144145-0893ea02156a
|
||||
inet.af/tcpproxy v0.0.0-20221017015627-91f861402626
|
||||
inet.af/wf v0.0.0-20220728202103-50d96caab2f6
|
||||
@@ -97,24 +100,28 @@ require (
|
||||
)
|
||||
|
||||
require (
|
||||
4d63.com/gochecknoglobals v0.1.0 // indirect
|
||||
4d63.com/gocheckcompilerdirectives v1.2.1 // indirect
|
||||
4d63.com/gochecknoglobals v0.2.1 // indirect
|
||||
filippo.io/edwards25519 v1.0.0-rc.1 // indirect
|
||||
github.com/Antonboom/errname v0.1.5 // indirect
|
||||
github.com/Antonboom/nilnil v0.1.0 // 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/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.0.1 // indirect
|
||||
github.com/OpenPeeDeeP/depguard v1.1.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/ashanbrown/forbidigo v1.2.0 // indirect
|
||||
github.com/ashanbrown/makezero v0.0.0-20210520155254-b6261585ddde // 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/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
|
||||
@@ -130,35 +137,38 @@ 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.5.0 // indirect
|
||||
github.com/bombsimon/wsl/v3 v3.3.0 // indirect
|
||||
github.com/breml/bidichk v0.2.1 // 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/butuzov/ireturn v0.1.1 // indirect
|
||||
github.com/cavaliercoder/go-cpio v0.0.0-20180626203310-925f9528c45e // 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/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/cloudflare/circl v1.1.0 // indirect
|
||||
github.com/containerd/stargz-snapshotter/estargz v0.11.4 // indirect
|
||||
github.com/daixiang0/gci v0.2.9 // indirect
|
||||
github.com/curioswitch/go-reassign v0.2.0 // indirect
|
||||
github.com/daixiang0/gci v0.10.1 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/denis-tingajkin/go-header v0.4.2 // indirect
|
||||
github.com/denis-tingaikin/go-header v0.4.3 // 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.3 // indirect
|
||||
github.com/esimonov/ifshort v1.0.4 // 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.13.0 // indirect
|
||||
github.com/fatih/color v1.15.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.3.1 // indirect
|
||||
github.com/fzipp/gocyclo v0.6.0 // indirect
|
||||
github.com/gliderlabs/ssh v0.3.3 // indirect
|
||||
github.com/go-critic/go-critic v0.6.1 // indirect
|
||||
github.com/go-critic/go-critic v0.7.0 // 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
|
||||
@@ -166,34 +176,33 @@ 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.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/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/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-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/go-misc v0.0.0-20220329215616-d24fe342adfe // indirect
|
||||
github.com/golangci/gofmt v0.0.0-20220901101216-f2edd75033f2 // 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.3.5 // indirect
|
||||
github.com/golangci/revgrep v0.0.0-20210930125155-c22e5001d4f2 // indirect
|
||||
github.com/golangci/misspell v0.4.0 // indirect
|
||||
github.com/golangci/revgrep v0.0.0-20220804021717-745bb2f7c2e6 // 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-20210914165742-4cc7213b9bc8 // indirect
|
||||
github.com/gordonklaus/ineffassign v0.0.0-20230107090616-13ace0543b28 // 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
|
||||
@@ -202,10 +211,12 @@ 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.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.0.1 // 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
|
||||
@@ -213,105 +224,117 @@ 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.0.0-20210922140945-27e0a5d4dee2 // indirect
|
||||
github.com/julz/importas v0.1.0 // indirect
|
||||
github.com/junk1tm/musttag v0.5.0 // indirect
|
||||
github.com/kevinburke/ssh_config v1.1.0 // indirect
|
||||
github.com/kisielk/errcheck v1.6.0 // indirect
|
||||
github.com/kisielk/errcheck v1.6.3 // 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.0 // indirect
|
||||
github.com/kr/pretty v0.3.1 // indirect
|
||||
github.com/kr/text v0.2.0 // 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/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/mailru/easyjson v0.7.6 // indirect
|
||||
github.com/maratori/testpackage v1.0.1 // indirect
|
||||
github.com/matoous/godox v0.0.0-20210227103229-6504466cf951 // 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/mattn/go-runewidth v0.0.13 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
|
||||
github.com/mbilski/exhaustivestruct v1.2.0 // indirect
|
||||
github.com/mdlayher/socket v0.4.0 // indirect
|
||||
github.com/mgechev/dots v0.0.0-20210922191527-e955255bf517 // indirect
|
||||
github.com/mgechev/revive v1.1.2 // indirect
|
||||
github.com/mgechev/revive v1.3.1 // indirect
|
||||
github.com/mitchellh/copystructure v1.2.0 // indirect
|
||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||
github.com/mitchellh/mapstructure v1.4.3 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // 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.2.1 // indirect
|
||||
github.com/moricho/tparallel v0.3.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.7.11 // indirect
|
||||
github.com/nishanths/predeclared v0.2.1 // 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/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.4 // indirect
|
||||
github.com/phayes/checkstyle v0.0.0-20170904204023-bfd46e6a821d // indirect
|
||||
github.com/pelletier/go-toml v1.9.5 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.0.5 // 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 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/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/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.8.1-0.20211023094830-115ce09fd6b4 // indirect
|
||||
github.com/ryancurrah/gomodguard v1.2.3 // indirect
|
||||
github.com/ryanrolds/sqlclosecheck v0.3.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/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.9.3 // indirect
|
||||
github.com/securego/gosec/v2 v2.15.0 // 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.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/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/spf13/jwalterweatherman v1.1.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/spf13/viper v1.9.0 // indirect
|
||||
github.com/spf13/viper v1.12.0 // indirect
|
||||
github.com/ssgreg/nlreturn/v2 v2.2.1 // 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/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/tetafro/godot v1.4.11 // 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/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/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.4 // indirect
|
||||
github.com/uudashr/gocognit v1.0.5 // indirect
|
||||
github.com/ultraware/whitespace v0.0.5 // indirect
|
||||
github.com/uudashr/gocognit v1.0.6 // 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/yeya24/promlinter v0.1.0 // 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
|
||||
go.uber.org/atomic v1.7.0 // indirect
|
||||
go.uber.org/multierr v1.6.0 // indirect
|
||||
golang.org/x/exp/typeparams v0.0.0-20221208152030-732eee02a75a // 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.7.0 // indirect
|
||||
golang.org/x/text v0.8.0 // indirect
|
||||
gomodules.xyz/jsonpatch/v2 v2.2.0 // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
google.golang.org/protobuf v1.28.0 // indirect
|
||||
google.golang.org/protobuf v1.30.0 // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gopkg.in/ini.v1 v1.66.2 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // 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
|
||||
@@ -321,10 +344,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.2.0 // indirect
|
||||
mvdan.cc/gofumpt v0.4.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-20211002134041-24922b6997ca // indirect
|
||||
mvdan.cc/unparam v0.0.0-20221223090309-7455f1af531d // indirect
|
||||
sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect
|
||||
)
|
||||
|
||||
@@ -1 +1 @@
|
||||
sha256-LIvaxSo+4LuHUk8DIZ27IaRQwaDnjW6Jwm5AEc/V95A=
|
||||
sha256-lirn07XE3JOS6oiwZBMwxzywkbXHowOJUMWWLrZtccY=
|
||||
|
||||
@@ -1 +1 @@
|
||||
568add9f5d780e86f8b3e7002fd7b4a7479005fa
|
||||
ddff070c02790cb571006e820e58cce9627569cf
|
||||
|
||||
@@ -405,7 +405,7 @@ func DisabledEtcAptSource() bool {
|
||||
return false
|
||||
}
|
||||
mod := fi.ModTime()
|
||||
if c, ok := etcAptSrcCache.Load().(etcAptSrcResult); ok && c.mod == mod {
|
||||
if c, ok := etcAptSrcCache.Load().(etcAptSrcResult); ok && c.mod.Equal(mod) {
|
||||
return c.disabled
|
||||
}
|
||||
f, err := os.Open(path)
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
package tooldeps
|
||||
|
||||
import (
|
||||
_ "github.com/golangci/golangci-lint/cmd/golangci-lint"
|
||||
_ "github.com/tailscale/depaware/depaware"
|
||||
_ "golang.org/x/tools/cmd/goimports"
|
||||
)
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"time"
|
||||
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/net/sockstats"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/util/clientmetric"
|
||||
"tailscale.com/util/goroutines"
|
||||
@@ -89,8 +90,13 @@ func (b *LocalBackend) handleC2N(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
if b.sockstatLogger == nil {
|
||||
http.Error(w, "no sockstatLogger", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
b.sockstatLogger.Flush()
|
||||
fmt.Fprintln(w, b.sockstatLogger.LogID())
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -158,7 +158,7 @@ func (em *expiryManager) nextPeerExpiry(nm *netmap.NetworkMap, localNow time.Tim
|
||||
// nextExpiry being zero is a sentinel that we haven't yet set
|
||||
// an expiry; otherwise, only update if this node's expiry is
|
||||
// sooner than the currently-stored one (since we want the
|
||||
// soonest-occuring expiry time).
|
||||
// soonest-occurring expiry time).
|
||||
if nextExpiry.IsZero() || peer.KeyExpiry.Before(nextExpiry) {
|
||||
nextExpiry = peer.KeyExpiry
|
||||
}
|
||||
|
||||
@@ -243,7 +243,7 @@ func TestNextPeerExpiry(t *testing.T) {
|
||||
em := newExpiryManager(t.Logf)
|
||||
em.timeNow = func() time.Time { return now }
|
||||
got := em.nextPeerExpiry(tt.netmap, now)
|
||||
if got != tt.want {
|
||||
if !got.Equal(tt.want) {
|
||||
t.Errorf("got %q, want %q", got.Format(time.RFC3339), tt.want.Format(time.RFC3339))
|
||||
} else if !got.IsZero() && got.Before(now) {
|
||||
t.Errorf("unexpectedly got expiry %q before now %q", got.Format(time.RFC3339), now.Format(time.RFC3339))
|
||||
@@ -269,7 +269,7 @@ func TestNextPeerExpiry(t *testing.T) {
|
||||
}
|
||||
got := em.nextPeerExpiry(nm, now)
|
||||
want := now.Add(30 * time.Second)
|
||||
if got != want {
|
||||
if !got.Equal(want) {
|
||||
t.Errorf("got %q, want %q", got.Format(time.RFC3339), want.Format(time.RFC3339))
|
||||
}
|
||||
})
|
||||
|
||||
@@ -35,6 +35,7 @@ import (
|
||||
"tailscale.com/client/tailscale/apitype"
|
||||
"tailscale.com/control/controlclient"
|
||||
"tailscale.com/doctor"
|
||||
"tailscale.com/doctor/permissions"
|
||||
"tailscale.com/doctor/routetable"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/health"
|
||||
@@ -141,7 +142,7 @@ type LocalBackend struct {
|
||||
store ipn.StateStore
|
||||
dialer *tsdial.Dialer // non-nil
|
||||
backendLogID logid.PublicID
|
||||
unregisterLinkMon func()
|
||||
unregisterNetMon func()
|
||||
unregisterHealthWatch func()
|
||||
portpoll *portlist.Poller // may be nil
|
||||
portpollOnce sync.Once // guards starting readPoller
|
||||
@@ -275,6 +276,9 @@ func NewLocalBackend(logf logger.Logf, logID logid.PublicID, store ipn.StateStor
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if sds, ok := store.(ipn.StateStoreDialerSetter); ok {
|
||||
sds.SetDialer(dialer.SystemDial)
|
||||
}
|
||||
|
||||
hi := hostinfo.New()
|
||||
logf.JSON(1, "Hostinfo", hi)
|
||||
@@ -309,12 +313,13 @@ func NewLocalBackend(logf logger.Logf, logID logid.PublicID, store ipn.StateStor
|
||||
loginFlags: loginFlags,
|
||||
}
|
||||
|
||||
// for now, only log sockstats on unstable builds
|
||||
if version.IsUnstableBuild() {
|
||||
b.sockstatLogger, err = sockstatlog.NewLogger(logpolicy.LogsDir(logf), logf, logID)
|
||||
if err != nil {
|
||||
log.Printf("error setting up sockstat logger: %v", err)
|
||||
}
|
||||
b.sockstatLogger, err = sockstatlog.NewLogger(logpolicy.LogsDir(logf), logf, logID, e.GetNetMon())
|
||||
if err != nil {
|
||||
log.Printf("error setting up sockstat logger: %v", err)
|
||||
}
|
||||
// Enable sockstats logs only on unstable builds
|
||||
if version.IsUnstableBuild() && b.sockstatLogger != nil {
|
||||
b.sockstatLogger.SetLoggingEnabled(true)
|
||||
}
|
||||
|
||||
// Default filter blocks everything and logs nothing, until Start() is called.
|
||||
@@ -325,12 +330,12 @@ func NewLocalBackend(logf logger.Logf, logID logid.PublicID, store ipn.StateStor
|
||||
b.statusChanged = sync.NewCond(&b.statusLock)
|
||||
b.e.SetStatusCallback(b.setWgengineStatus)
|
||||
|
||||
linkMon := e.GetLinkMonitor()
|
||||
b.prevIfState = linkMon.InterfaceState()
|
||||
netMon := e.GetNetMon()
|
||||
b.prevIfState = netMon.InterfaceState()
|
||||
// Call our linkChange code once with the current state, and
|
||||
// then also whenever it changes:
|
||||
b.linkChange(false, linkMon.InterfaceState())
|
||||
b.unregisterLinkMon = linkMon.RegisterChangeCallback(b.linkChange)
|
||||
b.linkChange(false, netMon.InterfaceState())
|
||||
b.unregisterNetMon = netMon.RegisterChangeCallback(b.linkChange)
|
||||
|
||||
b.unregisterHealthWatch = health.RegisterWatcher(b.onHealthChange)
|
||||
|
||||
@@ -365,6 +370,7 @@ type componentLogState struct {
|
||||
|
||||
var debuggableComponents = []string{
|
||||
"magicsock",
|
||||
"sockstats",
|
||||
}
|
||||
|
||||
func componentStateKey(component string) ipn.StateKey {
|
||||
@@ -377,6 +383,7 @@ func componentStateKey(component string) ipn.StateKey {
|
||||
// The following components are recognized:
|
||||
//
|
||||
// - magicsock
|
||||
// - sockstats
|
||||
func (b *LocalBackend) SetComponentDebugLogging(component string, until time.Time) error {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
@@ -389,6 +396,17 @@ func (b *LocalBackend) SetComponentDebugLogging(component string, until time.Tim
|
||||
return err
|
||||
}
|
||||
setEnabled = mc.SetDebugLoggingEnabled
|
||||
case "sockstats":
|
||||
if b.sockstatLogger != nil {
|
||||
setEnabled = func(v bool) {
|
||||
b.sockstatLogger.SetLoggingEnabled(v)
|
||||
// Flush (and thus upload) logs when the enabled period ends,
|
||||
// so that the logs are available for debugging.
|
||||
if !v {
|
||||
b.sockstatLogger.Flush()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if setEnabled == nil || !slices.Contains(debuggableComponents, component) {
|
||||
return fmt.Errorf("unknown component %q", component)
|
||||
@@ -420,7 +438,7 @@ func (b *LocalBackend) SetComponentDebugLogging(component string, until time.Tim
|
||||
// unchanged when the timer actually fires.
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
if ls := b.componentLogUntil[component]; ls.until == until {
|
||||
if ls := b.componentLogUntil[component]; ls.until.Equal(until) {
|
||||
setEnabled(false)
|
||||
b.logf("debugging logging for component %q disabled (by timer)", component)
|
||||
}
|
||||
@@ -481,7 +499,7 @@ func (b *LocalBackend) maybePauseControlClientLocked() {
|
||||
b.cc.SetPaused((b.state == ipn.Stopped && b.netMap != nil) || !networkUp)
|
||||
}
|
||||
|
||||
// linkChange is our link monitor callback, called whenever the network changes.
|
||||
// linkChange is our network monitor callback, called whenever the network changes.
|
||||
// major is whether ifst is different than earlier.
|
||||
func (b *LocalBackend) linkChange(major bool, ifst *interfaces.State) {
|
||||
b.mu.Lock()
|
||||
@@ -558,7 +576,7 @@ func (b *LocalBackend) Shutdown() {
|
||||
b.sockstatLogger.Shutdown()
|
||||
}
|
||||
|
||||
b.unregisterLinkMon()
|
||||
b.unregisterNetMon()
|
||||
b.unregisterHealthWatch()
|
||||
if cc != nil {
|
||||
cc.Shutdown()
|
||||
@@ -1059,7 +1077,7 @@ func (b *LocalBackend) setClientStatus(st controlclient.Status) {
|
||||
b.e.SetDERPMap(st.NetMap.DERPMap)
|
||||
|
||||
// Update our cached DERP map
|
||||
dnsfallback.UpdateCache(st.NetMap.DERPMap)
|
||||
dnsfallback.UpdateCache(st.NetMap.DERPMap, b.logf)
|
||||
|
||||
b.send(ipn.Notify{NetMap: st.NetMap})
|
||||
}
|
||||
@@ -1405,7 +1423,7 @@ func (b *LocalBackend) Start(opts ipn.Options) error {
|
||||
HTTPTestClient: httpTestClient,
|
||||
DiscoPublicKey: discoPublic,
|
||||
DebugFlags: debugFlags,
|
||||
LinkMonitor: b.e.GetLinkMonitor(),
|
||||
NetMon: b.e.GetNetMon(),
|
||||
Pinger: b,
|
||||
PopBrowserURL: b.tellClientToBrowseToURL,
|
||||
OnClientVersion: b.onClientVersion,
|
||||
@@ -2531,6 +2549,9 @@ func (b *LocalBackend) checkPrefsLocked(p *ipn.Prefs) error {
|
||||
if err := b.checkExitNodePrefsLocked(p); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
if err := b.checkFunnelEnabledLocked(p); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
return multierr.New(errs...)
|
||||
}
|
||||
|
||||
@@ -2615,6 +2636,13 @@ func (b *LocalBackend) checkExitNodePrefsLocked(p *ipn.Prefs) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *LocalBackend) checkFunnelEnabledLocked(p *ipn.Prefs) error {
|
||||
if p.ShieldsUp && b.serveConfig.IsFunnelOn() {
|
||||
return errors.New("Cannot enable shields-up when Funnel is enabled.")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *LocalBackend) EditPrefs(mp *ipn.MaskedPrefs) (ipn.PrefsView, error) {
|
||||
b.mu.Lock()
|
||||
if mp.EggSet {
|
||||
@@ -3192,6 +3220,10 @@ func (b *LocalBackend) TailscaleVarRoot() string {
|
||||
switch runtime.GOOS {
|
||||
case "ios", "android", "darwin":
|
||||
return paths.AppSharedDir.Load()
|
||||
case "linux":
|
||||
if distro.Get() == distro.Gokrazy {
|
||||
return "/perm/tailscaled"
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -4690,7 +4722,10 @@ func (b *LocalBackend) Doctor(ctx context.Context, logf logger.Logf) {
|
||||
logf = logger.SlowLoggerWithClock(ctx, logf, 20*time.Millisecond, 60, time.Now)
|
||||
|
||||
var checks []doctor.Check
|
||||
checks = append(checks, routetable.Check{})
|
||||
checks = append(checks,
|
||||
permissions.Check{},
|
||||
routetable.Check{},
|
||||
)
|
||||
|
||||
// Print a log message if any of the global DNS resolvers are Tailscale
|
||||
// IPs; this can interfere with our ability to connect to the Tailscale
|
||||
|
||||
@@ -875,7 +875,7 @@ func TestTKAForceDisable(t *testing.T) {
|
||||
}
|
||||
|
||||
if b.tka != nil {
|
||||
t.Fatal("tka was re-initalized")
|
||||
t.Fatal("tka was re-initialized")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -605,6 +605,16 @@ func (h *peerAPIHandler) logf(format string, a ...any) {
|
||||
h.ps.b.logf("peerapi: "+format, a...)
|
||||
}
|
||||
|
||||
// isAddressValid reports whether addr is a valid destination address for this
|
||||
// node originating from the peer.
|
||||
func (h *peerAPIHandler) isAddressValid(addr netip.Addr) bool {
|
||||
if h.peerNode.SelfNodeV4MasqAddrForThisPeer != nil {
|
||||
return *h.peerNode.SelfNodeV4MasqAddrForThisPeer == addr
|
||||
}
|
||||
pfx := netip.PrefixFrom(addr, addr.BitLen())
|
||||
return slices.Contains(h.selfNode.Addresses, pfx)
|
||||
}
|
||||
|
||||
func (h *peerAPIHandler) validateHost(r *http.Request) error {
|
||||
if r.Host == "peer" {
|
||||
return nil
|
||||
@@ -613,9 +623,8 @@ func (h *peerAPIHandler) validateHost(r *http.Request) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
hostIPPfx := netip.PrefixFrom(ap.Addr(), ap.Addr().BitLen())
|
||||
if !slices.Contains(h.selfNode.Addresses, hostIPPfx) {
|
||||
return fmt.Errorf("%v not found in self addresses", hostIPPfx)
|
||||
if !h.isAddressValid(ap.Addr()) {
|
||||
return fmt.Errorf("%v not found in self addresses", ap.Addr())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -865,6 +874,11 @@ func (h *peerAPIHandler) handleServeSockStats(w http.ResponseWriter, r *http.Req
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
fmt.Fprintln(w, "<!DOCTYPE html><h1>Socket Stats</h1>")
|
||||
|
||||
if !sockstats.IsAvailable {
|
||||
fmt.Fprintln(w, "Socket stats are not available for this client")
|
||||
return
|
||||
}
|
||||
|
||||
stats, interfaceStats, validation := sockstats.Get(), sockstats.GetInterfaces(), sockstats.GetValidation()
|
||||
if stats == nil {
|
||||
fmt.Fprintln(w, "No socket stats available")
|
||||
@@ -942,6 +956,12 @@ func (h *peerAPIHandler) handleServeSockStats(w http.ResponseWriter, r *http.Req
|
||||
fmt.Fprintln(w, "</tfoot>")
|
||||
|
||||
fmt.Fprintln(w, "</table>")
|
||||
|
||||
fmt.Fprintln(w, "<h2>Debug Info</h2>")
|
||||
|
||||
fmt.Fprintln(w, "<pre>")
|
||||
fmt.Fprintln(w, html.EscapeString(sockstats.DebugInfo()))
|
||||
fmt.Fprintln(w, "</pre>")
|
||||
}
|
||||
|
||||
type incomingFile struct {
|
||||
|
||||
@@ -20,7 +20,6 @@ import (
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/clientmetric"
|
||||
"tailscale.com/util/winutil"
|
||||
"tailscale.com/version"
|
||||
)
|
||||
|
||||
// profileManager is a wrapper around a StateStore that manages
|
||||
@@ -66,7 +65,13 @@ func (pm *profileManager) SetCurrentUserID(uid ipn.WindowsUserID) error {
|
||||
// the selected profile for the current user.
|
||||
b, err := pm.store.ReadState(ipn.CurrentProfileKey(string(uid)))
|
||||
if err == ipn.ErrStateNotExist || len(b) == 0 {
|
||||
pm.NewProfile()
|
||||
if runtime.GOOS == "windows" {
|
||||
if err := pm.migrateFromLegacyPrefs(); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
pm.NewProfile()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -424,12 +429,7 @@ var defaultPrefs = func() ipn.PrefsView {
|
||||
prefs.WantRunning = false
|
||||
|
||||
prefs.ControlURL = winutil.GetPolicyString("LoginURL", "")
|
||||
|
||||
if exitNode := winutil.GetPolicyString("ExitNodeIP", ""); exitNode != "" {
|
||||
if ip, err := netip.ParseAddr(exitNode); err == nil {
|
||||
prefs.ExitNodeIP = ip
|
||||
}
|
||||
}
|
||||
prefs.ExitNodeIP = resolveExitNodeIP(netip.Addr{})
|
||||
|
||||
// Allow Incoming (used by the UI) is the negation of ShieldsUp (used by the
|
||||
// backend), so this has to convert between the two conventions.
|
||||
@@ -439,6 +439,16 @@ var defaultPrefs = func() ipn.PrefsView {
|
||||
return prefs.View()
|
||||
}()
|
||||
|
||||
func resolveExitNodeIP(defIP netip.Addr) (ret netip.Addr) {
|
||||
ret = defIP
|
||||
if exitNode := winutil.GetPolicyString("ExitNodeIP", ""); exitNode != "" {
|
||||
if ip, err := netip.ParseAddr(exitNode); err == nil {
|
||||
ret = ip
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
// Store returns the StateStore used by the ProfileManager.
|
||||
func (pm *profileManager) Store() ipn.StateStore {
|
||||
return pm.store
|
||||
@@ -549,27 +559,16 @@ func newProfileManagerWithGOOS(store ipn.StateStore, logf logger.Logf, goos stri
|
||||
func (pm *profileManager) migrateFromLegacyPrefs() error {
|
||||
metricMigration.Add(1)
|
||||
pm.NewProfile()
|
||||
k := ipn.LegacyGlobalDaemonStateKey
|
||||
switch {
|
||||
case runtime.GOOS == "ios":
|
||||
k = "ipn-go-bridge"
|
||||
case version.IsSandboxedMacOS():
|
||||
k = "ipn-go-bridge"
|
||||
case runtime.GOOS == "android":
|
||||
k = "ipn-android"
|
||||
}
|
||||
prefs, err := pm.loadSavedPrefs(k)
|
||||
sentinel, prefs, err := pm.loadLegacyPrefs()
|
||||
if err != nil {
|
||||
metricMigrationError.Add(1)
|
||||
return fmt.Errorf("calling ReadState on state store: %w", err)
|
||||
return err
|
||||
}
|
||||
pm.logf("migrating %q profile to new format", k)
|
||||
if err := pm.SetPrefs(prefs); err != nil {
|
||||
metricMigrationError.Add(1)
|
||||
return fmt.Errorf("migrating _daemon profile: %w", err)
|
||||
}
|
||||
// Do not delete the old state key, as we may be downgraded to an
|
||||
// older version that still relies on it.
|
||||
pm.completeMigration(sentinel)
|
||||
metricMigrationSuccess.Add(1)
|
||||
return nil
|
||||
}
|
||||
|
||||
37
ipn/ipnlocal/profiles_notwindows.go
Normal file
37
ipn/ipnlocal/profiles_notwindows.go
Normal file
@@ -0,0 +1,37 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !windows
|
||||
|
||||
package ipnlocal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/version"
|
||||
)
|
||||
|
||||
func (pm *profileManager) loadLegacyPrefs() (string, ipn.PrefsView, error) {
|
||||
k := ipn.LegacyGlobalDaemonStateKey
|
||||
switch {
|
||||
case runtime.GOOS == "ios":
|
||||
k = "ipn-go-bridge"
|
||||
case version.IsSandboxedMacOS():
|
||||
k = "ipn-go-bridge"
|
||||
case runtime.GOOS == "android":
|
||||
k = "ipn-android"
|
||||
}
|
||||
prefs, err := pm.loadSavedPrefs(k)
|
||||
if err != nil {
|
||||
return "", ipn.PrefsView{}, fmt.Errorf("calling ReadState on state store: %w", err)
|
||||
}
|
||||
pm.logf("migrating %q profile to new format", k)
|
||||
return "", prefs, nil
|
||||
}
|
||||
|
||||
func (pm *profileManager) completeMigration(migrationSentinel string) {
|
||||
// Do not delete the old state key, as we may be downgraded to an
|
||||
// older version that still relies on it.
|
||||
}
|
||||
@@ -5,6 +5,7 @@ package ipnlocal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
@@ -17,6 +18,9 @@ import (
|
||||
)
|
||||
|
||||
func TestProfileCurrentUserSwitch(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("TODO(#7876): test regressed on windows while CI was broken")
|
||||
}
|
||||
store := new(mem.Store)
|
||||
|
||||
pm, err := newProfileManagerWithGOOS(store, logger.Discard, "linux")
|
||||
@@ -73,6 +77,9 @@ func TestProfileCurrentUserSwitch(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestProfileList(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("TODO(#7876): test regressed on windows while CI was broken")
|
||||
}
|
||||
store := new(mem.Store)
|
||||
|
||||
pm, err := newProfileManagerWithGOOS(store, logger.Discard, "linux")
|
||||
@@ -151,6 +158,9 @@ func TestProfileList(t *testing.T) {
|
||||
|
||||
// TestProfileManagement tests creating, loading, and switching profiles.
|
||||
func TestProfileManagement(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("TODO(#7876): test regressed on windows while CI was broken")
|
||||
}
|
||||
store := new(mem.Store)
|
||||
|
||||
pm, err := newProfileManagerWithGOOS(store, logger.Discard, "linux")
|
||||
@@ -302,6 +312,11 @@ func TestProfileManagement(t *testing.T) {
|
||||
// TestProfileManagementWindows tests going into and out of Unattended mode on
|
||||
// Windows.
|
||||
func TestProfileManagementWindows(t *testing.T) {
|
||||
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("TODO(#7876): test regressed on windows while CI was broken")
|
||||
}
|
||||
|
||||
store := new(mem.Store)
|
||||
|
||||
pm, err := newProfileManagerWithGOOS(store, logger.Discard, "windows")
|
||||
|
||||
84
ipn/ipnlocal/profiles_windows.go
Normal file
84
ipn/ipnlocal/profiles_windows.go
Normal file
@@ -0,0 +1,84 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package ipnlocal
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
|
||||
"tailscale.com/atomicfile"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/util/winutil/policy"
|
||||
)
|
||||
|
||||
const (
|
||||
legacyPrefsFile = "prefs"
|
||||
legacyPrefsMigrationSentinelFile = "_migrated-to-profiles"
|
||||
legacyPrefsExt = ".conf"
|
||||
)
|
||||
|
||||
var errAlreadyMigrated = errors.New("profile migration already completed")
|
||||
|
||||
func legacyPrefsDir(uid ipn.WindowsUserID) (string, error) {
|
||||
// TODO(aaron): Ideally we'd have the impersonation token for the pipe's
|
||||
// client and use it to call SHGetKnownFolderPath, thus yielding the correct
|
||||
// path without having to make gross assumptions about directory names.
|
||||
usr, err := user.LookupId(string(uid))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if usr.HomeDir == "" {
|
||||
return "", fmt.Errorf("user %q does not have a home directory", uid)
|
||||
}
|
||||
userLegacyPrefsDir := filepath.Join(usr.HomeDir, "AppData", "Local", "Tailscale")
|
||||
return userLegacyPrefsDir, nil
|
||||
}
|
||||
|
||||
func (pm *profileManager) loadLegacyPrefs() (string, ipn.PrefsView, error) {
|
||||
userLegacyPrefsDir, err := legacyPrefsDir(pm.currentUserID)
|
||||
if err != nil {
|
||||
return "", ipn.PrefsView{}, err
|
||||
}
|
||||
|
||||
migrationSentinel := filepath.Join(userLegacyPrefsDir, legacyPrefsMigrationSentinelFile+legacyPrefsExt)
|
||||
// verify that migration sentinel is not present
|
||||
_, err = os.Stat(migrationSentinel)
|
||||
if err == nil {
|
||||
return "", ipn.PrefsView{}, errAlreadyMigrated
|
||||
}
|
||||
if !os.IsNotExist(err) {
|
||||
return "", ipn.PrefsView{}, err
|
||||
}
|
||||
|
||||
prefsPath := filepath.Join(userLegacyPrefsDir, legacyPrefsFile+legacyPrefsExt)
|
||||
prefs, err := ipn.LoadPrefs(prefsPath)
|
||||
if err != nil {
|
||||
return "", ipn.PrefsView{}, err
|
||||
}
|
||||
|
||||
prefs.ControlURL = policy.SelectControlURL(defaultPrefs.ControlURL(), prefs.ControlURL)
|
||||
prefs.ExitNodeIP = resolveExitNodeIP(prefs.ExitNodeIP)
|
||||
prefs.ShieldsUp = resolveShieldsUp(prefs.ShieldsUp)
|
||||
prefs.ForceDaemon = resolveForceDaemon(prefs.ForceDaemon)
|
||||
|
||||
pm.logf("migrating Windows profile to new format")
|
||||
return migrationSentinel, prefs.View(), nil
|
||||
}
|
||||
|
||||
func (pm *profileManager) completeMigration(migrationSentinel string) {
|
||||
atomicfile.WriteFile(migrationSentinel, []byte{}, 0600)
|
||||
}
|
||||
|
||||
func resolveShieldsUp(defval bool) bool {
|
||||
pol := policy.GetPreferenceOptionPolicy("AllowIncomingConnections")
|
||||
return !pol.ShouldEnable(!defval)
|
||||
}
|
||||
|
||||
func resolveForceDaemon(defval bool) bool {
|
||||
pol := policy.GetPreferenceOptionPolicy("UnattendedMode")
|
||||
return pol.ShouldEnable(defval)
|
||||
}
|
||||
@@ -17,7 +17,6 @@ import (
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
pathpkg "path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -144,7 +143,7 @@ func (s *serveListener) Run() {
|
||||
}
|
||||
|
||||
func (s *serveListener) shouldWarnAboutListenError(err error) bool {
|
||||
if !s.b.e.GetLinkMonitor().InterfaceState().HasIP(s.ap.Addr()) {
|
||||
if !s.b.e.GetNetMon().InterfaceState().HasIP(s.ap.Addr()) {
|
||||
// Machine likely doesn't have IPv6 enabled (or the IP is still being
|
||||
// assigned). No need to warn. Notably, WSL2 (Issue 6303).
|
||||
return false
|
||||
@@ -218,6 +217,11 @@ func (b *LocalBackend) SetServeConfig(config *ipn.ServeConfig) error {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
|
||||
prefs := b.pm.CurrentPrefs()
|
||||
if config.IsFunnelOn() && prefs.ShieldsUp() {
|
||||
return errors.New("Unable to turn on Funnel while shields-up is enabled")
|
||||
}
|
||||
|
||||
nm := b.netMap
|
||||
if nm == nil {
|
||||
return errors.New("netMap is nil")
|
||||
@@ -415,19 +419,19 @@ func (b *LocalBackend) getServeHandler(r *http.Request) (_ ipn.HTTPHandlerView,
|
||||
if h, ok := wsc.Handlers().GetOk(r.URL.Path); ok {
|
||||
return h, r.URL.Path, true
|
||||
}
|
||||
path := path.Clean(r.URL.Path)
|
||||
pth := path.Clean(r.URL.Path)
|
||||
for {
|
||||
withSlash := path + "/"
|
||||
withSlash := pth + "/"
|
||||
if h, ok := wsc.Handlers().GetOk(withSlash); ok {
|
||||
return h, withSlash, true
|
||||
}
|
||||
if h, ok := wsc.Handlers().GetOk(path); ok {
|
||||
return h, path, true
|
||||
if h, ok := wsc.Handlers().GetOk(pth); ok {
|
||||
return h, pth, true
|
||||
}
|
||||
if path == "/" {
|
||||
if pth == "/" {
|
||||
return z, "", false
|
||||
}
|
||||
path = pathpkg.Dir(path)
|
||||
pth = path.Dir(pth)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -439,18 +443,26 @@ func (b *LocalBackend) proxyHandlerForBackend(backend string) (*httputil.Reverse
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid url %s: %w", targetURL, err)
|
||||
}
|
||||
rp := httputil.NewSingleHostReverseProxy(u)
|
||||
rp.Transport = &http.Transport{
|
||||
DialContext: b.dialer.SystemDial,
|
||||
TLSClientConfig: &tls.Config{
|
||||
InsecureSkipVerify: insecure,
|
||||
rp := &httputil.ReverseProxy{
|
||||
Rewrite: func(r *httputil.ProxyRequest) {
|
||||
r.SetURL(u)
|
||||
r.Out.Host = r.In.Host
|
||||
if c, ok := r.Out.Context().Value(serveHTTPContextKey{}).(*serveHTTPContext); ok {
|
||||
r.Out.Header.Set("X-Forwarded-For", c.SrcAddr.Addr().String())
|
||||
}
|
||||
},
|
||||
Transport: &http.Transport{
|
||||
DialContext: b.dialer.SystemDial,
|
||||
TLSClientConfig: &tls.Config{
|
||||
InsecureSkipVerify: insecure,
|
||||
},
|
||||
// Values for the following parameters have been copied from http.DefaultTransport.
|
||||
ForceAttemptHTTP2: true,
|
||||
MaxIdleConns: 100,
|
||||
IdleConnTimeout: 90 * time.Second,
|
||||
TLSHandshakeTimeout: 10 * time.Second,
|
||||
ExpectContinueTimeout: 1 * time.Second,
|
||||
},
|
||||
// Values for the following parameters have been copied from http.DefaultTransport.
|
||||
ForceAttemptHTTP2: true,
|
||||
MaxIdleConns: 100,
|
||||
IdleConnTimeout: 90 * time.Second,
|
||||
TLSHandshakeTimeout: 10 * time.Second,
|
||||
ExpectContinueTimeout: 1 * time.Second,
|
||||
}
|
||||
return rp, nil
|
||||
}
|
||||
@@ -476,7 +488,12 @@ func (b *LocalBackend) serveWebHandler(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "unknown proxy destination", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
p.(http.Handler).ServeHTTP(w, r)
|
||||
h := p.(http.Handler)
|
||||
// Trim the mount point from the URL path before proxying. (#6571)
|
||||
if r.URL.Path != "/" {
|
||||
h = http.StripPrefix(strings.TrimSuffix(mountPoint, "/"), h)
|
||||
}
|
||||
h.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -37,8 +37,8 @@ func (s *Server) handleProxyConnectConn(w http.ResponseWriter, r *http.Request)
|
||||
return
|
||||
}
|
||||
|
||||
tr := logpolicy.NewLogtailTransport(logHost)
|
||||
back, err := tr.DialContext(ctx, "tcp", hostPort)
|
||||
dialContext := logpolicy.MakeDialFunc(s.netMon)
|
||||
back, err := dialContext(ctx, "tcp", hostPort)
|
||||
if err != nil {
|
||||
s.logf("error CONNECT dialing %v: %v", hostPort, err)
|
||||
http.Error(w, "Connect failure", http.StatusBadGateway)
|
||||
|
||||
@@ -24,6 +24,7 @@ import (
|
||||
"tailscale.com/ipn/ipnauth"
|
||||
"tailscale.com/ipn/ipnlocal"
|
||||
"tailscale.com/ipn/localapi"
|
||||
"tailscale.com/net/netmon"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/logid"
|
||||
"tailscale.com/util/mak"
|
||||
@@ -36,6 +37,7 @@ import (
|
||||
type Server struct {
|
||||
lb atomic.Pointer[ipnlocal.LocalBackend]
|
||||
logf logger.Logf
|
||||
netMon *netmon.Monitor // optional; nil means interfaces will be looked up on-demand
|
||||
backendLogID logid.PublicID
|
||||
// resetOnZero is whether to call bs.Reset on transition from
|
||||
// 1->0 active HTTP requests. That is, this is whether the backend is
|
||||
@@ -197,7 +199,7 @@ func (s *Server) serveHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
defer onDone()
|
||||
|
||||
if strings.HasPrefix(r.URL.Path, "/localapi/") {
|
||||
lah := localapi.NewHandler(lb, s.logf, s.backendLogID)
|
||||
lah := localapi.NewHandler(lb, s.logf, s.netMon, s.backendLogID)
|
||||
lah.PermitRead, lah.PermitWrite = s.localAPIPermissions(ci)
|
||||
lah.PermitCert = s.connCanFetchCerts(ci)
|
||||
lah.ServeHTTP(w, r)
|
||||
@@ -408,15 +410,18 @@ func (s *Server) addActiveHTTPRequest(req *http.Request, ci *ipnauth.ConnIdentit
|
||||
}
|
||||
|
||||
// New returns a new Server.
|
||||
// The netMon parameter is optional; if non-nil it's used to do faster interface
|
||||
// lookups.
|
||||
//
|
||||
// To start it, use the Server.Run method.
|
||||
//
|
||||
// At some point, either before or after Run, the Server's SetLocalBackend
|
||||
// method must also be called before Server can do anything useful.
|
||||
func New(logf logger.Logf, logID logid.PublicID) *Server {
|
||||
func New(logf logger.Logf, logID logid.PublicID, netMon *netmon.Monitor) *Server {
|
||||
return &Server{
|
||||
backendLogID: logID,
|
||||
logf: logf,
|
||||
netMon: netMon,
|
||||
resetOnZero: envknob.GOOS() == "windows",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,17 +4,24 @@
|
||||
package localapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"tailscale.com/derp/derphttp"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/net/netaddr"
|
||||
"tailscale.com/net/netns"
|
||||
"tailscale.com/net/stun"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/nettype"
|
||||
)
|
||||
|
||||
func (h *Handler) serveDebugDERPRegion(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -132,6 +139,92 @@ func (h *Handler) serveDebugDERPRegion(w http.ResponseWriter, r *http.Request) {
|
||||
return hasIPv4 || hasIPv6
|
||||
}
|
||||
|
||||
checkSTUN4 := func(derpNode *tailcfg.DERPNode) {
|
||||
u4, err := nettype.MakePacketListenerWithNetIP(netns.Listener(h.logf, h.netMon)).ListenPacket(ctx, "udp4", ":0")
|
||||
if err != nil {
|
||||
st.Errors = append(st.Errors, fmt.Sprintf("Error creating IPv4 STUN listener: %v", err))
|
||||
return
|
||||
}
|
||||
defer u4.Close()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
var addr netip.Addr
|
||||
if derpNode.IPv4 != "" {
|
||||
addr, err = netip.ParseAddr(derpNode.IPv4)
|
||||
if err != nil {
|
||||
// Error printed elsewhere
|
||||
return
|
||||
}
|
||||
} else {
|
||||
addrs, err := net.DefaultResolver.LookupNetIP(ctx, "ip4", derpNode.HostName)
|
||||
if err != nil {
|
||||
st.Errors = append(st.Errors, fmt.Sprintf("Error resolving node %q IPv4 addresses: %v", derpNode.HostName, err))
|
||||
return
|
||||
}
|
||||
addr = addrs[0]
|
||||
}
|
||||
|
||||
addrPort := netip.AddrPortFrom(addr, uint16(firstNonzero(derpNode.STUNPort, 3478)))
|
||||
|
||||
txID := stun.NewTxID()
|
||||
req := stun.Request(txID)
|
||||
|
||||
done := make(chan struct{})
|
||||
defer close(done)
|
||||
|
||||
go func() {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
case <-done:
|
||||
}
|
||||
u4.Close()
|
||||
}()
|
||||
|
||||
gotResponse := make(chan netip.AddrPort, 1)
|
||||
go func() {
|
||||
defer u4.Close()
|
||||
|
||||
var buf [64 << 10]byte
|
||||
for {
|
||||
n, addr, err := u4.ReadFromUDPAddrPort(buf[:])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
pkt := buf[:n]
|
||||
if !stun.Is(pkt) {
|
||||
continue
|
||||
}
|
||||
ap := netaddr.Unmap(addr)
|
||||
if !ap.IsValid() {
|
||||
continue
|
||||
}
|
||||
tx, addrPort, err := stun.ParseResponse(pkt)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if tx == txID {
|
||||
gotResponse <- addrPort
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
_, err = u4.WriteToUDPAddrPort(req, addrPort)
|
||||
if err != nil {
|
||||
st.Errors = append(st.Errors, fmt.Sprintf("Error sending IPv4 STUN packet to %v (%q): %v", addrPort, derpNode.HostName, err))
|
||||
return
|
||||
}
|
||||
|
||||
select {
|
||||
case resp := <-gotResponse:
|
||||
st.Info = append(st.Info, fmt.Sprintf("Node %q returned IPv4 STUN response: %v", derpNode.HostName, resp))
|
||||
case <-ctx.Done():
|
||||
st.Warnings = append(st.Warnings, fmt.Sprintf("Node %q did not return a IPv4 STUN response", derpNode.HostName))
|
||||
}
|
||||
}
|
||||
|
||||
// Start by checking whether we can establish a HTTP connection
|
||||
for _, derpNode := range reg.Nodes {
|
||||
connSuccess := checkConn(derpNode)
|
||||
@@ -156,7 +249,7 @@ func (h *Handler) serveDebugDERPRegion(w http.ResponseWriter, r *http.Request) {
|
||||
serverPubKeys := make(map[key.NodePublic]bool)
|
||||
for i := 0; i < 5; i++ {
|
||||
func() {
|
||||
rc := derphttp.NewRegionClient(fakePrivKey, h.logf, func() *tailcfg.DERPRegion {
|
||||
rc := derphttp.NewRegionClient(fakePrivKey, h.logf, h.netMon, func() *tailcfg.DERPRegion {
|
||||
return &tailcfg.DERPRegion{
|
||||
RegionID: reg.RegionID,
|
||||
RegionCode: reg.RegionCode,
|
||||
@@ -178,6 +271,10 @@ func (h *Handler) serveDebugDERPRegion(w http.ResponseWriter, r *http.Request) {
|
||||
if len(serverPubKeys) > 1 {
|
||||
st.Errors = append(st.Errors, fmt.Sprintf("Received multiple server public keys (%d); is the DERP server behind a load balancer?", len(serverPubKeys)))
|
||||
}
|
||||
|
||||
// Send a STUN query to this node to verify whether or not it
|
||||
// correctly returns an IP address.
|
||||
checkSTUN4(derpNode)
|
||||
}
|
||||
|
||||
// TODO(bradfitz): finish:
|
||||
@@ -191,7 +288,6 @@ func (h *Handler) serveDebugDERPRegion(w http.ResponseWriter, r *http.Request) {
|
||||
// protocol to say how many peers it's meshed with. Should match count
|
||||
// in DERPRegion. Or maybe even list all their server pub keys that it's peered
|
||||
// with.
|
||||
// * try STUN queries
|
||||
// * If their certificate is bad, either expired or just wrongly
|
||||
// issued in the first place, tell them specifically that the
|
||||
// cert is bad not just that the connection failed.
|
||||
|
||||
@@ -34,6 +34,7 @@ import (
|
||||
"tailscale.com/ipn/ipnlocal"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/logtail"
|
||||
"tailscale.com/net/netmon"
|
||||
"tailscale.com/net/netutil"
|
||||
"tailscale.com/net/portmapper"
|
||||
"tailscale.com/tailcfg"
|
||||
@@ -46,7 +47,6 @@ import (
|
||||
"tailscale.com/util/httpm"
|
||||
"tailscale.com/util/mak"
|
||||
"tailscale.com/version"
|
||||
"tailscale.com/wgengine/monitor"
|
||||
)
|
||||
|
||||
type localAPIHandler func(*Handler, http.ResponseWriter, *http.Request)
|
||||
@@ -125,8 +125,10 @@ var (
|
||||
metrics = map[string]*clientmetric.Metric{}
|
||||
)
|
||||
|
||||
func NewHandler(b *ipnlocal.LocalBackend, logf logger.Logf, logID logid.PublicID) *Handler {
|
||||
return &Handler{b: b, logf: logf, backendLogID: logID}
|
||||
// NewHandler creates a new LocalAPI HTTP handler. All parameters except netMon
|
||||
// are required (if non-nil it's used to do faster interface lookups).
|
||||
func NewHandler(b *ipnlocal.LocalBackend, logf logger.Logf, netMon *netmon.Monitor, logID logid.PublicID) *Handler {
|
||||
return &Handler{b: b, logf: logf, netMon: netMon, backendLogID: logID}
|
||||
}
|
||||
|
||||
type Handler struct {
|
||||
@@ -150,6 +152,7 @@ type Handler struct {
|
||||
|
||||
b *ipnlocal.LocalBackend
|
||||
logf logger.Logf
|
||||
netMon *netmon.Monitor // optional; nil means interfaces will be looked up on-demand
|
||||
backendLogID logid.PublicID
|
||||
}
|
||||
|
||||
@@ -679,7 +682,7 @@ func (h *Handler) serveDebugPortmap(w http.ResponseWriter, r *http.Request) {
|
||||
done := make(chan bool, 1)
|
||||
|
||||
var c *portmapper.Client
|
||||
c = portmapper.NewClient(logger.WithPrefix(logf, "portmapper: "), debugKnobs, func() {
|
||||
c = portmapper.NewClient(logger.WithPrefix(logf, "portmapper: "), h.netMon, debugKnobs, func() {
|
||||
logf("portmapping changed.")
|
||||
logf("have mapping: %v", c.HaveMapping())
|
||||
|
||||
@@ -695,7 +698,7 @@ func (h *Handler) serveDebugPortmap(w http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
defer c.Close()
|
||||
|
||||
linkMon, err := monitor.New(logger.WithPrefix(logf, "monitor: "))
|
||||
netMon, err := netmon.New(logger.WithPrefix(logf, "monitor: "))
|
||||
if err != nil {
|
||||
logf("error creating monitor: %v", err)
|
||||
return
|
||||
@@ -707,14 +710,14 @@ func (h *Handler) serveDebugPortmap(w http.ResponseWriter, r *http.Request) {
|
||||
self = netip.MustParseAddr(b)
|
||||
return gw, self, true
|
||||
}
|
||||
return linkMon.GatewayAndSelfIP()
|
||||
return netMon.GatewayAndSelfIP()
|
||||
}
|
||||
|
||||
c.SetGatewayLookupFunc(gatewayAndSelfIP)
|
||||
|
||||
gw, selfIP, ok := gatewayAndSelfIP()
|
||||
if !ok {
|
||||
logf("no gateway or self IP; %v", linkMon.InterfaceState())
|
||||
logf("no gateway or self IP; %v", netMon.InterfaceState())
|
||||
return
|
||||
}
|
||||
logf("gw=%v; self=%v", gw, selfIP)
|
||||
|
||||
@@ -26,8 +26,8 @@ func IsInterestingService(s tailcfg.Service, os string) bool {
|
||||
// there.
|
||||
return true
|
||||
}
|
||||
// Windows has tons of TCP listeners. We need to move to a blacklist
|
||||
// model later, but for now we just whitelist some common ones:
|
||||
// Windows has tons of TCP listeners. We need to move to a denylist
|
||||
// model later, but for now we just allow some common ones:
|
||||
switch s.Port {
|
||||
case 22, // ssh
|
||||
80, // http
|
||||
|
||||
10
ipn/serve.go
10
ipn/serve.go
@@ -163,6 +163,12 @@ func (sc *ServeConfig) IsServingWeb(port uint16) bool {
|
||||
return sc.TCP[port].HTTPS
|
||||
}
|
||||
|
||||
// IsFunnelOn checks if ServeConfig is currently allowing
|
||||
// funnel traffic for any host:port.
|
||||
//
|
||||
// View version of ServeConfig.IsFunnelOn.
|
||||
func (v ServeConfigView) IsFunnelOn() bool { return v.ж.IsFunnelOn() }
|
||||
|
||||
// IsFunnelOn checks if ServeConfig is currently allowing
|
||||
// funnel traffic for any host:port.
|
||||
func (sc *ServeConfig) IsFunnelOn() bool {
|
||||
@@ -180,7 +186,7 @@ func (sc *ServeConfig) IsFunnelOn() bool {
|
||||
// CheckFunnelAccess checks whether Funnel access is allowed for the given node
|
||||
// and port.
|
||||
// It checks:
|
||||
// 1. an invite was used to join the Funnel alpha
|
||||
// 1. Funnel is enabled on the Tailnet
|
||||
// 2. HTTPS is enabled on the Tailnet
|
||||
// 3. the node has the "funnel" nodeAttr
|
||||
// 4. the port is allowed for Funnel
|
||||
@@ -190,7 +196,7 @@ func (sc *ServeConfig) IsFunnelOn() bool {
|
||||
// Funnel.
|
||||
func CheckFunnelAccess(port uint16, 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/s/no-funnel.")
|
||||
return errors.New("Funnel not enabled; See https://tailscale.com/s/no-funnel.")
|
||||
}
|
||||
if slices.Contains(nodeAttrs, tailcfg.CapabilityWarnFunnelNoHTTPS) {
|
||||
return errors.New("Funnel not available; HTTPS must be enabled. See https://tailscale.com/s/https.")
|
||||
|
||||
@@ -4,8 +4,10 @@
|
||||
package ipn
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
@@ -72,6 +74,12 @@ type StateStore interface {
|
||||
WriteState(id StateKey, bs []byte) error
|
||||
}
|
||||
|
||||
// StateStoreDialerSetter is an optional interface that StateStores
|
||||
// can implement to allow the caller to set a custom dialer.
|
||||
type StateStoreDialerSetter interface {
|
||||
SetDialer(d func(ctx context.Context, network, address string) (net.Conn, error))
|
||||
}
|
||||
|
||||
// ReadStoreInt reads an integer from a StateStore.
|
||||
func ReadStoreInt(store StateStore, id StateKey) (int64, error) {
|
||||
v, err := store.ReadState(id)
|
||||
|
||||
@@ -51,6 +51,12 @@ type awsStore struct {
|
||||
|
||||
// New returns a new ipn.StateStore using the AWS SSM storage
|
||||
// location given by ssmARN.
|
||||
//
|
||||
// Note that we store the entire store in a single parameter
|
||||
// key, therefore if the state is above 8kb, it can cause
|
||||
// Tailscaled to only only store new state in-memory and
|
||||
// restarting Tailscaled can fail until you delete your state
|
||||
// from the AWS Parameter Store.
|
||||
func New(_ logger.Logf, ssmARN string) (ipn.StateStore, error) {
|
||||
return newStore(ssmARN, nil)
|
||||
}
|
||||
@@ -160,14 +166,19 @@ func (s *awsStore) persistState() error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Store in AWS SSM parameter store
|
||||
// Store in AWS SSM parameter store.
|
||||
//
|
||||
// We use intelligent tiering so that when the state is below 4kb, it uses Standard tiering
|
||||
// which is free. However, if it exceeds 4kb it switches the parameter to advanced tiering
|
||||
// doubling the capacity to 8kb per the following docs:
|
||||
// https://aws.amazon.com/about-aws/whats-new/2019/08/aws-systems-manager-parameter-store-announces-intelligent-tiering-to-enable-automatic-parameter-tier-selection/
|
||||
_, err = s.ssmClient.PutParameter(
|
||||
context.TODO(),
|
||||
&ssm.PutParameterInput{
|
||||
Name: aws.String(s.ParameterName()),
|
||||
Value: aws.String(string(bs)),
|
||||
Overwrite: aws.Bool(true),
|
||||
Tier: ssmTypes.ParameterTierStandard,
|
||||
Tier: ssmTypes.ParameterTierIntelligentTiering,
|
||||
Type: ssmTypes.ParameterTypeSecureString,
|
||||
},
|
||||
)
|
||||
|
||||
@@ -7,6 +7,7 @@ package kubestore
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -33,6 +34,10 @@ func New(_ logger.Logf, secretName string) (*Store, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Store) SetDialer(d func(ctx context.Context, network, address string) (net.Conn, error)) {
|
||||
s.client.SetDialer(d)
|
||||
}
|
||||
|
||||
func (s *Store) String() string { return "kube.Store" }
|
||||
|
||||
// ReadState implements the StateStore interface.
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
@@ -90,6 +91,12 @@ func (c *Client) SetURL(url string) {
|
||||
c.url = url
|
||||
}
|
||||
|
||||
// SetDialer sets the dialer to use when establishing a connection
|
||||
// to the Kubernetes API server.
|
||||
func (c *Client) SetDialer(dialer func(ctx context.Context, network, addr string) (net.Conn, error)) {
|
||||
c.client.Transport.(*http.Transport).DialContext = dialer
|
||||
}
|
||||
|
||||
func (c *Client) expireToken() {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
@@ -13,6 +13,20 @@ Client][]. See also the dependencies in the [Tailscale CLI][].
|
||||
- [gioui.org](https://pkg.go.dev/gioui.org) ([MIT](https://git.sr.ht/~eliasnaur/gio/tree/32c6a9b10d0b/LICENSE))
|
||||
- [gioui.org/cpu](https://pkg.go.dev/gioui.org/cpu) ([MIT](https://git.sr.ht/~eliasnaur/gio-cpu/tree/8d6a761490d2/LICENSE))
|
||||
- [gioui.org/shader](https://pkg.go.dev/gioui.org/shader) ([MIT](https://git.sr.ht/~eliasnaur/gio-shader/tree/v1.0.6/LICENSE))
|
||||
- [github.com/aws/aws-sdk-go-v2](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/v1.17.3/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2/config](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/config) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/config/v1.11.0/config/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2/credentials](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/credentials) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/credentials/v1.6.4/credentials/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2/feature/ec2/imds](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/feature/ec2/imds) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/feature/ec2/imds/v1.8.2/feature/ec2/imds/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2/internal/configsources](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/internal/configsources) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/internal/configsources/v1.1.27/internal/configsources/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2/internal/endpoints/v2](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/internal/endpoints/v2) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/internal/endpoints/v2.4.21/internal/endpoints/v2/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2/internal/ini](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/internal/ini) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/internal/ini/v1.3.2/internal/ini/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2/internal/sync/singleflight](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/internal/sync/singleflight) ([BSD-3-Clause](https://github.com/aws/aws-sdk-go-v2/blob/v1.17.3/internal/sync/singleflight/LICENSE))
|
||||
- [github.com/aws/aws-sdk-go-v2/service/internal/presigned-url](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/internal/presigned-url) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/service/internal/presigned-url/v1.5.2/service/internal/presigned-url/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2/service/ssm](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/ssm) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/service/ssm/v1.35.0/service/ssm/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2/service/sso](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/sso) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/service/sso/v1.6.2/service/sso/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2/service/sts](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/sts) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/service/sts/v1.11.1/service/sts/LICENSE.txt))
|
||||
- [github.com/aws/smithy-go](https://pkg.go.dev/github.com/aws/smithy-go) ([Apache-2.0](https://github.com/aws/smithy-go/blob/v1.13.5/LICENSE))
|
||||
- [github.com/aws/smithy-go/internal/sync/singleflight](https://pkg.go.dev/github.com/aws/smithy-go/internal/sync/singleflight) ([BSD-3-Clause](https://github.com/aws/smithy-go/blob/v1.13.5/internal/sync/singleflight/LICENSE))
|
||||
- [github.com/benoitkugler/textlayout](https://pkg.go.dev/github.com/benoitkugler/textlayout) ([MIT](https://github.com/benoitkugler/textlayout/blob/v0.3.0/LICENSE))
|
||||
- [github.com/benoitkugler/textlayout/fonts](https://pkg.go.dev/github.com/benoitkugler/textlayout/fonts) ([MIT](https://github.com/benoitkugler/textlayout/blob/v0.3.0/fonts/LICENSE))
|
||||
- [github.com/benoitkugler/textlayout/graphite](https://pkg.go.dev/github.com/benoitkugler/textlayout/graphite) ([MIT](https://github.com/benoitkugler/textlayout/blob/v0.3.0/graphite/LICENSE))
|
||||
@@ -26,6 +40,7 @@ Client][]. See also the dependencies in the [Tailscale CLI][].
|
||||
- [github.com/hdevalence/ed25519consensus](https://pkg.go.dev/github.com/hdevalence/ed25519consensus) ([BSD-3-Clause](https://github.com/hdevalence/ed25519consensus/blob/c00d1f31bab3/LICENSE))
|
||||
- [github.com/illarion/gonotify](https://pkg.go.dev/github.com/illarion/gonotify) ([MIT](https://github.com/illarion/gonotify/blob/v1.0.1/LICENSE))
|
||||
- [github.com/insomniacslk/dhcp](https://pkg.go.dev/github.com/insomniacslk/dhcp) ([BSD-3-Clause](https://github.com/insomniacslk/dhcp/blob/de60144f33f8/LICENSE))
|
||||
- [github.com/jmespath/go-jmespath](https://pkg.go.dev/github.com/jmespath/go-jmespath) ([Apache-2.0](https://github.com/jmespath/go-jmespath/blob/v0.4.0/LICENSE))
|
||||
- [github.com/josharian/native](https://pkg.go.dev/github.com/josharian/native) ([MIT](https://github.com/josharian/native/blob/5c7d0dd6ab86/license))
|
||||
- [github.com/jsimonetti/rtnetlink](https://pkg.go.dev/github.com/jsimonetti/rtnetlink) ([MIT](https://github.com/jsimonetti/rtnetlink/blob/d380b505068b/LICENSE.md))
|
||||
- [github.com/klauspost/compress](https://pkg.go.dev/github.com/klauspost/compress) ([Apache-2.0](https://github.com/klauspost/compress/blob/v1.15.4/LICENSE))
|
||||
@@ -42,7 +57,7 @@ Client][]. See also the dependencies in the [Tailscale CLI][].
|
||||
- [github.com/tailscale/goupnp](https://pkg.go.dev/github.com/tailscale/goupnp) ([BSD-2-Clause](https://github.com/tailscale/goupnp/blob/c64d0f06ea05/LICENSE))
|
||||
- [github.com/tailscale/netlink](https://pkg.go.dev/github.com/tailscale/netlink) ([Apache-2.0](https://github.com/tailscale/netlink/blob/cabfb018fe85/LICENSE))
|
||||
- [github.com/tailscale/tailscale-android](https://pkg.go.dev/github.com/tailscale/tailscale-android) ([BSD-3-Clause](https://github.com/tailscale/tailscale-android/blob/HEAD/LICENSE))
|
||||
- [github.com/tailscale/wireguard-go](https://pkg.go.dev/github.com/tailscale/wireguard-go) ([MIT](https://github.com/tailscale/wireguard-go/blob/4fa124729667/LICENSE))
|
||||
- [github.com/tailscale/wireguard-go](https://pkg.go.dev/github.com/tailscale/wireguard-go) ([MIT](https://github.com/tailscale/wireguard-go/blob/af172621b4dd/LICENSE))
|
||||
- [github.com/tcnksm/go-httpstat](https://pkg.go.dev/github.com/tcnksm/go-httpstat) ([MIT](https://github.com/tcnksm/go-httpstat/blob/v0.2.0/LICENSE))
|
||||
- [github.com/u-root/uio](https://pkg.go.dev/github.com/u-root/uio) ([BSD-3-Clause](https://github.com/u-root/uio/blob/c3537552635f/LICENSE))
|
||||
- [github.com/vishvananda/netlink/nl](https://pkg.go.dev/github.com/vishvananda/netlink/nl) ([Apache-2.0](https://github.com/vishvananda/netlink/blob/650dca95af54/LICENSE))
|
||||
@@ -58,11 +73,11 @@ Client][]. See also the dependencies in the [Tailscale CLI][].
|
||||
- [golang.org/x/image](https://pkg.go.dev/golang.org/x/image) ([BSD-3-Clause](https://cs.opensource.google/go/x/image/+/v0.5.0:LICENSE))
|
||||
- [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://cs.opensource.google/go/x/net/+/v0.7.0:LICENSE))
|
||||
- [golang.org/x/sync/errgroup](https://pkg.go.dev/golang.org/x/sync/errgroup) ([BSD-3-Clause](https://cs.opensource.google/go/x/sync/+/v0.1.0:LICENSE))
|
||||
- [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/v0.5.0:LICENSE))
|
||||
- [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/a3b23cc7:LICENSE))
|
||||
- [golang.org/x/term](https://pkg.go.dev/golang.org/x/term) ([BSD-3-Clause](https://cs.opensource.google/go/x/term/+/v0.5.0:LICENSE))
|
||||
- [golang.org/x/text](https://pkg.go.dev/golang.org/x/text) ([BSD-3-Clause](https://cs.opensource.google/go/x/text/+/v0.7.0:LICENSE))
|
||||
- [golang.org/x/time/rate](https://pkg.go.dev/golang.org/x/time/rate) ([BSD-3-Clause](https://cs.opensource.google/go/x/time/+/579cf78f:LICENSE))
|
||||
- [gvisor.dev/gvisor/pkg](https://pkg.go.dev/gvisor.dev/gvisor/pkg) ([Apache-2.0](https://github.com/google/gvisor/blob/703fd9b7fbc0/LICENSE))
|
||||
- [gvisor.dev/gvisor/pkg](https://pkg.go.dev/gvisor.dev/gvisor/pkg) ([Apache-2.0](https://github.com/google/gvisor/blob/162ed5ef888d/LICENSE))
|
||||
- [inet.af/netaddr](https://pkg.go.dev/inet.af/netaddr) ([BSD-3-Clause](https://github.com/inetaf/netaddr/blob/097006376321/LICENSE))
|
||||
- [inet.af/peercred](https://pkg.go.dev/inet.af/peercred) ([BSD-3-Clause](https://github.com/inetaf/peercred/blob/0893ea02156a/LICENSE))
|
||||
- [nhooyr.io/websocket](https://pkg.go.dev/nhooyr.io/websocket) ([MIT](https://github.com/nhooyr/websocket/blob/v1.8.7/LICENSE.txt))
|
||||
|
||||
@@ -11,6 +11,21 @@ and [iOS][]. See also the dependencies in the [Tailscale CLI][].
|
||||
|
||||
|
||||
- [filippo.io/edwards25519](https://pkg.go.dev/filippo.io/edwards25519) ([BSD-3-Clause](https://github.com/FiloSottile/edwards25519/blob/v1.0.0-rc.1/LICENSE))
|
||||
- [github.com/aws/aws-sdk-go-v2](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/v1.17.3/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2/config](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/config) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/config/v1.17.7/config/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2/credentials](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/credentials) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/credentials/v1.12.20/credentials/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2/feature/ec2/imds](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/feature/ec2/imds) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/feature/ec2/imds/v1.12.17/feature/ec2/imds/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2/internal/configsources](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/internal/configsources) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/internal/configsources/v1.1.27/internal/configsources/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2/internal/endpoints/v2](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/internal/endpoints/v2) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/internal/endpoints/v2.4.21/internal/endpoints/v2/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2/internal/ini](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/internal/ini) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/internal/ini/v1.3.24/internal/ini/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2/internal/sync/singleflight](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/internal/sync/singleflight) ([BSD-3-Clause](https://github.com/aws/aws-sdk-go-v2/blob/v1.17.3/internal/sync/singleflight/LICENSE))
|
||||
- [github.com/aws/aws-sdk-go-v2/service/internal/presigned-url](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/internal/presigned-url) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/service/internal/presigned-url/v1.9.17/service/internal/presigned-url/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2/service/ssm](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/ssm) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/service/ssm/v1.35.0/service/ssm/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2/service/sso](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/sso) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/service/sso/v1.11.23/service/sso/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2/service/ssooidc](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/ssooidc) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/service/ssooidc/v1.13.5/service/ssooidc/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2/service/sts](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/sts) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/service/sts/v1.16.19/service/sts/LICENSE.txt))
|
||||
- [github.com/aws/smithy-go](https://pkg.go.dev/github.com/aws/smithy-go) ([Apache-2.0](https://github.com/aws/smithy-go/blob/v1.13.5/LICENSE))
|
||||
- [github.com/aws/smithy-go/internal/sync/singleflight](https://pkg.go.dev/github.com/aws/smithy-go/internal/sync/singleflight) ([BSD-3-Clause](https://github.com/aws/smithy-go/blob/v1.13.5/internal/sync/singleflight/LICENSE))
|
||||
- [github.com/coreos/go-iptables/iptables](https://pkg.go.dev/github.com/coreos/go-iptables/iptables) ([Apache-2.0](https://github.com/coreos/go-iptables/blob/v0.6.0/LICENSE))
|
||||
- [github.com/fxamacker/cbor/v2](https://pkg.go.dev/github.com/fxamacker/cbor/v2) ([MIT](https://github.com/fxamacker/cbor/blob/v2.4.0/LICENSE))
|
||||
- [github.com/godbus/dbus/v5](https://pkg.go.dev/github.com/godbus/dbus/v5) ([BSD-2-Clause](https://github.com/godbus/dbus/blob/v5.0.6/LICENSE))
|
||||
@@ -19,6 +34,7 @@ and [iOS][]. See also the dependencies in the [Tailscale CLI][].
|
||||
- [github.com/hdevalence/ed25519consensus](https://pkg.go.dev/github.com/hdevalence/ed25519consensus) ([BSD-3-Clause](https://github.com/hdevalence/ed25519consensus/blob/c00d1f31bab3/LICENSE))
|
||||
- [github.com/illarion/gonotify](https://pkg.go.dev/github.com/illarion/gonotify) ([MIT](https://github.com/illarion/gonotify/blob/v1.0.1/LICENSE))
|
||||
- [github.com/insomniacslk/dhcp](https://pkg.go.dev/github.com/insomniacslk/dhcp) ([BSD-3-Clause](https://github.com/insomniacslk/dhcp/blob/de60144f33f8/LICENSE))
|
||||
- [github.com/jmespath/go-jmespath](https://pkg.go.dev/github.com/jmespath/go-jmespath) ([Apache-2.0](https://github.com/jmespath/go-jmespath/blob/v0.4.0/LICENSE))
|
||||
- [github.com/josharian/native](https://pkg.go.dev/github.com/josharian/native) ([MIT](https://github.com/josharian/native/blob/5c7d0dd6ab86/license))
|
||||
- [github.com/jsimonetti/rtnetlink](https://pkg.go.dev/github.com/jsimonetti/rtnetlink) ([MIT](https://github.com/jsimonetti/rtnetlink/blob/d380b505068b/LICENSE.md))
|
||||
- [github.com/klauspost/compress](https://pkg.go.dev/github.com/klauspost/compress) ([Apache-2.0](https://github.com/klauspost/compress/blob/v1.15.11/LICENSE))
|
||||
@@ -33,7 +49,7 @@ and [iOS][]. See also the dependencies in the [Tailscale CLI][].
|
||||
- [github.com/tailscale/golang-x-crypto](https://pkg.go.dev/github.com/tailscale/golang-x-crypto) ([BSD-3-Clause](https://github.com/tailscale/golang-x-crypto/blob/bc99ab8c2d17/LICENSE))
|
||||
- [github.com/tailscale/goupnp](https://pkg.go.dev/github.com/tailscale/goupnp) ([BSD-2-Clause](https://github.com/tailscale/goupnp/blob/c64d0f06ea05/LICENSE))
|
||||
- [github.com/tailscale/netlink](https://pkg.go.dev/github.com/tailscale/netlink) ([Apache-2.0](https://github.com/tailscale/netlink/blob/cabfb018fe85/LICENSE))
|
||||
- [github.com/tailscale/wireguard-go](https://pkg.go.dev/github.com/tailscale/wireguard-go) ([MIT](https://github.com/tailscale/wireguard-go/blob/4fa124729667/LICENSE))
|
||||
- [github.com/tailscale/wireguard-go](https://pkg.go.dev/github.com/tailscale/wireguard-go) ([MIT](https://github.com/tailscale/wireguard-go/blob/af172621b4dd/LICENSE))
|
||||
- [github.com/tcnksm/go-httpstat](https://pkg.go.dev/github.com/tcnksm/go-httpstat) ([MIT](https://github.com/tcnksm/go-httpstat/blob/v0.2.0/LICENSE))
|
||||
- [github.com/u-root/uio](https://pkg.go.dev/github.com/u-root/uio) ([BSD-3-Clause](https://github.com/u-root/uio/blob/c3537552635f/LICENSE))
|
||||
- [github.com/vishvananda/netlink/nl](https://pkg.go.dev/github.com/vishvananda/netlink/nl) ([Apache-2.0](https://github.com/vishvananda/netlink/blob/650dca95af54/LICENSE))
|
||||
@@ -43,13 +59,13 @@ and [iOS][]. See also the dependencies in the [Tailscale CLI][].
|
||||
- [go4.org/netipx](https://pkg.go.dev/go4.org/netipx) ([BSD-3-Clause](https://github.com/go4org/netipx/blob/7e7bdc8411bf/LICENSE))
|
||||
- [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) ([BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/v0.6.0:LICENSE))
|
||||
- [golang.org/x/exp](https://pkg.go.dev/golang.org/x/exp) ([BSD-3-Clause](https://cs.opensource.google/go/x/exp/+/cafedaf6:LICENSE))
|
||||
- [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://cs.opensource.google/go/x/net/+/v0.7.0:LICENSE))
|
||||
- [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://cs.opensource.google/go/x/net/+/v0.8.0:LICENSE))
|
||||
- [golang.org/x/sync/errgroup](https://pkg.go.dev/golang.org/x/sync/errgroup) ([BSD-3-Clause](https://cs.opensource.google/go/x/sync/+/v0.1.0:LICENSE))
|
||||
- [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/v0.5.0:LICENSE))
|
||||
- [golang.org/x/term](https://pkg.go.dev/golang.org/x/term) ([BSD-3-Clause](https://cs.opensource.google/go/x/term/+/v0.5.0:LICENSE))
|
||||
- [golang.org/x/text](https://pkg.go.dev/golang.org/x/text) ([BSD-3-Clause](https://cs.opensource.google/go/x/text/+/v0.7.0:LICENSE))
|
||||
- [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/v0.6.0:LICENSE))
|
||||
- [golang.org/x/term](https://pkg.go.dev/golang.org/x/term) ([BSD-3-Clause](https://cs.opensource.google/go/x/term/+/v0.6.0:LICENSE))
|
||||
- [golang.org/x/text](https://pkg.go.dev/golang.org/x/text) ([BSD-3-Clause](https://cs.opensource.google/go/x/text/+/v0.8.0:LICENSE))
|
||||
- [golang.org/x/time/rate](https://pkg.go.dev/golang.org/x/time/rate) ([BSD-3-Clause](https://cs.opensource.google/go/x/time/+/579cf78f:LICENSE))
|
||||
- [gvisor.dev/gvisor/pkg](https://pkg.go.dev/gvisor.dev/gvisor/pkg) ([Apache-2.0](https://github.com/google/gvisor/blob/703fd9b7fbc0/LICENSE))
|
||||
- [gvisor.dev/gvisor/pkg](https://pkg.go.dev/gvisor.dev/gvisor/pkg) ([Apache-2.0](https://github.com/google/gvisor/blob/162ed5ef888d/LICENSE))
|
||||
- [inet.af/peercred](https://pkg.go.dev/inet.af/peercred) ([BSD-3-Clause](https://github.com/inetaf/peercred/blob/0893ea02156a/LICENSE))
|
||||
- [nhooyr.io/websocket](https://pkg.go.dev/nhooyr.io/websocket) ([MIT](https://github.com/nhooyr/websocket/blob/v1.8.7/LICENSE.txt))
|
||||
- [tailscale.com](https://pkg.go.dev/tailscale.com) ([BSD-3-Clause](https://github.com/tailscale/tailscale/blob/HEAD/LICENSE))
|
||||
|
||||
@@ -53,8 +53,8 @@ Some packages may only be included on certain architectures or operating systems
|
||||
- [github.com/klauspost/compress/zstd/internal/xxhash](https://pkg.go.dev/github.com/klauspost/compress/zstd/internal/xxhash) ([MIT](https://github.com/klauspost/compress/blob/v1.15.4/zstd/internal/xxhash/LICENSE.txt))
|
||||
- [github.com/kortschak/wol](https://pkg.go.dev/github.com/kortschak/wol) ([BSD-3-Clause](https://github.com/kortschak/wol/blob/da482cc4850a/LICENSE))
|
||||
- [github.com/kr/fs](https://pkg.go.dev/github.com/kr/fs) ([BSD-3-Clause](https://github.com/kr/fs/blob/v0.1.0/LICENSE))
|
||||
- [github.com/mattn/go-colorable](https://pkg.go.dev/github.com/mattn/go-colorable) ([MIT](https://github.com/mattn/go-colorable/blob/v0.1.12/LICENSE))
|
||||
- [github.com/mattn/go-isatty](https://pkg.go.dev/github.com/mattn/go-isatty) ([MIT](https://github.com/mattn/go-isatty/blob/v0.0.14/LICENSE))
|
||||
- [github.com/mattn/go-colorable](https://pkg.go.dev/github.com/mattn/go-colorable) ([MIT](https://github.com/mattn/go-colorable/blob/v0.1.13/LICENSE))
|
||||
- [github.com/mattn/go-isatty](https://pkg.go.dev/github.com/mattn/go-isatty) ([MIT](https://github.com/mattn/go-isatty/blob/v0.0.17/LICENSE))
|
||||
- [github.com/mdlayher/genetlink](https://pkg.go.dev/github.com/mdlayher/genetlink) ([MIT](https://github.com/mdlayher/genetlink/blob/v1.2.0/LICENSE.md))
|
||||
- [github.com/mdlayher/netlink](https://pkg.go.dev/github.com/mdlayher/netlink) ([MIT](https://github.com/mdlayher/netlink/blob/v1.7.1/LICENSE.md))
|
||||
- [github.com/mdlayher/sdnotify](https://pkg.go.dev/github.com/mdlayher/sdnotify) ([MIT](https://github.com/mdlayher/sdnotify/blob/v1.0.0/LICENSE.md))
|
||||
@@ -67,7 +67,7 @@ Some packages may only be included on certain architectures or operating systems
|
||||
- [github.com/tailscale/certstore](https://pkg.go.dev/github.com/tailscale/certstore) ([MIT](https://github.com/tailscale/certstore/blob/78d6e1c49d8d/LICENSE.md))
|
||||
- [github.com/tailscale/golang-x-crypto](https://pkg.go.dev/github.com/tailscale/golang-x-crypto) ([BSD-3-Clause](https://github.com/tailscale/golang-x-crypto/blob/bc99ab8c2d17/LICENSE))
|
||||
- [github.com/tailscale/netlink](https://pkg.go.dev/github.com/tailscale/netlink) ([Apache-2.0](https://github.com/tailscale/netlink/blob/cabfb018fe85/LICENSE))
|
||||
- [github.com/tailscale/wireguard-go](https://pkg.go.dev/github.com/tailscale/wireguard-go) ([MIT](https://github.com/tailscale/wireguard-go/blob/4fa124729667/LICENSE))
|
||||
- [github.com/tailscale/wireguard-go](https://pkg.go.dev/github.com/tailscale/wireguard-go) ([MIT](https://github.com/tailscale/wireguard-go/blob/af172621b4dd/LICENSE))
|
||||
- [github.com/tcnksm/go-httpstat](https://pkg.go.dev/github.com/tcnksm/go-httpstat) ([MIT](https://github.com/tcnksm/go-httpstat/blob/v0.2.0/LICENSE))
|
||||
- [github.com/toqueteos/webbrowser](https://pkg.go.dev/github.com/toqueteos/webbrowser) ([MIT](https://github.com/toqueteos/webbrowser/blob/v1.2.0/LICENSE.md))
|
||||
- [github.com/u-root/u-root/pkg/termios](https://pkg.go.dev/github.com/u-root/u-root/pkg/termios) ([BSD-3-Clause](https://github.com/u-root/u-root/blob/948a78c969ad/LICENSE))
|
||||
@@ -79,16 +79,16 @@ Some packages may only be included on certain architectures or operating systems
|
||||
- [go4.org/netipx](https://pkg.go.dev/go4.org/netipx) ([BSD-3-Clause](https://github.com/go4org/netipx/blob/7e7bdc8411bf/LICENSE))
|
||||
- [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) ([BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/v0.6.0:LICENSE))
|
||||
- [golang.org/x/exp](https://pkg.go.dev/golang.org/x/exp) ([BSD-3-Clause](https://cs.opensource.google/go/x/exp/+/47842c84:LICENSE))
|
||||
- [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://cs.opensource.google/go/x/net/+/v0.7.0:LICENSE))
|
||||
- [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://cs.opensource.google/go/x/net/+/v0.8.0:LICENSE))
|
||||
- [golang.org/x/sync/errgroup](https://pkg.go.dev/golang.org/x/sync/errgroup) ([BSD-3-Clause](https://cs.opensource.google/go/x/sync/+/v0.1.0:LICENSE))
|
||||
- [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/v0.5.0:LICENSE))
|
||||
- [golang.org/x/term](https://pkg.go.dev/golang.org/x/term) ([BSD-3-Clause](https://cs.opensource.google/go/x/term/+/v0.5.0:LICENSE))
|
||||
- [golang.org/x/text](https://pkg.go.dev/golang.org/x/text) ([BSD-3-Clause](https://cs.opensource.google/go/x/text/+/v0.7.0:LICENSE))
|
||||
- [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/v0.6.0:LICENSE))
|
||||
- [golang.org/x/term](https://pkg.go.dev/golang.org/x/term) ([BSD-3-Clause](https://cs.opensource.google/go/x/term/+/v0.6.0:LICENSE))
|
||||
- [golang.org/x/text](https://pkg.go.dev/golang.org/x/text) ([BSD-3-Clause](https://cs.opensource.google/go/x/text/+/v0.8.0:LICENSE))
|
||||
- [golang.org/x/time/rate](https://pkg.go.dev/golang.org/x/time/rate) ([BSD-3-Clause](https://cs.opensource.google/go/x/time/+/579cf78f:LICENSE))
|
||||
- [golang.zx2c4.com/wintun](https://pkg.go.dev/golang.zx2c4.com/wintun) ([MIT](https://git.zx2c4.com/wintun-go/tree/LICENSE?id=0fa3db229ce2))
|
||||
- [golang.zx2c4.com/wireguard/windows/tunnel/winipcfg](https://pkg.go.dev/golang.zx2c4.com/wireguard/windows/tunnel/winipcfg) ([MIT](https://git.zx2c4.com/wireguard-windows/tree/COPYING?h=v0.5.3))
|
||||
- [gopkg.in/yaml.v2](https://pkg.go.dev/gopkg.in/yaml.v2) ([Apache-2.0](https://github.com/go-yaml/yaml/blob/v2.4.0/LICENSE))
|
||||
- [gvisor.dev/gvisor/pkg](https://pkg.go.dev/gvisor.dev/gvisor/pkg) ([Apache-2.0](https://github.com/google/gvisor/blob/703fd9b7fbc0/LICENSE))
|
||||
- [gvisor.dev/gvisor/pkg](https://pkg.go.dev/gvisor.dev/gvisor/pkg) ([Apache-2.0](https://github.com/google/gvisor/blob/162ed5ef888d/LICENSE))
|
||||
- [inet.af/peercred](https://pkg.go.dev/inet.af/peercred) ([BSD-3-Clause](https://github.com/inetaf/peercred/blob/0893ea02156a/LICENSE))
|
||||
- [inet.af/wf](https://pkg.go.dev/inet.af/wf) ([BSD-3-Clause](https://github.com/inetaf/wf/blob/50d96caab2f6/LICENSE))
|
||||
- [k8s.io/client-go/util/homedir](https://pkg.go.dev/k8s.io/client-go/util/homedir) ([Apache-2.0](https://github.com/kubernetes/client-go/blob/v0.25.0/LICENSE))
|
||||
@@ -96,4 +96,5 @@ Some packages may only be included on certain architectures or operating systems
|
||||
- [sigs.k8s.io/yaml](https://pkg.go.dev/sigs.k8s.io/yaml) ([MIT](https://github.com/kubernetes-sigs/yaml/blob/v1.3.0/LICENSE))
|
||||
- [software.sslmate.com/src/go-pkcs12](https://pkg.go.dev/software.sslmate.com/src/go-pkcs12) ([BSD-3-Clause](https://github.com/SSLMate/go-pkcs12/blob/v0.2.0/LICENSE))
|
||||
- [tailscale.com](https://pkg.go.dev/tailscale.com) ([BSD-3-Clause](https://github.com/tailscale/tailscale/blob/HEAD/LICENSE))
|
||||
- [tailscale.com/tempfork/device](https://pkg.go.dev/tailscale.com/tempfork/device) ([MIT](https://github.com/tailscale/tailscale/blob/HEAD/tempfork/device/LICENSE))
|
||||
- [tailscale.com/tempfork/gliderlabs/ssh](https://pkg.go.dev/tailscale.com/tempfork/gliderlabs/ssh) ([BSD-3-Clause](https://github.com/tailscale/tailscale/blob/HEAD/tempfork/gliderlabs/ssh/LICENSE))
|
||||
|
||||
@@ -17,7 +17,9 @@ Windows][]. See also the dependencies in the [Tailscale CLI][].
|
||||
- [github.com/dblohm7/wingoes](https://pkg.go.dev/github.com/dblohm7/wingoes) ([BSD-3-Clause](https://github.com/dblohm7/wingoes/blob/2b26ab7fb5f9/LICENSE))
|
||||
- [github.com/fxamacker/cbor/v2](https://pkg.go.dev/github.com/fxamacker/cbor/v2) ([MIT](https://github.com/fxamacker/cbor/blob/v2.4.0/LICENSE))
|
||||
- [github.com/golang/groupcache/lru](https://pkg.go.dev/github.com/golang/groupcache/lru) ([Apache-2.0](https://github.com/golang/groupcache/blob/41bb18bfe9da/LICENSE))
|
||||
- [github.com/google/btree](https://pkg.go.dev/github.com/google/btree) ([Apache-2.0](https://github.com/google/btree/blob/v1.1.2/LICENSE))
|
||||
- [github.com/google/uuid](https://pkg.go.dev/github.com/google/uuid) ([BSD-3-Clause](https://github.com/google/uuid/blob/v1.3.0/LICENSE))
|
||||
- [github.com/gregjones/httpcache](https://pkg.go.dev/github.com/gregjones/httpcache) ([MIT](https://github.com/gregjones/httpcache/blob/901d90724c79/LICENSE.txt))
|
||||
- [github.com/hdevalence/ed25519consensus](https://pkg.go.dev/github.com/hdevalence/ed25519consensus) ([BSD-3-Clause](https://github.com/hdevalence/ed25519consensus/blob/c00d1f31bab3/LICENSE))
|
||||
- [github.com/josharian/native](https://pkg.go.dev/github.com/josharian/native) ([MIT](https://github.com/josharian/native/blob/5c7d0dd6ab86/license))
|
||||
- [github.com/jsimonetti/rtnetlink](https://pkg.go.dev/github.com/jsimonetti/rtnetlink) ([MIT](https://github.com/jsimonetti/rtnetlink/blob/d380b505068b/LICENSE.md))
|
||||
@@ -28,8 +30,9 @@ Windows][]. See also the dependencies in the [Tailscale CLI][].
|
||||
- [github.com/mdlayher/socket](https://pkg.go.dev/github.com/mdlayher/socket) ([MIT](https://github.com/mdlayher/socket/blob/v0.4.0/LICENSE.md))
|
||||
- [github.com/mitchellh/go-ps](https://pkg.go.dev/github.com/mitchellh/go-ps) ([MIT](https://github.com/mitchellh/go-ps/blob/v1.0.0/LICENSE.md))
|
||||
- [github.com/nfnt/resize](https://pkg.go.dev/github.com/nfnt/resize) ([ISC](https://github.com/nfnt/resize/blob/83c6a9932646/LICENSE))
|
||||
- [github.com/peterbourgon/diskv](https://pkg.go.dev/github.com/peterbourgon/diskv) ([MIT](https://github.com/peterbourgon/diskv/blob/v2.0.1/LICENSE))
|
||||
- [github.com/skip2/go-qrcode](https://pkg.go.dev/github.com/skip2/go-qrcode) ([MIT](https://github.com/skip2/go-qrcode/blob/da1b6568686e/LICENSE))
|
||||
- [github.com/tailscale/walk](https://pkg.go.dev/github.com/tailscale/walk) ([BSD-3-Clause](https://github.com/tailscale/walk/blob/31689615ddb4/LICENSE))
|
||||
- [github.com/tailscale/walk](https://pkg.go.dev/github.com/tailscale/walk) ([BSD-3-Clause](https://github.com/tailscale/walk/blob/f6f2f17d9da1/LICENSE))
|
||||
- [github.com/tailscale/win](https://pkg.go.dev/github.com/tailscale/win) ([BSD-3-Clause](https://github.com/tailscale/win/blob/ad93eed16885/LICENSE))
|
||||
- [github.com/tc-hib/winres](https://pkg.go.dev/github.com/tc-hib/winres) ([0BSD](https://github.com/tc-hib/winres/blob/v0.1.6/LICENSE))
|
||||
- [github.com/x448/float16](https://pkg.go.dev/github.com/x448/float16) ([MIT](https://github.com/x448/float16/blob/v0.8.4/LICENSE))
|
||||
@@ -38,11 +41,12 @@ Windows][]. See also the dependencies in the [Tailscale CLI][].
|
||||
- [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) ([BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/v0.6.0:LICENSE))
|
||||
- [golang.org/x/exp](https://pkg.go.dev/golang.org/x/exp) ([BSD-3-Clause](https://cs.opensource.google/go/x/exp/+/cafedaf6:LICENSE))
|
||||
- [golang.org/x/image/bmp](https://pkg.go.dev/golang.org/x/image/bmp) ([BSD-3-Clause](https://cs.opensource.google/go/x/image/+/v0.5.0:LICENSE))
|
||||
- [golang.org/x/mod](https://pkg.go.dev/golang.org/x/mod) ([BSD-3-Clause](https://cs.opensource.google/go/x/mod/+/v0.7.0:LICENSE))
|
||||
- [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://cs.opensource.google/go/x/net/+/v0.7.0:LICENSE))
|
||||
- [golang.org/x/mod](https://pkg.go.dev/golang.org/x/mod) ([BSD-3-Clause](https://cs.opensource.google/go/x/mod/+/v0.9.0:LICENSE))
|
||||
- [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://cs.opensource.google/go/x/net/+/v0.8.0:LICENSE))
|
||||
- [golang.org/x/sync/errgroup](https://pkg.go.dev/golang.org/x/sync/errgroup) ([BSD-3-Clause](https://cs.opensource.google/go/x/sync/+/v0.1.0:LICENSE))
|
||||
- [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/v0.5.0:LICENSE))
|
||||
- [golang.org/x/term](https://pkg.go.dev/golang.org/x/term) ([BSD-3-Clause](https://cs.opensource.google/go/x/term/+/v0.5.0:LICENSE))
|
||||
- [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/v0.6.0:LICENSE))
|
||||
- [golang.org/x/term](https://pkg.go.dev/golang.org/x/term) ([BSD-3-Clause](https://cs.opensource.google/go/x/term/+/v0.6.0:LICENSE))
|
||||
- [golang.org/x/text](https://pkg.go.dev/golang.org/x/text) ([BSD-3-Clause](https://cs.opensource.google/go/x/text/+/v0.8.0:LICENSE))
|
||||
- [golang.zx2c4.com/wintun](https://pkg.go.dev/golang.zx2c4.com/wintun) ([MIT](https://git.zx2c4.com/wintun-go/tree/LICENSE?id=0fa3db229ce2))
|
||||
- [golang.zx2c4.com/wireguard/windows/tunnel/winipcfg](https://pkg.go.dev/golang.zx2c4.com/wireguard/windows/tunnel/winipcfg) ([MIT](https://git.zx2c4.com/wireguard-windows/tree/COPYING?h=v0.5.3))
|
||||
- [gopkg.in/Knetic/govaluate.v3](https://pkg.go.dev/gopkg.in/Knetic/govaluate.v3) ([MIT](https://github.com/Knetic/govaluate/blob/v3.0.0/LICENSE))
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package sockstatlog provides a logger for capturing and storing network socket stats.
|
||||
// Package sockstatlog provides a logger for capturing network socket stats for debugging.
|
||||
// Stats are collected at a frequency of 10 Hz and logged to disk.
|
||||
// Stats are only uploaded to the log server on demand.
|
||||
package sockstatlog
|
||||
|
||||
import (
|
||||
@@ -12,11 +14,13 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"tailscale.com/logpolicy"
|
||||
"tailscale.com/logtail"
|
||||
"tailscale.com/logtail/filch"
|
||||
"tailscale.com/net/netmon"
|
||||
"tailscale.com/net/sockstats"
|
||||
"tailscale.com/smallzstd"
|
||||
"tailscale.com/types/logger"
|
||||
@@ -24,20 +28,38 @@ import (
|
||||
"tailscale.com/util/mak"
|
||||
)
|
||||
|
||||
// pollPeriod specifies how often to poll for socket stats.
|
||||
const pollPeriod = time.Second / 10
|
||||
// pollInterval specifies how often to poll for socket stats.
|
||||
const pollInterval = time.Second / 10
|
||||
|
||||
// logInterval specifies how often to log sockstat events to disk.
|
||||
// This delay is added to prevent an infinite loop when logs are uploaded,
|
||||
// which itself creates additional sockstat events.
|
||||
const logInterval = 10 * time.Second
|
||||
|
||||
// maxLogFileSize specifies the maximum size of a log file before it is rotated.
|
||||
// Our logs are fairly compact, and we are mostly only looking at a few hours of data.
|
||||
// Combined with the fact that these are often uploaded over cellular connections,
|
||||
// we keep this relatively small.
|
||||
const maxLogFileSize = 5 << 20 // 5 MB
|
||||
|
||||
// Logger logs statistics about network sockets.
|
||||
type Logger struct {
|
||||
// enabled identifies whether the logger is enabled.
|
||||
enabled atomic.Bool
|
||||
|
||||
ctx context.Context
|
||||
cancelFn context.CancelFunc
|
||||
|
||||
ticker *time.Ticker
|
||||
logf logger.Logf
|
||||
// eventCh is used to pass events from the poller to the logger.
|
||||
eventCh chan event
|
||||
|
||||
logf logger.Logf
|
||||
|
||||
// logger is the underlying logtail logger than manages log files on disk
|
||||
// and uploading to the log server.
|
||||
logger *logtail.Logger
|
||||
filch *filch.Filch
|
||||
tr *http.Transport
|
||||
tr http.RoundTripper
|
||||
}
|
||||
|
||||
// deltaStat represents the bytes transferred during a time period.
|
||||
@@ -62,15 +84,17 @@ type event struct {
|
||||
// SockstatLogID reproducibly derives a new logid.PrivateID for sockstat logging from a node's public backend log ID.
|
||||
// The returned PrivateID is the sha256 sum of logID + "sockstat".
|
||||
// If a node's public log ID becomes known, it is trivial to spoof sockstat logs for that node.
|
||||
// Given the this is just for debugging, we're not too concerned about that.
|
||||
// Given that this is just for debugging, we're not too concerned about that.
|
||||
func SockstatLogID(logID logid.PublicID) logid.PrivateID {
|
||||
return logid.PrivateID(sha256.Sum256([]byte(logID.String() + "sockstat")))
|
||||
}
|
||||
|
||||
// NewLogger returns a new Logger that will store stats in logdir.
|
||||
// On platforms that do not support sockstat logging, a nil Logger will be returned.
|
||||
// The returned Logger must be shut down with Shutdown when it is no longer needed.
|
||||
func NewLogger(logdir string, logf logger.Logf, logID logid.PublicID) (*Logger, error) {
|
||||
// The returned Logger is not yet enabled, and must be shut down with Shutdown when it is no longer needed.
|
||||
// Logs will be uploaded to the log server using a new log ID derived from the provided backend logID.
|
||||
// The netMon parameter is optional; if non-nil it's used to do faster interface lookups.
|
||||
func NewLogger(logdir string, logf logger.Logf, logID logid.PublicID, netMon *netmon.Monitor) (*Logger, error) {
|
||||
if !sockstats.IsAvailable {
|
||||
return nil, nil
|
||||
}
|
||||
@@ -79,19 +103,18 @@ func NewLogger(logdir string, logf logger.Logf, logID logid.PublicID) (*Logger,
|
||||
return nil, err
|
||||
}
|
||||
filchPrefix := filepath.Join(logdir, "sockstats")
|
||||
filch, err := filch.New(filchPrefix, filch.Options{ReplaceStderr: false})
|
||||
filch, err := filch.New(filchPrefix, filch.Options{
|
||||
MaxFileSize: maxLogFileSize,
|
||||
ReplaceStderr: false,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
logger := &Logger{
|
||||
ctx: ctx,
|
||||
cancelFn: cancel,
|
||||
ticker: time.NewTicker(pollPeriod),
|
||||
logf: logf,
|
||||
filch: filch,
|
||||
tr: logpolicy.NewLogtailTransport(logtail.DefaultHost),
|
||||
logf: logf,
|
||||
filch: filch,
|
||||
tr: logpolicy.NewLogtailTransport(logtail.DefaultHost, netMon),
|
||||
}
|
||||
logger.logger = logtail.NewLogger(logtail.Config{
|
||||
BaseURL: logpolicy.LogURL(),
|
||||
@@ -113,26 +136,52 @@ func NewLogger(logdir string, logf logger.Logf, logID logid.PublicID) (*Logger,
|
||||
|
||||
HTTPC: &http.Client{Transport: logger.tr},
|
||||
}, logf)
|
||||
|
||||
go logger.poll()
|
||||
logger.logger.SetSockstatsLabel(sockstats.LabelSockstatlogLogger)
|
||||
|
||||
return logger, nil
|
||||
}
|
||||
|
||||
// SetLoggingEnabled enables or disables logging.
|
||||
// When disabled, socket stats are not polled and no new logs are written to disk.
|
||||
// Existing logs can still be fetched via the C2N API.
|
||||
func (l *Logger) SetLoggingEnabled(v bool) {
|
||||
old := l.enabled.Load()
|
||||
if old != v && l.enabled.CompareAndSwap(old, v) {
|
||||
if v {
|
||||
if l.eventCh == nil {
|
||||
// eventCh should be large enough for the number of events that will occur within logInterval.
|
||||
// Add an extra second's worth of events to ensure we don't drop any.
|
||||
l.eventCh = make(chan event, (logInterval+time.Second)/pollInterval)
|
||||
}
|
||||
l.ctx, l.cancelFn = context.WithCancel(context.Background())
|
||||
go l.poll()
|
||||
go l.logEvents()
|
||||
} else {
|
||||
l.cancelFn()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Logger) Write(p []byte) (int, error) {
|
||||
return l.logger.Write(p)
|
||||
}
|
||||
|
||||
// poll fetches the current socket stats at the configured time interval,
|
||||
// calculates the delta since the last poll, and logs any non-zero values.
|
||||
// calculates the delta since the last poll,
|
||||
// and writes any non-zero values to the logger event channel.
|
||||
// This method does not return.
|
||||
func (l *Logger) poll() {
|
||||
// last is the last set of socket stats we saw.
|
||||
var lastStats *sockstats.SockStats
|
||||
var lastTime time.Time
|
||||
|
||||
enc := json.NewEncoder(l.logger)
|
||||
ticker := time.NewTicker(pollInterval)
|
||||
for {
|
||||
select {
|
||||
case <-l.ctx.Done():
|
||||
ticker.Stop()
|
||||
return
|
||||
case t := <-l.ticker.C:
|
||||
case t := <-ticker.C:
|
||||
stats := sockstats.Get()
|
||||
if lastStats != nil {
|
||||
diffstats := delta(lastStats, stats)
|
||||
@@ -145,9 +194,7 @@ func (l *Logger) poll() {
|
||||
if stats.CurrentInterfaceCellular {
|
||||
e.IsCellularInterface = 1
|
||||
}
|
||||
if err := enc.Encode(e); err != nil {
|
||||
l.logf("sockstatlog: error encoding log: %v", err)
|
||||
}
|
||||
l.eventCh <- e
|
||||
}
|
||||
}
|
||||
lastTime = t
|
||||
@@ -156,6 +203,34 @@ func (l *Logger) poll() {
|
||||
}
|
||||
}
|
||||
|
||||
// logEvents reads events from the event channel at logInterval and logs them to disk.
|
||||
// This method does not return.
|
||||
func (l *Logger) logEvents() {
|
||||
enc := json.NewEncoder(l)
|
||||
flush := func() {
|
||||
for {
|
||||
select {
|
||||
case e := <-l.eventCh:
|
||||
if err := enc.Encode(e); err != nil {
|
||||
l.logf("sockstatlog: error encoding log: %v", err)
|
||||
}
|
||||
default:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
ticker := time.NewTicker(logInterval)
|
||||
for {
|
||||
select {
|
||||
case <-l.ctx.Done():
|
||||
ticker.Stop()
|
||||
return
|
||||
case <-ticker.C:
|
||||
flush()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Logger) LogID() string {
|
||||
if l.logger == nil {
|
||||
return ""
|
||||
@@ -169,11 +244,18 @@ func (l *Logger) Flush() {
|
||||
}
|
||||
|
||||
func (l *Logger) Shutdown() {
|
||||
l.ticker.Stop()
|
||||
l.logger.Shutdown(l.ctx)
|
||||
l.cancelFn()
|
||||
if l.cancelFn != nil {
|
||||
l.cancelFn()
|
||||
}
|
||||
l.filch.Close()
|
||||
l.tr.CloseIdleConnections()
|
||||
l.logger.Shutdown(context.Background())
|
||||
|
||||
type closeIdler interface {
|
||||
CloseIdleConnections()
|
||||
}
|
||||
if tr, ok := l.tr.(closeIdler); ok {
|
||||
tr.CloseIdleConnections()
|
||||
}
|
||||
}
|
||||
|
||||
// delta calculates the delta stats between two SockStats snapshots.
|
||||
|
||||
@@ -8,8 +8,29 @@ import (
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"tailscale.com/net/sockstats"
|
||||
"tailscale.com/tstest"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/logid"
|
||||
)
|
||||
|
||||
func TestResourceCleanup(t *testing.T) {
|
||||
if !sockstats.IsAvailable {
|
||||
t.Skip("sockstats not available")
|
||||
}
|
||||
tstest.ResourceCheck(t)
|
||||
td := t.TempDir()
|
||||
id, err := logid.NewPrivateID()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
lg, err := NewLogger(td, logger.Discard, id.Public(), nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
lg.Write([]byte("hello"))
|
||||
lg.Shutdown()
|
||||
}
|
||||
|
||||
func TestDelta(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
@@ -36,6 +37,7 @@ import (
|
||||
"tailscale.com/net/dnscache"
|
||||
"tailscale.com/net/dnsfallback"
|
||||
"tailscale.com/net/netknob"
|
||||
"tailscale.com/net/netmon"
|
||||
"tailscale.com/net/netns"
|
||||
"tailscale.com/net/tlsdial"
|
||||
"tailscale.com/net/tshttpproxy"
|
||||
@@ -52,6 +54,8 @@ import (
|
||||
"tailscale.com/version/distro"
|
||||
)
|
||||
|
||||
func inTest() bool { return flag.Lookup("test.v") != nil }
|
||||
|
||||
var getLogTargetOnce struct {
|
||||
sync.Once
|
||||
v string // URL of logs server, or empty for default
|
||||
@@ -447,14 +451,15 @@ func tryFixLogStateLocation(dir, cmdname string) {
|
||||
|
||||
// New returns a new log policy (a logger and its instance ID) for a
|
||||
// given collection name.
|
||||
func New(collection string) *Policy {
|
||||
return NewWithConfigPath(collection, "", "")
|
||||
// The netMon parameter is optional; if non-nil it's used to do faster interface lookups.
|
||||
func New(collection string, netMon *netmon.Monitor) *Policy {
|
||||
return NewWithConfigPath(collection, "", "", netMon)
|
||||
}
|
||||
|
||||
// NewWithConfigPath is identical to New,
|
||||
// but uses the specified directory and command name.
|
||||
// If either is empty, it derives them automatically.
|
||||
func NewWithConfigPath(collection, dir, cmdName string) *Policy {
|
||||
func NewWithConfigPath(collection, dir, cmdName string, netMon *netmon.Monitor) *Policy {
|
||||
var lflags int
|
||||
if term.IsTerminal(2) || runtime.GOOS == "windows" {
|
||||
lflags = 0
|
||||
@@ -551,7 +556,7 @@ func NewWithConfigPath(collection, dir, cmdName string) *Policy {
|
||||
}
|
||||
return w
|
||||
},
|
||||
HTTPC: &http.Client{Transport: NewLogtailTransport(logtail.DefaultHost)},
|
||||
HTTPC: &http.Client{Transport: NewLogtailTransport(logtail.DefaultHost, netMon)},
|
||||
}
|
||||
if collection == logtail.CollectionNode {
|
||||
conf.MetricsDelta = clientmetric.EncodeLogTailMetricsDelta
|
||||
@@ -559,14 +564,14 @@ func NewWithConfigPath(collection, dir, cmdName string) *Policy {
|
||||
conf.IncludeProcSequence = true
|
||||
}
|
||||
|
||||
if envknob.NoLogsNoSupport() {
|
||||
if envknob.NoLogsNoSupport() || inTest() {
|
||||
log.Println("You have disabled logging. Tailscale will not be able to provide support.")
|
||||
conf.HTTPC = &http.Client{Transport: noopPretendSuccessTransport{}}
|
||||
} else if val := getLogTarget(); val != "" {
|
||||
log.Println("You have enabled a non-default log target. Doing without being told to by Tailscale staff or your network administrator will make getting support difficult.")
|
||||
conf.BaseURL = val
|
||||
u, _ := url.Parse(val)
|
||||
conf.HTTPC = &http.Client{Transport: NewLogtailTransport(u.Host)}
|
||||
conf.HTTPC = &http.Client{Transport: NewLogtailTransport(u.Host, netMon)}
|
||||
}
|
||||
|
||||
filchOptions := filch.Options{
|
||||
@@ -667,12 +672,74 @@ func (p *Policy) Shutdown(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewLogtailTransport returns an HTTP Transport particularly suited to uploading
|
||||
// logs to the given host name. This includes:
|
||||
// - If DNS lookup fails, consult the bootstrap DNS list of Tailscale hostnames.
|
||||
// MakeDialFunc creates a net.Dialer.DialContext function specialized for use
|
||||
// by logtail.
|
||||
// It does the following:
|
||||
// - If DNS lookup fails, consults the bootstrap DNS list of Tailscale hostnames.
|
||||
// - If TLS connection fails, try again using LetsEncrypt's built-in root certificate,
|
||||
// for the benefit of older OS platforms which might not include it.
|
||||
func NewLogtailTransport(host string) *http.Transport {
|
||||
//
|
||||
// The netMon parameter is optional; if non-nil it's used to do faster interface lookups.
|
||||
func MakeDialFunc(netMon *netmon.Monitor) func(ctx context.Context, netw, addr string) (net.Conn, error) {
|
||||
return func(ctx context.Context, netw, addr string) (net.Conn, error) {
|
||||
return dialContext(ctx, netw, addr, netMon)
|
||||
}
|
||||
}
|
||||
|
||||
func dialContext(ctx context.Context, netw, addr string, netMon *netmon.Monitor) (net.Conn, error) {
|
||||
nd := netns.FromDialer(log.Printf, netMon, &net.Dialer{
|
||||
Timeout: 30 * time.Second,
|
||||
KeepAlive: netknob.PlatformTCPKeepAlive(),
|
||||
})
|
||||
t0 := time.Now()
|
||||
c, err := nd.DialContext(ctx, netw, addr)
|
||||
d := time.Since(t0).Round(time.Millisecond)
|
||||
if err == nil {
|
||||
dialLog.Printf("dialed %q in %v", addr, d)
|
||||
return c, nil
|
||||
}
|
||||
|
||||
if version.IsWindowsGUI() && strings.HasPrefix(netw, "tcp") {
|
||||
if c, err := safesocket.Connect(safesocket.DefaultConnectionStrategy("")); err == nil {
|
||||
fmt.Fprintf(c, "CONNECT %s HTTP/1.0\r\n\r\n", addr)
|
||||
br := bufio.NewReader(c)
|
||||
res, err := http.ReadResponse(br, nil)
|
||||
if err == nil && res.StatusCode != 200 {
|
||||
err = errors.New(res.Status)
|
||||
}
|
||||
if err != nil {
|
||||
log.Printf("logtail: CONNECT response error from tailscaled: %v", err)
|
||||
c.Close()
|
||||
} else {
|
||||
dialLog.Printf("connected via tailscaled")
|
||||
return c, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we failed to dial, try again with bootstrap DNS.
|
||||
log.Printf("logtail: dial %q failed: %v (in %v), trying bootstrap...", addr, err, d)
|
||||
dnsCache := &dnscache.Resolver{
|
||||
Forward: dnscache.Get().Forward, // use default cache's forwarder
|
||||
UseLastGood: true,
|
||||
LookupIPFallback: dnsfallback.MakeLookupFunc(log.Printf, netMon),
|
||||
NetMon: netMon,
|
||||
}
|
||||
dialer := dnscache.Dialer(nd.DialContext, dnsCache)
|
||||
c, err = dialer(ctx, netw, addr)
|
||||
if err == nil {
|
||||
log.Printf("logtail: bootstrap dial succeeded")
|
||||
}
|
||||
return c, err
|
||||
}
|
||||
|
||||
// NewLogtailTransport returns an HTTP Transport particularly suited to uploading
|
||||
// logs to the given host name. See DialContext for details on how it works.
|
||||
// The netMon parameter is optional; if non-nil it's used to do faster interface lookups.
|
||||
func NewLogtailTransport(host string, netMon *netmon.Monitor) http.RoundTripper {
|
||||
if inTest() {
|
||||
return noopPretendSuccessTransport{}
|
||||
}
|
||||
// Start with a copy of http.DefaultTransport and tweak it a bit.
|
||||
tr := http.DefaultTransport.(*http.Transport).Clone()
|
||||
|
||||
@@ -685,51 +752,7 @@ func NewLogtailTransport(host string) *http.Transport {
|
||||
tr.DisableCompression = true
|
||||
|
||||
// Log whenever we dial:
|
||||
tr.DialContext = func(ctx context.Context, netw, addr string) (net.Conn, error) {
|
||||
nd := netns.FromDialer(log.Printf, &net.Dialer{
|
||||
Timeout: 30 * time.Second,
|
||||
KeepAlive: netknob.PlatformTCPKeepAlive(),
|
||||
})
|
||||
t0 := time.Now()
|
||||
c, err := nd.DialContext(ctx, netw, addr)
|
||||
d := time.Since(t0).Round(time.Millisecond)
|
||||
if err == nil {
|
||||
dialLog.Printf("dialed %q in %v", addr, d)
|
||||
return c, nil
|
||||
}
|
||||
|
||||
if version.IsWindowsGUI() && strings.HasPrefix(netw, "tcp") {
|
||||
if c, err := safesocket.Connect(safesocket.DefaultConnectionStrategy("")); err == nil {
|
||||
fmt.Fprintf(c, "CONNECT %s HTTP/1.0\r\n\r\n", addr)
|
||||
br := bufio.NewReader(c)
|
||||
res, err := http.ReadResponse(br, nil)
|
||||
if err == nil && res.StatusCode != 200 {
|
||||
err = errors.New(res.Status)
|
||||
}
|
||||
if err != nil {
|
||||
log.Printf("logtail: CONNECT response error from tailscaled: %v", err)
|
||||
c.Close()
|
||||
} else {
|
||||
dialLog.Printf("connected via tailscaled")
|
||||
return c, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we failed to dial, try again with bootstrap DNS.
|
||||
log.Printf("logtail: dial %q failed: %v (in %v), trying bootstrap...", addr, err, d)
|
||||
dnsCache := &dnscache.Resolver{
|
||||
Forward: dnscache.Get().Forward, // use default cache's forwarder
|
||||
UseLastGood: true,
|
||||
LookupIPFallback: dnsfallback.Lookup,
|
||||
}
|
||||
dialer := dnscache.Dialer(nd.DialContext, dnsCache)
|
||||
c, err = dialer(ctx, netw, addr)
|
||||
if err == nil {
|
||||
log.Printf("logtail: bootstrap dial succeeded")
|
||||
}
|
||||
return c, err
|
||||
}
|
||||
tr.DialContext = MakeDialFunc(netMon)
|
||||
|
||||
// We're contacting exactly 1 hostname, so the default's 100
|
||||
// max idle conns is very high for our needs. Even 2 is
|
||||
@@ -762,7 +785,7 @@ func goVersion() string {
|
||||
type noopPretendSuccessTransport struct{}
|
||||
|
||||
func (noopPretendSuccessTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
io.ReadAll(req.Body)
|
||||
io.Copy(io.Discard, req.Body)
|
||||
req.Body.Close()
|
||||
return &http.Response{
|
||||
StatusCode: 200,
|
||||
|
||||
@@ -24,11 +24,11 @@ import (
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/logtail/backoff"
|
||||
"tailscale.com/net/interfaces"
|
||||
"tailscale.com/net/netmon"
|
||||
"tailscale.com/net/sockstats"
|
||||
tslogger "tailscale.com/types/logger"
|
||||
"tailscale.com/types/logid"
|
||||
"tailscale.com/util/set"
|
||||
"tailscale.com/wgengine/monitor"
|
||||
)
|
||||
|
||||
// DefaultHost is the default host name to upload logs to when
|
||||
@@ -150,6 +150,7 @@ func NewLogger(cfg Config, logf tslogger.Logf) *Logger {
|
||||
timeNow: cfg.TimeNow,
|
||||
bo: backoff.NewBackoff("logtail", stdLogf, 30*time.Second),
|
||||
metricsDelta: cfg.MetricsDelta,
|
||||
sockstatsLabel: sockstats.LabelLogtailLogger,
|
||||
|
||||
procID: procID,
|
||||
includeProcSequence: cfg.IncludeProcSequence,
|
||||
@@ -178,7 +179,7 @@ type Logger struct {
|
||||
url string
|
||||
lowMem bool
|
||||
skipClientTime bool
|
||||
linkMonitor *monitor.Mon
|
||||
netMonitor *netmon.Monitor
|
||||
buffer Buffer
|
||||
drainWake chan struct{} // signal to speed up drain
|
||||
flushDelayFn func() time.Duration // negative or zero return value to upload aggressively, or >0 to batch at this delay
|
||||
@@ -192,6 +193,7 @@ type Logger struct {
|
||||
metricsDelta func() string // or nil
|
||||
privateID logid.PrivateID
|
||||
httpDoCalls atomic.Int32
|
||||
sockstatsLabel sockstats.Label
|
||||
|
||||
procID uint32
|
||||
includeProcSequence bool
|
||||
@@ -212,12 +214,17 @@ func (l *Logger) SetVerbosityLevel(level int) {
|
||||
atomic.StoreInt64(&l.stderrLevel, int64(level))
|
||||
}
|
||||
|
||||
// SetLinkMonitor sets the optional the link monitor.
|
||||
// SetNetMon sets the optional the network monitor.
|
||||
//
|
||||
// It should not be changed concurrently with log writes and should
|
||||
// only be set once.
|
||||
func (l *Logger) SetLinkMonitor(lm *monitor.Mon) {
|
||||
l.linkMonitor = lm
|
||||
func (l *Logger) SetNetMon(lm *netmon.Monitor) {
|
||||
l.netMonitor = lm
|
||||
}
|
||||
|
||||
// SetSockstatsLabel sets the label used in sockstat logs to identify network traffic from this logger.
|
||||
func (l *Logger) SetSockstatsLabel(label sockstats.Label) {
|
||||
l.sockstatsLabel = label
|
||||
}
|
||||
|
||||
// PrivateID returns the logger's private log ID.
|
||||
@@ -396,16 +403,16 @@ func (l *Logger) uploading(ctx context.Context) {
|
||||
}
|
||||
|
||||
func (l *Logger) internetUp() bool {
|
||||
if l.linkMonitor == nil {
|
||||
if l.netMonitor == nil {
|
||||
// No way to tell, so assume it is.
|
||||
return true
|
||||
}
|
||||
return l.linkMonitor.InterfaceState().AnyInterfaceUp()
|
||||
return l.netMonitor.InterfaceState().AnyInterfaceUp()
|
||||
}
|
||||
|
||||
func (l *Logger) awaitInternetUp(ctx context.Context) {
|
||||
upc := make(chan bool, 1)
|
||||
defer l.linkMonitor.RegisterChangeCallback(func(changed bool, st *interfaces.State) {
|
||||
defer l.netMonitor.RegisterChangeCallback(func(changed bool, st *interfaces.State) {
|
||||
if st.AnyInterfaceUp() {
|
||||
select {
|
||||
case upc <- true:
|
||||
@@ -428,7 +435,7 @@ func (l *Logger) awaitInternetUp(ctx context.Context) {
|
||||
// origlen of -1 indicates that the body is not compressed.
|
||||
func (l *Logger) upload(ctx context.Context, body []byte, origlen int) (uploaded bool, err error) {
|
||||
const maxUploadTime = 45 * time.Second
|
||||
ctx = sockstats.WithSockStats(ctx, sockstats.LabelLogtailLogger)
|
||||
ctx = sockstats.WithSockStats(ctx, l.sockstatsLabel, l.Logf)
|
||||
ctx, cancel := context.WithTimeout(ctx, maxUploadTime)
|
||||
defer cancel()
|
||||
|
||||
|
||||
19
net/art/art_test.go
Normal file
19
net/art/art_test.go
Normal file
@@ -0,0 +1,19 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package art
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"tailscale.com/util/cibuild"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
if cibuild.On() {
|
||||
// Skip CI on GitHub for now
|
||||
// TODO: https://github.com/tailscale/tailscale/issues/7866
|
||||
os.Exit(0)
|
||||
}
|
||||
}
|
||||
231
net/art/stride_table.go
Normal file
231
net/art/stride_table.go
Normal file
@@ -0,0 +1,231 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package art
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/bits"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// strideEntry is a strideTable entry.
|
||||
type strideEntry[T any] struct {
|
||||
// prefixIndex is the prefixIndex(...) value that caused this stride entry's
|
||||
// value to be populated, or 0 if value is nil.
|
||||
//
|
||||
// We need to keep track of this because allot() uses it to determine
|
||||
// whether an entry was propagated from a parent entry, or if it's a
|
||||
// different independent route.
|
||||
prefixIndex int
|
||||
// value is the value associated with the strideEntry, if any.
|
||||
value *T
|
||||
// child is the child strideTable associated with the strideEntry, if any.
|
||||
child *strideTable[T]
|
||||
}
|
||||
|
||||
// strideTable is a binary tree that implements an 8-bit routing table.
|
||||
//
|
||||
// The leaves of the binary tree are host routes (/8s). Each parent is a
|
||||
// successively larger prefix that encompasses its children (/7 through /0).
|
||||
type strideTable[T any] struct {
|
||||
// entries is the nodes of the binary tree, laid out in a flattened array.
|
||||
//
|
||||
// The array indices are arranged by the prefixIndex function, such that the
|
||||
// parent of the node at index i is located at index i>>1, and its children
|
||||
// at indices i<<1 and (i<<1)+1.
|
||||
//
|
||||
// A few consequences of this arrangement: host routes (/8) occupy the last
|
||||
// 256 entries in the table; the single default route /0 is at index 1, and
|
||||
// index 0 is unused (in the original paper, it's hijacked through sneaky C
|
||||
// memory trickery to store the refcount, but this is Go, where we don't
|
||||
// store random bits in pointers lest we confuse the GC)
|
||||
entries [lastHostIndex + 1]strideEntry[T]
|
||||
// refs is the number of route entries and child strideTables referenced by
|
||||
// this table. It is used in the multi-layered logic to determine when this
|
||||
// table is empty and can be deleted.
|
||||
refs int
|
||||
}
|
||||
|
||||
const (
|
||||
// firstHostIndex is the array index of the first host route. This is hostIndex(0/8).
|
||||
firstHostIndex = 0b1_0000_0000
|
||||
// lastHostIndex is the array index of the last host route. This is hostIndex(0xFF/8).
|
||||
lastHostIndex = 0b1_1111_1111
|
||||
)
|
||||
|
||||
// getChild returns the child strideTable pointer for addr (if any), and an
|
||||
// internal array index that can be used with deleteChild.
|
||||
func (t *strideTable[T]) getChild(addr uint8) (child *strideTable[T], idx int) {
|
||||
idx = hostIndex(addr)
|
||||
return t.entries[idx].child, idx
|
||||
}
|
||||
|
||||
// deleteChild deletes the child strideTable at idx (if any). idx should be
|
||||
// obtained via a call to getChild.
|
||||
func (t *strideTable[T]) deleteChild(idx int) {
|
||||
t.entries[idx].child = nil
|
||||
t.refs--
|
||||
}
|
||||
|
||||
// getOrCreateChild returns the child strideTable for addr, creating it if
|
||||
// necessary.
|
||||
func (t *strideTable[T]) getOrCreateChild(addr uint8) *strideTable[T] {
|
||||
idx := hostIndex(addr)
|
||||
if t.entries[idx].child == nil {
|
||||
t.entries[idx].child = new(strideTable[T])
|
||||
t.refs++
|
||||
}
|
||||
return t.entries[idx].child
|
||||
}
|
||||
|
||||
func (t *strideTable[T]) getValAndChild(addr uint8) (*T, *strideTable[T]) {
|
||||
idx := hostIndex(addr)
|
||||
return t.entries[idx].value, t.entries[idx].child
|
||||
}
|
||||
|
||||
// allot updates entries whose stored prefixIndex matches oldPrefixIndex, in the
|
||||
// subtree rooted at idx. Matching entries have their stored prefixIndex set to
|
||||
// newPrefixIndex, and their value set to val.
|
||||
//
|
||||
// allot is the core of the ART algorithm, enabling efficient insertion/deletion
|
||||
// while preserving very fast lookups.
|
||||
func (t *strideTable[T]) allot(idx int, oldPrefixIndex, newPrefixIndex int, val *T) {
|
||||
if t.entries[idx].prefixIndex != oldPrefixIndex {
|
||||
// current prefixIndex isn't what we expect. This is a recursive call
|
||||
// that found a child subtree that already has a more specific route
|
||||
// installed. Don't touch it.
|
||||
return
|
||||
}
|
||||
t.entries[idx].value = val
|
||||
t.entries[idx].prefixIndex = newPrefixIndex
|
||||
if idx >= firstHostIndex {
|
||||
// The entry we just updated was a host route, we're at the bottom of
|
||||
// the binary tree.
|
||||
return
|
||||
}
|
||||
// Propagate the allotment to this node's children.
|
||||
left := idx << 1
|
||||
t.allot(left, oldPrefixIndex, newPrefixIndex, val)
|
||||
right := left + 1
|
||||
t.allot(right, oldPrefixIndex, newPrefixIndex, val)
|
||||
}
|
||||
|
||||
// insert adds the route addr/prefixLen to t, with value val.
|
||||
func (t *strideTable[T]) insert(addr uint8, prefixLen int, val *T) {
|
||||
idx := prefixIndex(addr, prefixLen)
|
||||
old := t.entries[idx].value
|
||||
oldIdx := t.entries[idx].prefixIndex
|
||||
if oldIdx == idx && old == val {
|
||||
// This exact prefix+value is already in the table.
|
||||
return
|
||||
}
|
||||
t.allot(idx, oldIdx, idx, val)
|
||||
if oldIdx != idx {
|
||||
// This route entry was freshly created (not just updated), that's a new
|
||||
// reference.
|
||||
t.refs++
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// delete removes the route addr/prefixLen from t.
|
||||
func (t *strideTable[T]) delete(addr uint8, prefixLen int) *T {
|
||||
idx := prefixIndex(addr, prefixLen)
|
||||
recordedIdx := t.entries[idx].prefixIndex
|
||||
if recordedIdx != idx {
|
||||
// Route entry doesn't exist
|
||||
return nil
|
||||
}
|
||||
val := t.entries[idx].value
|
||||
|
||||
parentIdx := idx >> 1
|
||||
t.allot(idx, idx, t.entries[parentIdx].prefixIndex, t.entries[parentIdx].value)
|
||||
t.refs--
|
||||
return val
|
||||
}
|
||||
|
||||
// get does a route lookup for addr and returns the associated value, or nil if
|
||||
// no route matched.
|
||||
func (t *strideTable[T]) get(addr uint8) *T {
|
||||
return t.entries[hostIndex(addr)].value
|
||||
}
|
||||
|
||||
// TableDebugString returns the contents of t, formatted as a table with one
|
||||
// line per entry.
|
||||
func (t *strideTable[T]) tableDebugString() string {
|
||||
var ret bytes.Buffer
|
||||
for i, ent := range t.entries {
|
||||
if i == 0 {
|
||||
continue
|
||||
}
|
||||
v := "(nil)"
|
||||
if ent.value != nil {
|
||||
v = fmt.Sprint(*ent.value)
|
||||
}
|
||||
fmt.Fprintf(&ret, "idx=%3d (%s), parent=%3d (%s), val=%v\n", i, formatPrefixTable(inversePrefixIndex(i)), ent.prefixIndex, formatPrefixTable(inversePrefixIndex((ent.prefixIndex))), v)
|
||||
}
|
||||
return ret.String()
|
||||
}
|
||||
|
||||
// treeDebugString returns the contents of t, formatted as a sparse tree. Each
|
||||
// line is one entry, indented such that it is contained by all its parents, and
|
||||
// non-overlapping with any of its siblings.
|
||||
func (t *strideTable[T]) treeDebugString() string {
|
||||
var ret bytes.Buffer
|
||||
t.treeDebugStringRec(&ret, 1, 0) // index of 0/0, and 0 indent
|
||||
return ret.String()
|
||||
}
|
||||
|
||||
func (t *strideTable[T]) treeDebugStringRec(w io.Writer, idx, indent int) {
|
||||
addr, len := inversePrefixIndex(idx)
|
||||
if t.entries[idx].prefixIndex != 0 && t.entries[idx].prefixIndex == idx {
|
||||
fmt.Fprintf(w, "%s%d/%d (%d/%d) = %v\n", strings.Repeat(" ", indent), addr, len, addr, len, *t.entries[idx].value)
|
||||
indent += 2
|
||||
}
|
||||
if idx >= firstHostIndex {
|
||||
return
|
||||
}
|
||||
left := idx << 1
|
||||
t.treeDebugStringRec(w, left, indent)
|
||||
right := left + 1
|
||||
t.treeDebugStringRec(w, right, indent)
|
||||
}
|
||||
|
||||
// prefixIndex returns the array index of the tree node for addr/prefixLen.
|
||||
func prefixIndex(addr uint8, prefixLen int) int {
|
||||
// the prefixIndex of addr/prefixLen is the prefixLen most significant bits
|
||||
// of addr, with a 1 tacked onto the left-hand side. For example:
|
||||
//
|
||||
// - 0/0 is 1: 0 bits of the addr, with a 1 tacked on
|
||||
// - 42/8 is 1_00101010 (298): all bits of 42, with a 1 tacked on
|
||||
// - 48/4 is 1_0011 (19): 4 most-significant bits of 48, with a 1 tacked on
|
||||
return (int(addr) >> (8 - prefixLen)) + (1 << prefixLen)
|
||||
}
|
||||
|
||||
// hostIndex returns the array index of the host route for addr.
|
||||
// It is equivalent to prefixIndex(addr, 8).
|
||||
func hostIndex(addr uint8) int {
|
||||
return int(addr) + 1<<8
|
||||
}
|
||||
|
||||
// inversePrefixIndex returns the address and prefix length of idx. It is the
|
||||
// inverse of prefixIndex. Only used for debugging and in tests.
|
||||
func inversePrefixIndex(idx int) (addr uint8, len int) {
|
||||
lz := bits.LeadingZeros(uint(idx))
|
||||
len = strconv.IntSize - lz - 1
|
||||
addr = uint8(idx&(0xFF>>(8-len))) << (8 - len)
|
||||
return addr, len
|
||||
}
|
||||
|
||||
// formatPrefixTable formats addr and len as addr/len, with a constant width
|
||||
// suitable for use in table formatting.
|
||||
func formatPrefixTable(addr uint8, len int) string {
|
||||
if len < 0 { // this happens for inversePrefixIndex(0)
|
||||
return "<nil>"
|
||||
}
|
||||
return fmt.Sprintf("%3d/%d", addr, len)
|
||||
}
|
||||
384
net/art/stride_table_test.go
Normal file
384
net/art/stride_table_test.go
Normal file
@@ -0,0 +1,384 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package art
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"tailscale.com/types/ptr"
|
||||
)
|
||||
|
||||
func TestInversePrefix(t *testing.T) {
|
||||
t.Parallel()
|
||||
for i := 0; i < 256; i++ {
|
||||
for len := 0; len < 9; len++ {
|
||||
addr := i & (0xFF << (8 - len))
|
||||
idx := prefixIndex(uint8(addr), len)
|
||||
addr2, len2 := inversePrefixIndex(idx)
|
||||
if addr2 != uint8(addr) || len2 != len {
|
||||
t.Errorf("inverse(index(%d/%d)) != %d/%d", addr, len, addr2, len2)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestHostIndex(t *testing.T) {
|
||||
t.Parallel()
|
||||
for i := 0; i < 256; i++ {
|
||||
got := hostIndex(uint8(i))
|
||||
want := prefixIndex(uint8(i), 8)
|
||||
if got != want {
|
||||
t.Errorf("hostIndex(%d) = %d, want %d", i, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestStrideTableInsert(t *testing.T) {
|
||||
t.Parallel()
|
||||
// Verify that strideTable's lookup results after a bunch of inserts exactly
|
||||
// match those of a naive implementation that just scans all prefixes on
|
||||
// every lookup. The naive implementation is very slow, but its behavior is
|
||||
// easy to verify by inspection.
|
||||
|
||||
pfxs := shufflePrefixes(allPrefixes())[:100]
|
||||
slow := slowTable[int]{pfxs}
|
||||
fast := strideTable[int]{}
|
||||
|
||||
t.Logf("slow table:\n%s", slow.String())
|
||||
|
||||
for _, pfx := range pfxs {
|
||||
fast.insert(pfx.addr, pfx.len, pfx.val)
|
||||
t.Logf("after insert %d/%d:\n%s", pfx.addr, pfx.len, fast.tableDebugString())
|
||||
}
|
||||
|
||||
for i := 0; i < 256; i++ {
|
||||
addr := uint8(i)
|
||||
slowVal := slow.get(addr)
|
||||
fastVal := fast.get(addr)
|
||||
if slowVal != fastVal {
|
||||
t.Fatalf("strideTable.get(%d) = %v, want %v", addr, *fastVal, *slowVal)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestStrideTableInsertShuffled(t *testing.T) {
|
||||
t.Parallel()
|
||||
// The order in which routes are inserted into a route table does not
|
||||
// influence the final shape of the table, as long as the same set of
|
||||
// prefixes is being inserted. This test verifies that strideTable behaves
|
||||
// this way.
|
||||
//
|
||||
// In addition to the basic shuffle test, we also check that this behavior
|
||||
// is maintained if all inserted routes have the same value pointer. This
|
||||
// shouldn't matter (the strideTable still needs to correctly account for
|
||||
// each inserted route, regardless of associated value), but during initial
|
||||
// development a subtle bug made the table corrupt itself in that setup, so
|
||||
// this test includes a regression test for that.
|
||||
|
||||
routes := shufflePrefixes(allPrefixes())[:100]
|
||||
|
||||
zero := 0
|
||||
rt := strideTable[int]{}
|
||||
rtZero := strideTable[int]{}
|
||||
for _, route := range routes {
|
||||
rt.insert(route.addr, route.len, route.val)
|
||||
rtZero.insert(route.addr, route.len, &zero)
|
||||
}
|
||||
|
||||
// Order of insertion should not affect the final shape of the stride table.
|
||||
routes2 := append([]slowEntry[int](nil), routes...) // dup so we can print both slices on fail
|
||||
for i := 0; i < 100; i++ {
|
||||
rand.Shuffle(len(routes2), func(i, j int) { routes2[i], routes2[j] = routes2[j], routes2[i] })
|
||||
rt2 := strideTable[int]{}
|
||||
for _, route := range routes2 {
|
||||
rt2.insert(route.addr, route.len, route.val)
|
||||
}
|
||||
if diff := cmp.Diff(rt, rt2, cmp.AllowUnexported(strideTable[int]{}, strideEntry[int]{})); diff != "" {
|
||||
t.Errorf("tables ended up different with different insertion order (-got+want):\n%s\n\nOrder 1: %v\nOrder 2: %v", diff, formatSlowEntriesShort(routes), formatSlowEntriesShort(routes2))
|
||||
}
|
||||
|
||||
rtZero2 := strideTable[int]{}
|
||||
for _, route := range routes2 {
|
||||
rtZero2.insert(route.addr, route.len, &zero)
|
||||
}
|
||||
if diff := cmp.Diff(rtZero, rtZero2, cmp.AllowUnexported(strideTable[int]{}, strideEntry[int]{})); diff != "" {
|
||||
t.Errorf("tables with identical vals ended up different with different insertion order (-got+want):\n%s\n\nOrder 1: %v\nOrder 2: %v", diff, formatSlowEntriesShort(routes), formatSlowEntriesShort(routes2))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestStrideTableDelete(t *testing.T) {
|
||||
t.Parallel()
|
||||
// Compare route deletion to our reference slowTable.
|
||||
pfxs := shufflePrefixes(allPrefixes())[:100]
|
||||
slow := slowTable[int]{pfxs}
|
||||
fast := strideTable[int]{}
|
||||
|
||||
t.Logf("slow table:\n%s", slow.String())
|
||||
|
||||
for _, pfx := range pfxs {
|
||||
fast.insert(pfx.addr, pfx.len, pfx.val)
|
||||
t.Logf("after insert %d/%d:\n%s", pfx.addr, pfx.len, fast.tableDebugString())
|
||||
}
|
||||
|
||||
toDelete := pfxs[:50]
|
||||
for _, pfx := range toDelete {
|
||||
slow.delete(pfx.addr, pfx.len)
|
||||
fast.delete(pfx.addr, pfx.len)
|
||||
}
|
||||
|
||||
// Sanity check that slowTable seems to have done the right thing.
|
||||
if cnt := len(slow.prefixes); cnt != 50 {
|
||||
t.Fatalf("slowTable has %d entries after deletes, want 50", cnt)
|
||||
}
|
||||
|
||||
for i := 0; i < 256; i++ {
|
||||
addr := uint8(i)
|
||||
slowVal := slow.get(addr)
|
||||
fastVal := fast.get(addr)
|
||||
if slowVal != fastVal {
|
||||
t.Fatalf("strideTable.get(%d) = %v, want %v", addr, *fastVal, *slowVal)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestStrideTableDeleteShuffle(t *testing.T) {
|
||||
t.Parallel()
|
||||
// Same as TestStrideTableInsertShuffle, the order in which prefixes are
|
||||
// deleted should not impact the final shape of the route table.
|
||||
|
||||
routes := shufflePrefixes(allPrefixes())[:100]
|
||||
toDelete := routes[:50]
|
||||
|
||||
zero := 0
|
||||
rt := strideTable[int]{}
|
||||
rtZero := strideTable[int]{}
|
||||
for _, route := range routes {
|
||||
rt.insert(route.addr, route.len, route.val)
|
||||
rtZero.insert(route.addr, route.len, &zero)
|
||||
}
|
||||
for _, route := range toDelete {
|
||||
rt.delete(route.addr, route.len)
|
||||
rtZero.delete(route.addr, route.len)
|
||||
}
|
||||
|
||||
// Order of deletion should not affect the final shape of the stride table.
|
||||
toDelete2 := append([]slowEntry[int](nil), toDelete...) // dup so we can print both slices on fail
|
||||
for i := 0; i < 100; i++ {
|
||||
rand.Shuffle(len(toDelete2), func(i, j int) { toDelete2[i], toDelete2[j] = toDelete2[j], toDelete2[i] })
|
||||
rt2 := strideTable[int]{}
|
||||
for _, route := range routes {
|
||||
rt2.insert(route.addr, route.len, route.val)
|
||||
}
|
||||
for _, route := range toDelete2 {
|
||||
rt2.delete(route.addr, route.len)
|
||||
}
|
||||
if diff := cmp.Diff(rt, rt2, cmp.AllowUnexported(strideTable[int]{}, strideEntry[int]{})); diff != "" {
|
||||
t.Errorf("tables ended up different with different deletion order (-got+want):\n%s\n\nOrder 1: %v\nOrder 2: %v", diff, formatSlowEntriesShort(toDelete), formatSlowEntriesShort(toDelete2))
|
||||
}
|
||||
|
||||
rtZero2 := strideTable[int]{}
|
||||
for _, route := range routes {
|
||||
rtZero2.insert(route.addr, route.len, &zero)
|
||||
}
|
||||
for _, route := range toDelete2 {
|
||||
rtZero2.delete(route.addr, route.len)
|
||||
}
|
||||
if diff := cmp.Diff(rtZero, rtZero2, cmp.AllowUnexported(strideTable[int]{}, strideEntry[int]{})); diff != "" {
|
||||
t.Errorf("tables with identical vals ended up different with different deletion order (-got+want):\n%s\n\nOrder 1: %v\nOrder 2: %v", diff, formatSlowEntriesShort(toDelete), formatSlowEntriesShort(toDelete2))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var strideRouteCount = []int{10, 50, 100, 200}
|
||||
|
||||
// forCountAndOrdering runs the benchmark fn with different sets of routes.
|
||||
//
|
||||
// fn is called once for each combination of {num_routes, order}, where
|
||||
// num_routes is the values in strideRouteCount, and order is the order of the
|
||||
// routes in the list: random, largest prefix first (/0 to /8), and smallest
|
||||
// prefix first (/8 to /0).
|
||||
func forStrideCountAndOrdering(b *testing.B, fn func(b *testing.B, routes []slowEntry[int])) {
|
||||
routes := shufflePrefixes(allPrefixes())
|
||||
for _, nroutes := range strideRouteCount {
|
||||
b.Run(fmt.Sprint(nroutes), func(b *testing.B) {
|
||||
routes := append([]slowEntry[int](nil), routes[:nroutes]...)
|
||||
b.Run("random_order", func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
fn(b, routes)
|
||||
})
|
||||
sort.Slice(routes, func(i, j int) bool {
|
||||
if routes[i].len < routes[j].len {
|
||||
return true
|
||||
}
|
||||
return routes[i].addr < routes[j].addr
|
||||
})
|
||||
b.Run("largest_first", func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
fn(b, routes)
|
||||
})
|
||||
sort.Slice(routes, func(i, j int) bool {
|
||||
if routes[j].len < routes[i].len {
|
||||
return true
|
||||
}
|
||||
return routes[j].addr < routes[i].addr
|
||||
})
|
||||
b.Run("smallest_first", func(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
fn(b, routes)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkStrideTableInsertion(b *testing.B) {
|
||||
forStrideCountAndOrdering(b, func(b *testing.B, routes []slowEntry[int]) {
|
||||
val := 0
|
||||
for i := 0; i < b.N; i++ {
|
||||
var rt strideTable[int]
|
||||
for _, route := range routes {
|
||||
rt.insert(route.addr, route.len, &val)
|
||||
}
|
||||
}
|
||||
inserts := float64(b.N) * float64(len(routes))
|
||||
elapsed := float64(b.Elapsed().Nanoseconds())
|
||||
elapsedSec := b.Elapsed().Seconds()
|
||||
b.ReportMetric(elapsed/inserts, "ns/op")
|
||||
b.ReportMetric(inserts/elapsedSec, "routes/s")
|
||||
})
|
||||
}
|
||||
|
||||
func BenchmarkStrideTableDeletion(b *testing.B) {
|
||||
forStrideCountAndOrdering(b, func(b *testing.B, routes []slowEntry[int]) {
|
||||
val := 0
|
||||
var rt strideTable[int]
|
||||
for _, route := range routes {
|
||||
rt.insert(route.addr, route.len, &val)
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
rt2 := rt
|
||||
for _, route := range routes {
|
||||
rt2.delete(route.addr, route.len)
|
||||
}
|
||||
}
|
||||
deletes := float64(b.N) * float64(len(routes))
|
||||
elapsed := float64(b.Elapsed().Nanoseconds())
|
||||
elapsedSec := b.Elapsed().Seconds()
|
||||
b.ReportMetric(elapsed/deletes, "ns/op")
|
||||
b.ReportMetric(deletes/elapsedSec, "routes/s")
|
||||
})
|
||||
}
|
||||
|
||||
var writeSink *int
|
||||
|
||||
func BenchmarkStrideTableGet(b *testing.B) {
|
||||
// No need to forCountAndOrdering here, route lookup time is independent of
|
||||
// the route count.
|
||||
routes := shufflePrefixes(allPrefixes())[:100]
|
||||
var rt strideTable[int]
|
||||
for _, route := range routes {
|
||||
rt.insert(route.addr, route.len, route.val)
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
writeSink = rt.get(uint8(i))
|
||||
}
|
||||
gets := float64(b.N)
|
||||
elapsedSec := b.Elapsed().Seconds()
|
||||
b.ReportMetric(gets/elapsedSec, "routes/s")
|
||||
}
|
||||
|
||||
// slowTable is an 8-bit routing table implemented as a set of prefixes that are
|
||||
// explicitly scanned in full for every route lookup. It is very slow, but also
|
||||
// reasonably easy to verify by inspection, and so a good comparison target for
|
||||
// strideTable.
|
||||
type slowTable[T any] struct {
|
||||
prefixes []slowEntry[T]
|
||||
}
|
||||
|
||||
type slowEntry[T any] struct {
|
||||
addr uint8
|
||||
len int
|
||||
val *T
|
||||
}
|
||||
|
||||
func (t *slowTable[T]) String() string {
|
||||
pfxs := append([]slowEntry[T](nil), t.prefixes...)
|
||||
sort.Slice(pfxs, func(i, j int) bool {
|
||||
if pfxs[i].len != pfxs[j].len {
|
||||
return pfxs[i].len < pfxs[j].len
|
||||
}
|
||||
return pfxs[i].addr < pfxs[j].addr
|
||||
})
|
||||
var ret bytes.Buffer
|
||||
for _, pfx := range pfxs {
|
||||
fmt.Fprintf(&ret, "%3d/%d (%08b/%08b) = %v\n", pfx.addr, pfx.len, pfx.addr, pfxMask(pfx.len), *pfx.val)
|
||||
}
|
||||
return ret.String()
|
||||
}
|
||||
|
||||
func (t *slowTable[T]) insert(addr uint8, prefixLen int, val *T) {
|
||||
t.delete(addr, prefixLen) // no-op if prefix doesn't exist
|
||||
t.prefixes = append(t.prefixes, slowEntry[T]{addr, prefixLen, val})
|
||||
}
|
||||
|
||||
func (t *slowTable[T]) delete(addr uint8, prefixLen int) {
|
||||
pfx := make([]slowEntry[T], 0, len(t.prefixes))
|
||||
for _, e := range t.prefixes {
|
||||
if e.addr == addr && e.len == prefixLen {
|
||||
continue
|
||||
}
|
||||
pfx = append(pfx, e)
|
||||
}
|
||||
t.prefixes = pfx
|
||||
}
|
||||
|
||||
func (t *slowTable[T]) get(addr uint8) *T {
|
||||
var (
|
||||
ret *T
|
||||
curLen = -1
|
||||
)
|
||||
for _, e := range t.prefixes {
|
||||
if addr&pfxMask(e.len) == e.addr && e.len >= curLen {
|
||||
ret = e.val
|
||||
curLen = e.len
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func pfxMask(pfxLen int) uint8 {
|
||||
return 0xFF << (8 - pfxLen)
|
||||
}
|
||||
|
||||
func allPrefixes() []slowEntry[int] {
|
||||
ret := make([]slowEntry[int], 0, lastHostIndex)
|
||||
for i := 1; i < lastHostIndex+1; i++ {
|
||||
a, l := inversePrefixIndex(i)
|
||||
ret = append(ret, slowEntry[int]{a, l, ptr.To(i)})
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func shufflePrefixes(pfxs []slowEntry[int]) []slowEntry[int] {
|
||||
rand.Shuffle(len(pfxs), func(i, j int) { pfxs[i], pfxs[j] = pfxs[j], pfxs[i] })
|
||||
return pfxs
|
||||
}
|
||||
|
||||
func formatSlowEntriesShort[T any](ents []slowEntry[T]) string {
|
||||
var ret []string
|
||||
for _, ent := range ents {
|
||||
ret = append(ret, fmt.Sprintf("%d/%d", ent.addr, ent.len))
|
||||
}
|
||||
return "[" + strings.Join(ret, " ") + "]"
|
||||
}
|
||||
162
net/art/table.go
Normal file
162
net/art/table.go
Normal file
@@ -0,0 +1,162 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package art provides a routing table that implements the Allotment Routing
|
||||
// Table (ART) algorithm by Donald Knuth, as described in the paper by Yoichi
|
||||
// Hariguchi.
|
||||
//
|
||||
// ART outperforms the traditional radix tree implementations for route lookups,
|
||||
// insertions, and deletions.
|
||||
//
|
||||
// For more information, see Yoichi Hariguchi's paper:
|
||||
// https://cseweb.ucsd.edu//~varghese/TEACH/cs228/artlookup.pdf
|
||||
package art
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/netip"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Table is an IPv4 and IPv6 routing table.
|
||||
type Table[T any] struct {
|
||||
v4 strideTable[T]
|
||||
v6 strideTable[T]
|
||||
}
|
||||
|
||||
// Get does a route lookup for addr and returns the associated value, or nil if
|
||||
// no route matched.
|
||||
func (t *Table[T]) Get(addr netip.Addr) *T {
|
||||
st := &t.v4
|
||||
if addr.Is6() {
|
||||
st = &t.v6
|
||||
}
|
||||
|
||||
var ret *T
|
||||
for _, stride := range addr.AsSlice() {
|
||||
rt, child := st.getValAndChild(stride)
|
||||
if rt != nil {
|
||||
// Found a more specific route than whatever we found previously,
|
||||
// keep a note.
|
||||
ret = rt
|
||||
}
|
||||
if child == nil {
|
||||
// No sub-routes further down, whatever we have recorded in ret is
|
||||
// the result.
|
||||
return ret
|
||||
}
|
||||
st = child
|
||||
}
|
||||
|
||||
// Unreachable because Insert/Delete won't allow the leaf strideTables to
|
||||
// have children, so we must return via the nil check in the loop.
|
||||
panic("unreachable")
|
||||
}
|
||||
|
||||
// Insert adds pfx to the table, with value val.
|
||||
// If pfx is already present in the table, its value is set to val.
|
||||
func (t *Table[T]) Insert(pfx netip.Prefix, val *T) {
|
||||
if val == nil {
|
||||
panic("Table.Insert called with nil value")
|
||||
}
|
||||
st := &t.v4
|
||||
if pfx.Addr().Is6() {
|
||||
st = &t.v6
|
||||
}
|
||||
bs := pfx.Addr().AsSlice()
|
||||
i := 0
|
||||
numBits := pfx.Bits()
|
||||
|
||||
// The strideTable we want to insert into is potentially at the end of a
|
||||
// chain of parent tables, each one encoding successive 8 bits of the
|
||||
// prefix. Navigate downwards, allocating child tables as needed, until we
|
||||
// find the one this prefix belongs in.
|
||||
for numBits > 8 {
|
||||
st = st.getOrCreateChild(bs[i])
|
||||
i++
|
||||
numBits -= 8
|
||||
}
|
||||
// Finally, insert the remaining 0-8 bits of the prefix into the child
|
||||
// table.
|
||||
st.insert(bs[i], numBits, val)
|
||||
}
|
||||
|
||||
// Delete removes pfx from the table, if it is present.
|
||||
func (t *Table[T]) Delete(pfx netip.Prefix) {
|
||||
st := &t.v4
|
||||
if pfx.Addr().Is6() {
|
||||
st = &t.v6
|
||||
}
|
||||
bs := pfx.Addr().AsSlice()
|
||||
i := 0
|
||||
numBits := pfx.Bits()
|
||||
|
||||
// Deletion may drive the refcount of some strideTables down to zero. We
|
||||
// need to clean up these dangling tables, so we have to keep track of which
|
||||
// tables we touch on the way down, and which strideEntry index each child
|
||||
// is registered in.
|
||||
strideTables := [16]*strideTable[T]{st}
|
||||
var strideIndexes [16]int
|
||||
|
||||
// Similar to Insert, navigate down the tree of strideTables, looking for
|
||||
// the one that houses the last 0-8 bits of the prefix to delete.
|
||||
//
|
||||
// The only difference is that here, we don't create missing child tables.
|
||||
// If a child necessary to pfx is missing, then the pfx cannot exist in the
|
||||
// Table, and we can exit early.
|
||||
for numBits > 8 {
|
||||
child, idx := st.getChild(bs[i])
|
||||
if child == nil {
|
||||
// Prefix can't exist in the table, one of the necessary
|
||||
// strideTables doesn't exit.
|
||||
return
|
||||
}
|
||||
// Note that the strideIndex and strideTables entries are off-by-one.
|
||||
// The child table pointer is recorded at i+1, but it is referenced by a
|
||||
// particular index in the parent table, at index i.
|
||||
strideIndexes[i] = idx
|
||||
i++
|
||||
strideTables[i] = child
|
||||
numBits -= 8
|
||||
st = child
|
||||
}
|
||||
if st.delete(bs[i], numBits) == nil {
|
||||
// Prefix didn't exist in the expected strideTable, refcount hasn't
|
||||
// changed, no need to run through cleanup.
|
||||
return
|
||||
}
|
||||
|
||||
// st.delete reduced st's refcount by one, so we may be hanging onto a chain
|
||||
// of redundant strideTables. Walk back up the path we recorded in the
|
||||
// descent loop, deleting tables until we encounter one that still has other
|
||||
// refs (or we hit the root strideTable, which is never deleted).
|
||||
for i > 0 && strideTables[i].refs == 0 {
|
||||
strideTables[i-1].deleteChild(strideIndexes[i-1])
|
||||
i--
|
||||
}
|
||||
}
|
||||
|
||||
// debugSummary prints the tree of allocated strideTables in t, with each
|
||||
// strideTable's refcount.
|
||||
func (t *Table[T]) debugSummary() string {
|
||||
var ret bytes.Buffer
|
||||
fmt.Fprintf(&ret, "v4: ")
|
||||
strideSummary(&ret, &t.v4, 0)
|
||||
fmt.Fprintf(&ret, "v6: ")
|
||||
strideSummary(&ret, &t.v6, 0)
|
||||
return ret.String()
|
||||
}
|
||||
|
||||
func strideSummary[T any](w io.Writer, st *strideTable[T], indent int) {
|
||||
fmt.Fprintf(w, "%d refs\n", st.refs)
|
||||
indent += 2
|
||||
for i := firstHostIndex; i <= lastHostIndex; i++ {
|
||||
if child := st.entries[i].child; child != nil {
|
||||
addr, len := inversePrefixIndex(i)
|
||||
fmt.Fprintf(w, "%s%d/%d: ", strings.Repeat(" ", indent), addr, len)
|
||||
strideSummary(w, child, indent)
|
||||
}
|
||||
}
|
||||
}
|
||||
550
net/art/table_test.go
Normal file
550
net/art/table_test.go
Normal file
@@ -0,0 +1,550 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package art
|
||||
|
||||
import (
|
||||
crand "crypto/rand"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net/netip"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"tailscale.com/types/ptr"
|
||||
)
|
||||
|
||||
func TestInsert(t *testing.T) {
|
||||
t.Parallel()
|
||||
pfxs := randomPrefixes(10_000)
|
||||
|
||||
slow := slowPrefixTable[int]{pfxs}
|
||||
fast := Table[int]{}
|
||||
|
||||
for _, pfx := range pfxs {
|
||||
fast.Insert(pfx.pfx, pfx.val)
|
||||
}
|
||||
|
||||
t.Logf(fast.debugSummary())
|
||||
|
||||
seenVals4 := map[*int]bool{}
|
||||
seenVals6 := map[*int]bool{}
|
||||
for i := 0; i < 10_000; i++ {
|
||||
a := randomAddr()
|
||||
slowVal := slow.get(a)
|
||||
fastVal := fast.Get(a)
|
||||
if a.Is6() {
|
||||
seenVals6[fastVal] = true
|
||||
} else {
|
||||
seenVals4[fastVal] = true
|
||||
}
|
||||
if slowVal != fastVal {
|
||||
t.Errorf("get(%q) = %p, want %p", a, fastVal, slowVal)
|
||||
}
|
||||
}
|
||||
// Empirically, 10k probes into 5k v4 prefixes and 5k v6 prefixes results in
|
||||
// ~1k distinct values for v4 and ~300 for v6. distinct routes. This sanity
|
||||
// check that we didn't just return a single route for everything should be
|
||||
// very generous indeed.
|
||||
if cnt := len(seenVals4); cnt < 10 {
|
||||
t.Fatalf("saw %d distinct v4 route results, statistically expected ~1000", cnt)
|
||||
}
|
||||
if cnt := len(seenVals6); cnt < 10 {
|
||||
t.Fatalf("saw %d distinct v6 route results, statistically expected ~300", cnt)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInsertShuffled(t *testing.T) {
|
||||
t.Parallel()
|
||||
pfxs := randomPrefixes(10_000)
|
||||
|
||||
rt := Table[int]{}
|
||||
for _, pfx := range pfxs {
|
||||
rt.Insert(pfx.pfx, pfx.val)
|
||||
}
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
pfxs2 := append([]slowPrefixEntry[int](nil), pfxs...)
|
||||
rand.Shuffle(len(pfxs2), func(i, j int) { pfxs2[i], pfxs2[j] = pfxs2[j], pfxs2[i] })
|
||||
rt2 := Table[int]{}
|
||||
for _, pfx := range pfxs2 {
|
||||
rt2.Insert(pfx.pfx, pfx.val)
|
||||
}
|
||||
|
||||
// Diffing a deep tree of tables gives cmp.Diff a nervous breakdown, so
|
||||
// test for equivalence statistically with random probes instead.
|
||||
for i := 0; i < 10_000; i++ {
|
||||
a := randomAddr()
|
||||
val1 := rt.Get(a)
|
||||
val2 := rt2.Get(a)
|
||||
if (val1 == nil && val2 != nil) || (val1 != nil && val2 == nil) || (*val1 != *val2) {
|
||||
t.Errorf("get(%q) = %s, want %s", a, printIntPtr(val2), printIntPtr(val1))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDelete(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const (
|
||||
numPrefixes = 10_000 // total prefixes to insert (test deletes 50% of them)
|
||||
numPerFamily = numPrefixes / 2
|
||||
deleteCut = numPerFamily / 2
|
||||
numProbes = 10_000 // random addr lookups to do
|
||||
)
|
||||
|
||||
// We have to do this little dance instead of just using allPrefixes,
|
||||
// because we want pfxs and toDelete to be non-overlapping sets.
|
||||
all4, all6 := randomPrefixes4(numPerFamily), randomPrefixes6(numPerFamily)
|
||||
pfxs := append([]slowPrefixEntry[int](nil), all4[:deleteCut]...)
|
||||
pfxs = append(pfxs, all6[:deleteCut]...)
|
||||
toDelete := append([]slowPrefixEntry[int](nil), all4[deleteCut:]...)
|
||||
toDelete = append(toDelete, all6[deleteCut:]...)
|
||||
|
||||
slow := slowPrefixTable[int]{pfxs}
|
||||
fast := Table[int]{}
|
||||
|
||||
for _, pfx := range pfxs {
|
||||
fast.Insert(pfx.pfx, pfx.val)
|
||||
}
|
||||
|
||||
for _, pfx := range toDelete {
|
||||
fast.Insert(pfx.pfx, pfx.val)
|
||||
}
|
||||
for _, pfx := range toDelete {
|
||||
fast.Delete(pfx.pfx)
|
||||
}
|
||||
|
||||
seenVals4 := map[*int]bool{}
|
||||
seenVals6 := map[*int]bool{}
|
||||
for i := 0; i < numProbes; i++ {
|
||||
a := randomAddr()
|
||||
slowVal := slow.get(a)
|
||||
fastVal := fast.Get(a)
|
||||
if a.Is6() {
|
||||
seenVals6[fastVal] = true
|
||||
} else {
|
||||
seenVals4[fastVal] = true
|
||||
}
|
||||
if slowVal != fastVal {
|
||||
t.Fatalf("get(%q) = %p, want %p", a, fastVal, slowVal)
|
||||
}
|
||||
}
|
||||
// Empirically, 10k probes into 5k v4 prefixes and 5k v6 prefixes results in
|
||||
// ~1k distinct values for v4 and ~300 for v6. distinct routes. This sanity
|
||||
// check that we didn't just return a single route for everything should be
|
||||
// very generous indeed.
|
||||
if cnt := len(seenVals4); cnt < 10 {
|
||||
t.Fatalf("saw %d distinct v4 route results, statistically expected ~1000", cnt)
|
||||
}
|
||||
if cnt := len(seenVals6); cnt < 10 {
|
||||
t.Fatalf("saw %d distinct v6 route results, statistically expected ~300", cnt)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteShuffled(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const (
|
||||
numPrefixes = 10_000 // prefixes to insert (test deletes 50% of them)
|
||||
numPerFamily = numPrefixes / 2
|
||||
deleteCut = numPerFamily / 2
|
||||
numProbes = 10_000 // random addr lookups to do
|
||||
)
|
||||
|
||||
// We have to do this little dance instead of just using allPrefixes,
|
||||
// because we want pfxs and toDelete to be non-overlapping sets.
|
||||
all4, all6 := randomPrefixes4(numPerFamily), randomPrefixes6(numPerFamily)
|
||||
pfxs := append([]slowPrefixEntry[int](nil), all4[:deleteCut]...)
|
||||
pfxs = append(pfxs, all6[:deleteCut]...)
|
||||
toDelete := append([]slowPrefixEntry[int](nil), all4[deleteCut:]...)
|
||||
toDelete = append(toDelete, all6[deleteCut:]...)
|
||||
|
||||
rt := Table[int]{}
|
||||
for _, pfx := range pfxs {
|
||||
rt.Insert(pfx.pfx, pfx.val)
|
||||
}
|
||||
for _, pfx := range toDelete {
|
||||
rt.Insert(pfx.pfx, pfx.val)
|
||||
}
|
||||
for _, pfx := range toDelete {
|
||||
rt.Delete(pfx.pfx)
|
||||
}
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
pfxs2 := append([]slowPrefixEntry[int](nil), pfxs...)
|
||||
toDelete2 := append([]slowPrefixEntry[int](nil), toDelete...)
|
||||
rand.Shuffle(len(toDelete2), func(i, j int) { toDelete2[i], toDelete2[j] = toDelete2[j], toDelete2[i] })
|
||||
rt2 := Table[int]{}
|
||||
for _, pfx := range pfxs2 {
|
||||
rt2.Insert(pfx.pfx, pfx.val)
|
||||
}
|
||||
for _, pfx := range toDelete2 {
|
||||
rt2.Insert(pfx.pfx, pfx.val)
|
||||
}
|
||||
for _, pfx := range toDelete2 {
|
||||
rt2.Delete(pfx.pfx)
|
||||
}
|
||||
|
||||
// Diffing a deep tree of tables gives cmp.Diff a nervous breakdown, so
|
||||
// test for equivalence statistically with random probes instead.
|
||||
for i := 0; i < numProbes; i++ {
|
||||
a := randomAddr()
|
||||
val1 := rt.Get(a)
|
||||
val2 := rt2.Get(a)
|
||||
if val1 == nil && val2 == nil {
|
||||
continue
|
||||
}
|
||||
if (val1 == nil && val2 != nil) || (val1 != nil && val2 == nil) || (*val1 != *val2) {
|
||||
t.Errorf("get(%q) = %s, want %s", a, printIntPtr(val2), printIntPtr(val1))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 100k routes for IPv6, at the current size of strideTable and strideEntry, is
|
||||
// in the ballpark of 4GiB if you assume worst-case prefix distribution. Future
|
||||
// optimizations will knock down the memory consumption by over an order of
|
||||
// magnitude, so for now just skip the 100k benchmarks to stay well away of
|
||||
// OOMs.
|
||||
//
|
||||
// TODO(go/bug/7781): reenable larger table tests once memory utilization is
|
||||
// optimized.
|
||||
var benchRouteCount = []int{10, 100, 1000, 10_000} //, 100_000}
|
||||
|
||||
// forFamilyAndCount runs the benchmark fn with different sets of
|
||||
// routes.
|
||||
//
|
||||
// fn is called once for each combination of {addr_family, num_routes},
|
||||
// where addr_family is ipv4 or ipv6, num_routes is the values in
|
||||
// benchRouteCount.
|
||||
func forFamilyAndCount(b *testing.B, fn func(b *testing.B, routes []slowPrefixEntry[int])) {
|
||||
for _, fam := range []string{"ipv4", "ipv6"} {
|
||||
rng := randomPrefixes4
|
||||
if fam == "ipv6" {
|
||||
rng = randomPrefixes6
|
||||
}
|
||||
b.Run(fam, func(b *testing.B) {
|
||||
for _, nroutes := range benchRouteCount {
|
||||
routes := rng(nroutes)
|
||||
b.Run(fmt.Sprint(nroutes), func(b *testing.B) {
|
||||
fn(b, routes)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkTableInsertion(b *testing.B) {
|
||||
forFamilyAndCount(b, func(b *testing.B, routes []slowPrefixEntry[int]) {
|
||||
b.StopTimer()
|
||||
b.ResetTimer()
|
||||
var startMem, endMem runtime.MemStats
|
||||
runtime.ReadMemStats(&startMem)
|
||||
b.StartTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
var rt Table[int]
|
||||
for _, route := range routes {
|
||||
rt.Insert(route.pfx, route.val)
|
||||
}
|
||||
}
|
||||
b.StopTimer()
|
||||
runtime.ReadMemStats(&endMem)
|
||||
inserts := float64(b.N) * float64(len(routes))
|
||||
allocs := float64(endMem.Mallocs - startMem.Mallocs)
|
||||
bytes := float64(endMem.TotalAlloc - startMem.TotalAlloc)
|
||||
elapsed := float64(b.Elapsed().Nanoseconds())
|
||||
elapsedSec := b.Elapsed().Seconds()
|
||||
b.ReportMetric(elapsed/inserts, "ns/op")
|
||||
b.ReportMetric(inserts/elapsedSec, "routes/s")
|
||||
b.ReportMetric(roundFloat64(allocs/inserts), "avg-allocs/op")
|
||||
b.ReportMetric(roundFloat64(bytes/inserts), "avg-B/op")
|
||||
})
|
||||
}
|
||||
|
||||
func BenchmarkTableDelete(b *testing.B) {
|
||||
forFamilyAndCount(b, func(b *testing.B, routes []slowPrefixEntry[int]) {
|
||||
// Collect memstats for one round of insertions, so we can remove it
|
||||
// from the total at the end and get only the deletion alloc count.
|
||||
insertAllocs, insertBytes := getMemCost(func() {
|
||||
var rt Table[int]
|
||||
for _, route := range routes {
|
||||
rt.Insert(route.pfx, route.val)
|
||||
}
|
||||
})
|
||||
insertAllocs *= float64(b.N)
|
||||
insertBytes *= float64(b.N)
|
||||
|
||||
var t runningTimer
|
||||
allocs, bytes := getMemCost(func() {
|
||||
for i := 0; i < b.N; i++ {
|
||||
var rt Table[int]
|
||||
for _, route := range routes {
|
||||
rt.Insert(route.pfx, route.val)
|
||||
}
|
||||
t.Start()
|
||||
for _, route := range routes {
|
||||
rt.Delete(route.pfx)
|
||||
}
|
||||
t.Stop()
|
||||
}
|
||||
})
|
||||
inserts := float64(b.N) * float64(len(routes))
|
||||
allocs -= insertAllocs
|
||||
bytes -= insertBytes
|
||||
elapsed := float64(t.Elapsed().Nanoseconds())
|
||||
elapsedSec := t.Elapsed().Seconds()
|
||||
b.ReportMetric(elapsed/inserts, "ns/op")
|
||||
b.ReportMetric(inserts/elapsedSec, "routes/s")
|
||||
b.ReportMetric(roundFloat64(allocs/inserts), "avg-allocs/op")
|
||||
b.ReportMetric(roundFloat64(bytes/inserts), "avg-B/op")
|
||||
})
|
||||
}
|
||||
|
||||
var addrSink netip.Addr
|
||||
|
||||
func BenchmarkTableGet(b *testing.B) {
|
||||
forFamilyAndCount(b, func(b *testing.B, routes []slowPrefixEntry[int]) {
|
||||
genAddr := randomAddr4
|
||||
if routes[0].pfx.Addr().Is6() {
|
||||
genAddr = randomAddr6
|
||||
}
|
||||
var rt Table[int]
|
||||
for _, route := range routes {
|
||||
rt.Insert(route.pfx, route.val)
|
||||
}
|
||||
addrAllocs, addrBytes := getMemCost(func() {
|
||||
// Have to run genAddr more than once, otherwise the reported
|
||||
// cost is 16 bytes - presumably due to some amortized costs in
|
||||
// the memory allocator? Either way, empirically 100 iterations
|
||||
// reliably reports the correct cost.
|
||||
for i := 0; i < 100; i++ {
|
||||
_ = genAddr()
|
||||
}
|
||||
})
|
||||
addrAllocs /= 100
|
||||
addrBytes /= 100
|
||||
var t runningTimer
|
||||
allocs, bytes := getMemCost(func() {
|
||||
for i := 0; i < b.N; i++ {
|
||||
addr := genAddr()
|
||||
t.Start()
|
||||
writeSink = rt.Get(addr)
|
||||
t.Stop()
|
||||
}
|
||||
})
|
||||
b.ReportAllocs() // Enables the output, but we report manually below
|
||||
allocs -= (addrAllocs * float64(b.N))
|
||||
bytes -= (addrBytes * float64(b.N))
|
||||
lookups := float64(b.N)
|
||||
elapsed := float64(t.Elapsed().Nanoseconds())
|
||||
elapsedSec := float64(t.Elapsed().Seconds())
|
||||
b.ReportMetric(elapsed/lookups, "ns/op")
|
||||
b.ReportMetric(lookups/elapsedSec, "addrs/s")
|
||||
b.ReportMetric(allocs/lookups, "allocs/op")
|
||||
b.ReportMetric(bytes/lookups, "B/op")
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
// getMemCost runs fn 100 times and returns the number of allocations and bytes
|
||||
// allocated by each call to fn.
|
||||
//
|
||||
// Note that if your fn allocates very little memory (less than ~16 bytes), you
|
||||
// should make fn run its workload ~100 times and divide the results of
|
||||
// getMemCost yourself. Otherwise, the byte count you get will be rounded up due
|
||||
// to the memory allocator's bucketing granularity.
|
||||
func getMemCost(fn func()) (allocs, bytes float64) {
|
||||
var start, end runtime.MemStats
|
||||
runtime.ReadMemStats(&start)
|
||||
fn()
|
||||
runtime.ReadMemStats(&end)
|
||||
return float64(end.Mallocs - start.Mallocs), float64(end.TotalAlloc - start.TotalAlloc)
|
||||
}
|
||||
|
||||
// runningTimer is a timer that keeps track of the cumulative time it's spent
|
||||
// running since creation. A newly created runningTimer is stopped.
|
||||
//
|
||||
// This timer exists because some of our benchmarks have to interleave costly
|
||||
// ancillary logic in each benchmark iteration, rather than being able to
|
||||
// front-load all the work before a single b.ResetTimer().
|
||||
//
|
||||
// As it turns out, b.StartTimer() and b.StopTimer() are expensive function
|
||||
// calls, because they do costly memory allocation accounting on every call.
|
||||
// Starting and stopping the benchmark timer in every b.N loop iteration slows
|
||||
// the benchmarks down by orders of magnitude.
|
||||
//
|
||||
// So, rather than rely on testing.B's timing facility, we use this very
|
||||
// lightweight timer combined with getMemCost to do our own accounting more
|
||||
// efficiently.
|
||||
type runningTimer struct {
|
||||
cumulative time.Duration
|
||||
start time.Time
|
||||
}
|
||||
|
||||
func (t *runningTimer) Start() {
|
||||
t.Stop()
|
||||
t.start = time.Now()
|
||||
}
|
||||
|
||||
func (t *runningTimer) Stop() {
|
||||
if t.start.IsZero() {
|
||||
return
|
||||
}
|
||||
t.cumulative += time.Since(t.start)
|
||||
t.start = time.Time{}
|
||||
}
|
||||
|
||||
func (t *runningTimer) Elapsed() time.Duration {
|
||||
return t.cumulative
|
||||
}
|
||||
|
||||
// slowPrefixTable is a routing table implemented as a set of prefixes that are
|
||||
// explicitly scanned in full for every route lookup. It is very slow, but also
|
||||
// reasonably easy to verify by inspection, and so a good correctness reference
|
||||
// for Table.
|
||||
type slowPrefixTable[T any] struct {
|
||||
prefixes []slowPrefixEntry[T]
|
||||
}
|
||||
|
||||
type slowPrefixEntry[T any] struct {
|
||||
pfx netip.Prefix
|
||||
val *T
|
||||
}
|
||||
|
||||
func (t *slowPrefixTable[T]) delete(pfx netip.Prefix) {
|
||||
ret := make([]slowPrefixEntry[T], 0, len(t.prefixes))
|
||||
for _, ent := range t.prefixes {
|
||||
if ent.pfx == pfx {
|
||||
continue
|
||||
}
|
||||
ret = append(ret, ent)
|
||||
}
|
||||
t.prefixes = ret
|
||||
}
|
||||
|
||||
func (t *slowPrefixTable[T]) insert(pfx netip.Prefix, val *T) {
|
||||
for _, ent := range t.prefixes {
|
||||
if ent.pfx == pfx {
|
||||
ent.val = val
|
||||
return
|
||||
}
|
||||
}
|
||||
t.prefixes = append(t.prefixes, slowPrefixEntry[T]{pfx, val})
|
||||
}
|
||||
|
||||
func (t *slowPrefixTable[T]) get(addr netip.Addr) *T {
|
||||
var (
|
||||
ret *T
|
||||
bestLen = -1
|
||||
)
|
||||
|
||||
for _, pfx := range t.prefixes {
|
||||
if pfx.pfx.Contains(addr) && pfx.pfx.Bits() > bestLen {
|
||||
ret = pfx.val
|
||||
bestLen = pfx.pfx.Bits()
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
// randomPrefixes returns n randomly generated prefixes and associated values,
|
||||
// distributed equally between IPv4 and IPv6.
|
||||
func randomPrefixes(n int) []slowPrefixEntry[int] {
|
||||
pfxs := randomPrefixes4(n / 2)
|
||||
pfxs = append(pfxs, randomPrefixes6(n-len(pfxs))...)
|
||||
return pfxs
|
||||
}
|
||||
|
||||
// randomPrefixes4 returns n randomly generated IPv4 prefixes and associated values.
|
||||
func randomPrefixes4(n int) []slowPrefixEntry[int] {
|
||||
pfxs := map[netip.Prefix]bool{}
|
||||
|
||||
for len(pfxs) < n {
|
||||
len := rand.Intn(33)
|
||||
pfx, err := randomAddr4().Prefix(len)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
pfxs[pfx] = true
|
||||
}
|
||||
|
||||
ret := make([]slowPrefixEntry[int], 0, len(pfxs))
|
||||
for pfx := range pfxs {
|
||||
ret = append(ret, slowPrefixEntry[int]{pfx, ptr.To(rand.Int())})
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
// randomPrefixes6 returns n randomly generated IPv4 prefixes and associated values.
|
||||
func randomPrefixes6(n int) []slowPrefixEntry[int] {
|
||||
pfxs := map[netip.Prefix]bool{}
|
||||
|
||||
for len(pfxs) < n {
|
||||
len := rand.Intn(129)
|
||||
pfx, err := randomAddr6().Prefix(len)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
pfxs[pfx] = true
|
||||
}
|
||||
|
||||
ret := make([]slowPrefixEntry[int], 0, len(pfxs))
|
||||
for pfx := range pfxs {
|
||||
ret = append(ret, slowPrefixEntry[int]{pfx, ptr.To(rand.Int())})
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
// randomAddr returns a randomly generated IP address.
|
||||
func randomAddr() netip.Addr {
|
||||
if rand.Intn(2) == 1 {
|
||||
return randomAddr6()
|
||||
} else {
|
||||
return randomAddr4()
|
||||
}
|
||||
}
|
||||
|
||||
// randomAddr4 returns a randomly generated IPv4 address.
|
||||
func randomAddr4() netip.Addr {
|
||||
var b [4]byte
|
||||
if _, err := crand.Read(b[:]); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return netip.AddrFrom4(b)
|
||||
}
|
||||
|
||||
// randomAddr6 returns a randomly generated IPv6 address.
|
||||
func randomAddr6() netip.Addr {
|
||||
var b [16]byte
|
||||
if _, err := crand.Read(b[:]); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return netip.AddrFrom16(b)
|
||||
}
|
||||
|
||||
// printIntPtr returns *v as a string, or the literal "<nil>" if v is nil.
|
||||
func printIntPtr(v *int) string {
|
||||
if v == nil {
|
||||
return "<nil>"
|
||||
}
|
||||
return fmt.Sprint(*v)
|
||||
}
|
||||
|
||||
// roundFloat64 rounds f to 2 decimal places, for display.
|
||||
//
|
||||
// It round-trips through a float->string->float conversion, so should not be
|
||||
// used in a performance critical setting.
|
||||
func roundFloat64(f float64) float64 {
|
||||
s := fmt.Sprintf("%.2f", f)
|
||||
ret, err := strconv.ParseFloat(s, 64)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
|
||||
"golang.org/x/sync/errgroup"
|
||||
"tailscale.com/net/packet"
|
||||
"tailscale.com/net/tsaddr"
|
||||
"tailscale.com/types/netlogtype"
|
||||
)
|
||||
|
||||
@@ -92,6 +93,11 @@ func (s *Statistics) UpdateRxVirtual(b []byte) {
|
||||
s.updateVirtual(b, true)
|
||||
}
|
||||
|
||||
var (
|
||||
tailscaleServiceIPv4 = tsaddr.TailscaleServiceIP()
|
||||
tailscaleServiceIPv6 = tsaddr.TailscaleServiceIPv6()
|
||||
)
|
||||
|
||||
func (s *Statistics) updateVirtual(b []byte, receive bool) {
|
||||
var p packet.Parsed
|
||||
p.Decode(b)
|
||||
@@ -100,6 +106,15 @@ func (s *Statistics) updateVirtual(b []byte, receive bool) {
|
||||
conn.Src, conn.Dst = conn.Dst, conn.Src
|
||||
}
|
||||
|
||||
// Network logging is defined as traffic between two Tailscale nodes.
|
||||
// Traffic with the internal Tailscale service is not with another node
|
||||
// and should not be logged. It also happens to be a high volume
|
||||
// amount of discrete traffic flows (e.g., DNS lookups).
|
||||
switch conn.Dst.Addr() {
|
||||
case tailscaleServiceIPv4, tailscaleServiceIPv6:
|
||||
return
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
cnts, found := s.virtual[conn]
|
||||
|
||||
@@ -18,12 +18,12 @@ import (
|
||||
"golang.org/x/exp/slices"
|
||||
"tailscale.com/health"
|
||||
"tailscale.com/net/dns/resolver"
|
||||
"tailscale.com/net/netmon"
|
||||
"tailscale.com/net/tsdial"
|
||||
"tailscale.com/types/dnstype"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/clientmetric"
|
||||
"tailscale.com/util/dnsname"
|
||||
"tailscale.com/wgengine/monitor"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -64,14 +64,15 @@ type Manager struct {
|
||||
}
|
||||
|
||||
// NewManagers created a new manager from the given config.
|
||||
func NewManager(logf logger.Logf, oscfg OSConfigurator, linkMon *monitor.Mon, dialer *tsdial.Dialer, linkSel resolver.ForwardLinkSelector) *Manager {
|
||||
// The netMon parameter is optional; if non-nil it's used to do faster interface lookups.
|
||||
func NewManager(logf logger.Logf, oscfg OSConfigurator, netMon *netmon.Monitor, dialer *tsdial.Dialer, linkSel resolver.ForwardLinkSelector) *Manager {
|
||||
if dialer == nil {
|
||||
panic("nil Dialer")
|
||||
}
|
||||
logf = logger.WithPrefix(logf, "dns: ")
|
||||
m := &Manager{
|
||||
logf: logf,
|
||||
resolver: resolver.New(logf, linkMon, linkSel, dialer),
|
||||
resolver: resolver.New(logf, netMon, linkSel, dialer),
|
||||
os: oscfg,
|
||||
}
|
||||
m.ctx, m.ctxCancel = context.WithCancel(context.Background())
|
||||
|
||||
@@ -108,7 +108,7 @@ func TestDNSOverTCP(t *testing.T) {
|
||||
"bradfitz.ts.com.": "2.3.4.5",
|
||||
}
|
||||
|
||||
for domain, _ := range wantResults {
|
||||
for domain := range wantResults {
|
||||
b := mkDNSRequest(domain, dns.TypeA, addEDNS)
|
||||
binary.Write(c, binary.BigEndian, uint16(len(b)))
|
||||
c.Write(b)
|
||||
|
||||
@@ -502,7 +502,7 @@ func genRandomSubdomains(t *testing.T, n int) []dnsname.FQDN {
|
||||
for len(domains) < cap(domains) {
|
||||
l := r.Intn(19) + 1
|
||||
b := make([]byte, l)
|
||||
for i, _ := range b {
|
||||
for i := range b {
|
||||
b[i] = charset[r.Intn(len(charset))]
|
||||
}
|
||||
d := string(b) + ".example.com"
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user