Compare commits
211 Commits
aaron/migr
...
dgentry/at
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
765b519643 | ||
|
|
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 | ||
|
|
6f9aed1656 | ||
|
|
4cb1bfee44 | ||
|
|
4a89642f7f | ||
|
|
9e81db50f6 | ||
|
|
8a11f76a0d | ||
|
|
ec90522a53 | ||
|
|
0e203e414f | ||
|
|
0bf8c8e710 | ||
|
|
f6ea6863de | ||
|
|
bb31fd7d1c | ||
|
|
535fad16f8 | ||
|
|
f61b306133 | ||
|
|
583e86b7df | ||
|
|
df89b7de10 | ||
|
|
8a246487c2 | ||
|
|
8765568373 | ||
|
|
9d8b7a7383 | ||
|
|
57a008a1e1 | ||
|
|
13377e6458 | ||
|
|
9de8287d47 | ||
|
|
c350cd1f06 | ||
|
|
f13b8bf0cf | ||
|
|
731688e5cc | ||
|
|
7083246409 | ||
|
|
d92047cc30 | ||
|
|
7a97e64ef0 | ||
|
|
cc3806056f | ||
|
|
916aa782af | ||
|
|
60cd4ac08d | ||
|
|
1b78dc1f33 | ||
|
|
3efd83555f | ||
|
|
812025a39c | ||
|
|
39b289578e | ||
|
|
c9a4dbe383 | ||
|
|
f11c270c6b | ||
|
|
d2dec13392 | ||
|
|
e7a78bc28f | ||
|
|
df02bb013a | ||
|
|
ebc630c6c0 | ||
|
|
ccace1f7df | ||
|
|
e1fb687104 | ||
|
|
654b5a0616 | ||
|
|
50d211d1a4 | ||
|
|
e59dc29a55 | ||
|
|
60a028a4f6 | ||
|
|
927e2e3e7c | ||
|
|
82e067e0ff | ||
|
|
95494a155e | ||
|
|
9534783758 | ||
|
|
f34590d9ed | ||
|
|
c6d96a2b61 | ||
|
|
0498d5ea86 | ||
|
|
1f95bfedf7 | ||
|
|
9526858b1e | ||
|
|
df3996cae3 | ||
|
|
97b6d3e917 | ||
|
|
9ebab961c9 | ||
|
|
6d3490f399 | ||
|
|
51b0169b10 | ||
|
|
b4d3e2928b | ||
|
|
2b892ad6e7 | ||
|
|
6ef2105a8e | ||
|
|
8c4adde083 | ||
|
|
c87782ba9d | ||
|
|
09e0ccf4c2 | ||
|
|
a1d9f65354 | ||
|
|
5e8a80b845 | ||
|
|
558735bc63 | ||
|
|
489e27f085 | ||
|
|
56526ff57f | ||
|
|
09aed46d44 | ||
|
|
223713d4a1 | ||
|
|
83fa17d26c | ||
|
|
958c89470b | ||
|
|
e109cf9fdd | ||
|
|
3ff44b2307 | ||
|
|
ccdd534e81 | ||
|
|
047b324933 | ||
|
|
f0d6228c52 | ||
|
|
920de86cee | ||
|
|
b64d78d58f | ||
|
|
ea81bffdeb | ||
|
|
1e72de6b72 | ||
|
|
92fc243755 | ||
|
|
3471fbf8dc | ||
|
|
b797f773c7 | ||
|
|
dad78f31f3 | ||
|
|
be027a9899 | ||
|
|
87b4bbb94f | ||
|
|
4c2f67a1d0 | ||
|
|
e69682678f | ||
|
|
a2be1aabfa | ||
|
|
ce99474317 | ||
|
|
f4f8ed98d9 | ||
|
|
6eca47b16c | ||
|
|
48f6c1eba4 | ||
|
|
b0cb39cda1 | ||
|
|
c09578d060 | ||
|
|
a75360ccd6 | ||
|
|
5b68dcc8c1 | ||
|
|
3862a1e1d5 | ||
|
|
be107f92d3 | ||
|
|
9245d813c6 | ||
|
|
f7a7957a11 | ||
|
|
49e2d3a7bd | ||
|
|
b46c5ae82a | ||
|
|
7e6c5a2db4 | ||
|
|
9112e78925 | ||
|
|
3b18e65c6a | ||
|
|
6ac6ddbb47 | ||
|
|
9687f3700d | ||
|
|
2263d9c44b | ||
|
|
387b68fe11 | ||
|
|
df2561f6a2 | ||
|
|
96a555fc5a | ||
|
|
0f4359116e | ||
|
|
9ff51ca17f | ||
|
|
045f995203 | ||
|
|
f6cd24499b | ||
|
|
51eb0b2cb7 | ||
|
|
d379a25ae4 | ||
|
|
69f9c17555 | ||
|
|
1a30b2d73f | ||
|
|
57a44846ae | ||
|
|
a9c17dbf93 | ||
|
|
2d3ae485e3 | ||
|
|
b9ebf7cf14 | ||
|
|
12100320d2 | ||
|
|
73fa7dd7af | ||
|
|
88c7d19d54 | ||
|
|
e2d652ec4d | ||
|
|
3f8e8b04fd | ||
|
|
3e71e0ef68 | ||
|
|
7b73c9628d | ||
|
|
d92ef4c215 | ||
|
|
27575cd52d | ||
|
|
ef6f66bb9a | ||
|
|
1410682fb6 | ||
|
|
283a84724f | ||
|
|
e1530cdfcc | ||
|
|
5eb8a2a86a | ||
|
|
d8286d0dc2 | ||
|
|
51288221ce | ||
|
|
06302e30ae | ||
|
|
311352d195 | ||
|
|
0df11253ec | ||
|
|
f18beaa1e4 | ||
|
|
7985f5243a | ||
|
|
ff168a806e | ||
|
|
bb7033174c | ||
|
|
7e4788e383 | ||
|
|
9cb332f0e2 | ||
|
|
0c1510739c | ||
|
|
06134e9521 | ||
|
|
0d19f5d421 | ||
|
|
d41f6a8752 | ||
|
|
768df4ff7a | ||
|
|
e3211ff88b | ||
|
|
49c206fe1e |
8
.github/workflows/go-licenses.yml
vendored
8
.github/workflows/go-licenses.yml
vendored
@@ -17,7 +17,7 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
tailscale:
|
||||
update-licenses:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v3
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
|
||||
@@ -42,7 +42,7 @@ jobs:
|
||||
go-licenses report tailscale.com/cmd/tailscale tailscale.com/cmd/tailscaled > licenses/tailscale.md --template .github/licenses.tmpl
|
||||
|
||||
- name: Get access token
|
||||
uses: tibdex/github-app-token@f717b5ecd4534d3c4df4ce9b5c1c2214f0f7cd06 # v1.6.0
|
||||
uses: tibdex/github-app-token@b62528385c34dbc9f38e5f4225ac829252d1ea92 # v1.8.0
|
||||
id: generate-token
|
||||
with:
|
||||
app_id: ${{ secrets.LICENSING_APP_ID }}
|
||||
@@ -50,7 +50,7 @@ jobs:
|
||||
private_key: ${{ secrets.LICENSING_APP_PRIVATE_KEY }}
|
||||
|
||||
- name: Send pull request
|
||||
uses: peter-evans/create-pull-request@ad43dccb4d726ca8514126628bec209b8354b6dd #v4.1.4
|
||||
uses: peter-evans/create-pull-request@38e0b6e68b4c852a5500a94740f0e535e0d7ba54 #v4.2.4
|
||||
with:
|
||||
token: ${{ steps.generate-token.outputs.token }}
|
||||
author: License Updater <noreply@tailscale.com>
|
||||
|
||||
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
|
||||
6
.github/workflows/update-flake.yml
vendored
6
.github/workflows/update-flake.yml
vendored
@@ -16,7 +16,7 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
tailscale:
|
||||
update-flake:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
run: ./update-flake.sh
|
||||
|
||||
- name: Get access token
|
||||
uses: tibdex/github-app-token@f717b5ecd4534d3c4df4ce9b5c1c2214f0f7cd06 # v1.6.0
|
||||
uses: tibdex/github-app-token@b62528385c34dbc9f38e5f4225ac829252d1ea92 # v1.8.0
|
||||
id: generate-token
|
||||
with:
|
||||
app_id: ${{ secrets.LICENSING_APP_ID }}
|
||||
@@ -35,7 +35,7 @@ jobs:
|
||||
private_key: ${{ secrets.LICENSING_APP_PRIVATE_KEY }}
|
||||
|
||||
- name: Send pull request
|
||||
uses: peter-evans/create-pull-request@ad43dccb4d726ca8514126628bec209b8354b6dd #v4.1.4
|
||||
uses: peter-evans/create-pull-request@38e0b6e68b4c852a5500a94740f0e535e0d7ba54 #v4.2.4
|
||||
with:
|
||||
token: ${{ steps.generate-token.outputs.token }}
|
||||
author: Flakes Updater <noreply@tailscale.com>
|
||||
|
||||
@@ -1 +1 @@
|
||||
1.37.0
|
||||
1.39.0
|
||||
|
||||
@@ -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
|
||||
|
||||
38
atomicfile/atomicfile_test.go
Normal file
38
atomicfile/atomicfile_test.go
Normal file
@@ -0,0 +1,38 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build linux
|
||||
|
||||
package atomicfile
|
||||
|
||||
import (
|
||||
"net"
|
||||
"path/filepath"
|
||||
"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.
|
||||
|
||||
d := t.TempDir()
|
||||
special := filepath.Join(d, "special")
|
||||
|
||||
// The least troublesome thing to make that is not a file is a unix socket.
|
||||
// Making a null device sadly requries root.
|
||||
l, err := net.ListenUnix("unix", &net.UnixAddr{Name: special, Net: "unix"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer l.Close()
|
||||
|
||||
err = WriteFile(special, []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)
|
||||
}
|
||||
}
|
||||
@@ -16,7 +16,7 @@ if [ -n "${TS_USE_TOOLCHAIN:-}" ]; then
|
||||
go="./tool/go"
|
||||
fi
|
||||
|
||||
eval `$go run ./cmd/mkversion`
|
||||
eval `GOOS=$($go env GOHOSTOS) GOARCH=$($go env GOHOSTARCH) $go run ./cmd/mkversion`
|
||||
|
||||
if [ "$1" = "shellvars" ]; then
|
||||
cat <<EOF
|
||||
|
||||
@@ -103,7 +103,7 @@ func (c *Client) ACL(ctx context.Context) (acl *ACL, err error) {
|
||||
// it as a string.
|
||||
// HuJSON is JSON with a few modifications to make it more human-friendly. The primary
|
||||
// changes are allowing comments and trailing comments. See the following links for more info:
|
||||
// https://tailscale.com/kb/1018/acls?q=acl#tailscale-acl-policy-format
|
||||
// https://tailscale.com/s/acl-format
|
||||
// https://github.com/tailscale/hujson
|
||||
func (c *Client) ACLHuJSON(ctx context.Context) (acl *ACLHuJSON, err error) {
|
||||
// Format return errors to be descriptive.
|
||||
|
||||
@@ -36,6 +36,7 @@ import (
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tka"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/tkatype"
|
||||
)
|
||||
|
||||
// defaultLocalClient is the default LocalClient when using the legacy
|
||||
@@ -367,6 +368,34 @@ func (lc *LocalClient) DebugAction(ctx context.Context, action string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// DebugPortmap invokes the debug-portmap endpoint, and returns an
|
||||
// io.ReadCloser that can be used to read the logs that are printed during this
|
||||
// process.
|
||||
func (lc *LocalClient) DebugPortmap(ctx context.Context, duration time.Duration, ty, gwSelf string) (io.ReadCloser, error) {
|
||||
vals := make(url.Values)
|
||||
vals.Set("duration", duration.String())
|
||||
vals.Set("type", ty)
|
||||
if gwSelf != "" {
|
||||
vals.Set("gateway_and_self", gwSelf)
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", "http://"+apitype.LocalAPIHost+"/localapi/v0/debug-portmap?"+vals.Encode(), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res, err := lc.doLocalRequestNiceError(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if res.StatusCode != 200 {
|
||||
body, _ := io.ReadAll(res.Body)
|
||||
res.Body.Close()
|
||||
return nil, fmt.Errorf("HTTP %s: %s", res.Status, body)
|
||||
}
|
||||
|
||||
return res.Body, nil
|
||||
}
|
||||
|
||||
// SetDevStoreKeyValue set a statestore key/value. It's only meant for development.
|
||||
// The schema (including when keys are re-read) is not a stable interface.
|
||||
func (lc *LocalClient) SetDevStoreKeyValue(ctx context.Context, key, value string) error {
|
||||
@@ -821,6 +850,30 @@ func (lc *LocalClient) NetworkLockInit(ctx context.Context, keys []tka.Key, disa
|
||||
return decodeJSON[*ipnstate.NetworkLockStatus](body)
|
||||
}
|
||||
|
||||
// NetworkLockWrapPreauthKey wraps a pre-auth key with information to
|
||||
// enable unattended bringup in the locked tailnet.
|
||||
func (lc *LocalClient) NetworkLockWrapPreauthKey(ctx context.Context, preauthKey string, tkaKey key.NLPrivate) (string, error) {
|
||||
encodedPrivate, err := tkaKey.MarshalText()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var b bytes.Buffer
|
||||
type wrapRequest struct {
|
||||
TSKey string
|
||||
TKAKey string // key.NLPrivate.MarshalText
|
||||
}
|
||||
if err := json.NewEncoder(&b).Encode(wrapRequest{TSKey: preauthKey, TKAKey: string(encodedPrivate)}); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
body, err := lc.send(ctx, "POST", "/localapi/v0/tka/wrap-preauth-key", 200, &b)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error: %w", err)
|
||||
}
|
||||
return string(body), nil
|
||||
}
|
||||
|
||||
// NetworkLockModify adds and/or removes key(s) to the tailnet key authority.
|
||||
func (lc *LocalClient) NetworkLockModify(ctx context.Context, addKeys, removeKeys []tka.Key) error {
|
||||
var b bytes.Buffer
|
||||
@@ -858,6 +911,15 @@ func (lc *LocalClient) NetworkLockSign(ctx context.Context, nodeKey key.NodePubl
|
||||
return nil
|
||||
}
|
||||
|
||||
// NetworkLockAffectedSigs returns all signatures signed by the specified keyID.
|
||||
func (lc *LocalClient) NetworkLockAffectedSigs(ctx context.Context, keyID tkatype.KeyID) ([]tkatype.MarshaledSignature, error) {
|
||||
body, err := lc.send(ctx, "POST", "/localapi/v0/tka/affected-sigs", 200, bytes.NewReader(keyID))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error: %w", err)
|
||||
}
|
||||
return decodeJSON[[]tkatype.MarshaledSignature](body)
|
||||
}
|
||||
|
||||
// NetworkLockLog returns up to maxEntries number of changes to network-lock state.
|
||||
func (lc *LocalClient) NetworkLockLog(ctx context.Context, maxEntries int) ([]ipnstate.NetworkLockUpdate, error) {
|
||||
v := url.Values{}
|
||||
@@ -1039,7 +1101,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 {
|
||||
|
||||
@@ -6,138 +6,28 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"tailscale.com/kube"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/util/multierr"
|
||||
)
|
||||
|
||||
// checkSecretPermissions checks the secret access permissions of the current
|
||||
// pod. It returns an error if the basic permissions tailscale needs are
|
||||
// missing, and reports whether the patch permission is additionally present.
|
||||
//
|
||||
// Errors encountered during the access checking process are logged, but ignored
|
||||
// so that the pod tries to fail alive if the permissions exist and there's just
|
||||
// something wrong with SelfSubjectAccessReviews. There shouldn't be, pods
|
||||
// should always be able to use SSARs to assess their own permissions, but since
|
||||
// we didn't use to check permissions this way we'll be cautious in case some
|
||||
// old version of k8s deviates from the current behavior.
|
||||
func checkSecretPermissions(ctx context.Context, secretName string) (canPatch bool, err error) {
|
||||
var errs []error
|
||||
for _, verb := range []string{"get", "update"} {
|
||||
ok, err := checkPermission(ctx, verb, secretName)
|
||||
if err != nil {
|
||||
log.Printf("error checking %s permission on secret %s: %v", verb, secretName, err)
|
||||
} else if !ok {
|
||||
errs = append(errs, fmt.Errorf("missing %s permission on secret %q", verb, secretName))
|
||||
}
|
||||
}
|
||||
if len(errs) > 0 {
|
||||
return false, multierr.New(errs...)
|
||||
}
|
||||
ok, err := checkPermission(ctx, "patch", secretName)
|
||||
if err != nil {
|
||||
log.Printf("error checking patch permission on secret %s: %v", secretName, err)
|
||||
return false, nil
|
||||
}
|
||||
return ok, nil
|
||||
}
|
||||
|
||||
// checkPermission reports whether the current pod has permission to use the
|
||||
// given verb (e.g. get, update, patch) on secretName.
|
||||
func checkPermission(ctx context.Context, verb, secretName string) (bool, error) {
|
||||
sar := map[string]any{
|
||||
"apiVersion": "authorization.k8s.io/v1",
|
||||
"kind": "SelfSubjectAccessReview",
|
||||
"spec": map[string]any{
|
||||
"resourceAttributes": map[string]any{
|
||||
"namespace": kubeNamespace,
|
||||
"verb": verb,
|
||||
"resource": "secrets",
|
||||
"name": secretName,
|
||||
},
|
||||
},
|
||||
}
|
||||
bs, err := json.Marshal(sar)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
req, err := http.NewRequest("POST", "/apis/authorization.k8s.io/v1/selfsubjectaccessreviews", bytes.NewReader(bs))
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
resp, err := doKubeRequest(ctx, req)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
bs, err = io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
var res struct {
|
||||
Status struct {
|
||||
Allowed bool `json:"allowed"`
|
||||
} `json:"status"`
|
||||
}
|
||||
if err := json.Unmarshal(bs, &res); err != nil {
|
||||
return false, err
|
||||
}
|
||||
return res.Status.Allowed, nil
|
||||
}
|
||||
|
||||
// findKeyInKubeSecret inspects the kube secret secretName for a data
|
||||
// field called "authkey", and returns its value if present.
|
||||
func findKeyInKubeSecret(ctx context.Context, secretName string) (string, error) {
|
||||
req, err := http.NewRequest("GET", fmt.Sprintf("/api/v1/namespaces/%s/secrets/%s", kubeNamespace, secretName), nil)
|
||||
s, err := kc.GetSecret(ctx, secretName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
resp, err := doKubeRequest(ctx, req)
|
||||
if err != nil {
|
||||
if resp != nil && resp.StatusCode == http.StatusNotFound {
|
||||
// Kube secret doesn't exist yet, can't have an authkey.
|
||||
return "", nil
|
||||
}
|
||||
return "", err
|
||||
ak, ok := s.Data["authkey"]
|
||||
if !ok {
|
||||
return "", nil
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
bs, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// We use a map[string]any here rather than import corev1.Secret,
|
||||
// because we only do very limited things to the secret, and
|
||||
// importing corev1 adds 12MiB to the compiled binary.
|
||||
var s map[string]any
|
||||
if err := json.Unmarshal(bs, &s); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if d, ok := s["data"].(map[string]any); ok {
|
||||
if v, ok := d["authkey"].(string); ok {
|
||||
bs, err := base64.StdEncoding.DecodeString(v)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(bs), nil
|
||||
}
|
||||
}
|
||||
return "", nil
|
||||
return string(ak), nil
|
||||
}
|
||||
|
||||
// storeDeviceInfo writes deviceID into the "device_id" data field of the kube
|
||||
@@ -145,65 +35,38 @@ func findKeyInKubeSecret(ctx context.Context, secretName string) (string, error)
|
||||
func storeDeviceInfo(ctx context.Context, secretName string, deviceID tailcfg.StableNodeID, fqdn string) error {
|
||||
// First check if the secret exists at all. Even if running on
|
||||
// kubernetes, we do not necessarily store state in a k8s secret.
|
||||
req, err := http.NewRequest("GET", fmt.Sprintf("/api/v1/namespaces/%s/secrets/%s", kubeNamespace, secretName), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resp, err := doKubeRequest(ctx, req)
|
||||
if err != nil {
|
||||
if resp != nil && resp.StatusCode >= 400 && resp.StatusCode <= 499 {
|
||||
// Assume the secret doesn't exist, or we don't have
|
||||
// permission to access it.
|
||||
return nil
|
||||
if _, err := kc.GetSecret(ctx, secretName); err != nil {
|
||||
if s, ok := err.(*kube.Status); ok {
|
||||
if s.Code >= 400 && s.Code <= 499 {
|
||||
// Assume the secret doesn't exist, or we don't have
|
||||
// permission to access it.
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
m := map[string]map[string]string{
|
||||
"stringData": {
|
||||
"device_id": string(deviceID),
|
||||
"device_fqdn": fqdn,
|
||||
m := &kube.Secret{
|
||||
Data: map[string][]byte{
|
||||
"device_id": []byte(deviceID),
|
||||
"device_fqdn": []byte(fqdn),
|
||||
},
|
||||
}
|
||||
var b bytes.Buffer
|
||||
if err := json.NewEncoder(&b).Encode(m); err != nil {
|
||||
return err
|
||||
}
|
||||
req, err = http.NewRequest("PATCH", fmt.Sprintf("/api/v1/namespaces/%s/secrets/%s?fieldManager=tailscale-container", kubeNamespace, secretName), &b)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/strategic-merge-patch+json")
|
||||
if _, err := doKubeRequest(ctx, req); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
return kc.StrategicMergePatchSecret(ctx, secretName, m, "tailscale-container")
|
||||
}
|
||||
|
||||
// deleteAuthKey deletes the 'authkey' field of the given kube
|
||||
// secret. No-op if there is no authkey in the secret.
|
||||
func deleteAuthKey(ctx context.Context, secretName string) error {
|
||||
// m is a JSON Patch data structure, see https://jsonpatch.com/ or RFC 6902.
|
||||
m := []struct {
|
||||
Op string `json:"op"`
|
||||
Path string `json:"path"`
|
||||
}{
|
||||
m := []kube.JSONPatch{
|
||||
{
|
||||
Op: "remove",
|
||||
Path: "/data/authkey",
|
||||
},
|
||||
}
|
||||
var b bytes.Buffer
|
||||
if err := json.NewEncoder(&b).Encode(m); err != nil {
|
||||
return err
|
||||
}
|
||||
req, err := http.NewRequest("PATCH", fmt.Sprintf("/api/v1/namespaces/%s/secrets/%s?fieldManager=tailscale-container", kubeNamespace, secretName), &b)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json-patch+json")
|
||||
if resp, err := doKubeRequest(ctx, req); err != nil {
|
||||
if resp != nil && resp.StatusCode == http.StatusUnprocessableEntity {
|
||||
if err := kc.JSONPatchSecret(ctx, secretName, m); err != nil {
|
||||
if s, ok := err.(*kube.Status); ok && s.Code == http.StatusUnprocessableEntity {
|
||||
// This is kubernetes-ese for "the field you asked to
|
||||
// delete already doesn't exist", aka no-op.
|
||||
return nil
|
||||
@@ -213,65 +76,22 @@ func deleteAuthKey(ctx context.Context, secretName string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
kubeHost string
|
||||
kubeNamespace string
|
||||
kubeToken string
|
||||
kubeHTTP *http.Transport
|
||||
)
|
||||
var kc *kube.Client
|
||||
|
||||
func initKube(root string) {
|
||||
// If running in Kubernetes, set things up so that doKubeRequest
|
||||
// can talk successfully to the kube apiserver.
|
||||
if os.Getenv("KUBERNETES_SERVICE_HOST") == "" {
|
||||
return
|
||||
if root != "/" {
|
||||
// If we are running in a test, we need to set the root path to the fake
|
||||
// service account directory.
|
||||
kube.SetRootPathForTesting(root)
|
||||
}
|
||||
|
||||
kubeHost = os.Getenv("KUBERNETES_SERVICE_HOST") + ":" + os.Getenv("KUBERNETES_SERVICE_PORT_HTTPS")
|
||||
|
||||
bs, err := os.ReadFile(filepath.Join(root, "var/run/secrets/kubernetes.io/serviceaccount/namespace"))
|
||||
var err error
|
||||
kc, err = kube.New()
|
||||
if err != nil {
|
||||
log.Fatalf("Error reading kube namespace: %v", err)
|
||||
log.Fatalf("Error creating kube client: %v", err)
|
||||
}
|
||||
kubeNamespace = strings.TrimSpace(string(bs))
|
||||
|
||||
bs, err = os.ReadFile(filepath.Join(root, "var/run/secrets/kubernetes.io/serviceaccount/token"))
|
||||
if err != nil {
|
||||
log.Fatalf("Error reading kube token: %v", err)
|
||||
}
|
||||
kubeToken = strings.TrimSpace(string(bs))
|
||||
|
||||
bs, err = os.ReadFile(filepath.Join(root, "var/run/secrets/kubernetes.io/serviceaccount/ca.crt"))
|
||||
if err != nil {
|
||||
log.Fatalf("Error reading kube CA cert: %v", err)
|
||||
}
|
||||
cp := x509.NewCertPool()
|
||||
cp.AppendCertsFromPEM(bs)
|
||||
kubeHTTP = &http.Transport{
|
||||
TLSClientConfig: &tls.Config{
|
||||
RootCAs: cp,
|
||||
},
|
||||
IdleConnTimeout: time.Second,
|
||||
if root != "/" {
|
||||
// If we are running in a test, we need to set the URL to the
|
||||
// httptest server.
|
||||
kc.SetURL(fmt.Sprintf("https://%s:%s", os.Getenv("KUBERNETES_SERVICE_HOST"), os.Getenv("KUBERNETES_SERVICE_PORT_HTTPS")))
|
||||
}
|
||||
}
|
||||
|
||||
// doKubeRequest sends r to the kube apiserver.
|
||||
func doKubeRequest(ctx context.Context, r *http.Request) (*http.Response, error) {
|
||||
if kubeHTTP == nil {
|
||||
panic("not in kubernetes")
|
||||
}
|
||||
|
||||
r.URL.Scheme = "https"
|
||||
r.URL.Host = kubeHost
|
||||
r.Header.Set("Authorization", "Bearer "+kubeToken)
|
||||
r.Header.Set("Accept", "application/json")
|
||||
|
||||
resp, err := kubeHTTP.RoundTrip(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
|
||||
return resp, fmt.Errorf("got non-200/201 status code %d", resp.StatusCode)
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
@@ -123,7 +123,7 @@ func main() {
|
||||
defer cancel()
|
||||
|
||||
if cfg.InKubernetes && cfg.KubeSecret != "" {
|
||||
canPatch, err := checkSecretPermissions(ctx, cfg.KubeSecret)
|
||||
canPatch, err := kc.CheckSecretPermissions(ctx, cfg.KubeSecret)
|
||||
if err != nil {
|
||||
log.Fatalf("Some Kubernetes permissions are missing, please check your RBAC configuration: %v", err)
|
||||
}
|
||||
|
||||
@@ -607,7 +607,7 @@ func TestContainerBoot(t *testing.T) {
|
||||
}()
|
||||
|
||||
var wantCmds []string
|
||||
for _, p := range test.Phases {
|
||||
for i, p := range test.Phases {
|
||||
lapi.Notify(p.Notify)
|
||||
wantCmds = append(wantCmds, p.WantCmds...)
|
||||
waitArgs(t, 2*time.Second, d, argFile, strings.Join(wantCmds, "\n"))
|
||||
@@ -626,7 +626,7 @@ func TestContainerBoot(t *testing.T) {
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
t.Fatalf("phase %d: %v", i, err)
|
||||
}
|
||||
err = tstest.WaitFor(2*time.Second, func() error {
|
||||
for path, want := range p.WantFiles {
|
||||
@@ -983,13 +983,13 @@ func (k *kubeServer) serveSecret(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
case "application/strategic-merge-patch+json":
|
||||
req := struct {
|
||||
Data map[string]string `json:"stringData"`
|
||||
Data map[string][]byte `json:"data"`
|
||||
}{}
|
||||
if err := json.Unmarshal(bs, &req); err != nil {
|
||||
panic(fmt.Sprintf("json decode failed: %v. Body:\n\n%s", err, string(bs)))
|
||||
}
|
||||
for key, val := range req.Data {
|
||||
k.secret[key] = val
|
||||
k.secret[key] = string(val)
|
||||
}
|
||||
default:
|
||||
panic(fmt.Sprintf("unknown content type %q", r.Header.Get("Content-Type")))
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"time"
|
||||
|
||||
"tailscale.com/syncs"
|
||||
"tailscale.com/util/slicesx"
|
||||
)
|
||||
|
||||
const refreshTimeout = time.Minute
|
||||
@@ -52,6 +53,13 @@ func refreshBootstrapDNS() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), refreshTimeout)
|
||||
defer cancel()
|
||||
dnsEntries := resolveList(ctx, strings.Split(*bootstrapDNS, ","))
|
||||
// Randomize the order of the IPs for each name to avoid the client biasing
|
||||
// to IPv6
|
||||
for k := range dnsEntries {
|
||||
ips := dnsEntries[k]
|
||||
slicesx.Shuffle(ips)
|
||||
dnsEntries[k] = ips
|
||||
}
|
||||
j, err := json.MarshalIndent(dnsEntries, "", "\t")
|
||||
if err != nil {
|
||||
// leave the old values in place
|
||||
|
||||
@@ -11,14 +11,12 @@ import (
|
||||
"net/url"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"tailscale.com/tstest"
|
||||
)
|
||||
|
||||
func BenchmarkHandleBootstrapDNS(b *testing.B) {
|
||||
prev := *bootstrapDNS
|
||||
*bootstrapDNS = "log.tailscale.io,login.tailscale.com,controlplane.tailscale.com,login.us.tailscale.com"
|
||||
defer func() {
|
||||
*bootstrapDNS = prev
|
||||
}()
|
||||
tstest.Replace(b, bootstrapDNS, "log.tailscale.io,login.tailscale.com,controlplane.tailscale.com,login.us.tailscale.com")
|
||||
refreshBootstrapDNS()
|
||||
w := new(bitbucketResponseWriter)
|
||||
req, _ := http.NewRequest("GET", "https://localhost/bootstrap-dns?q="+url.QueryEscape("log.tailscale.io"), nil)
|
||||
|
||||
@@ -47,6 +47,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
|
||||
tailscale.com/net/netns from tailscale.com/derp/derphttp
|
||||
tailscale.com/net/netutil from tailscale.com/client/tailscale
|
||||
tailscale.com/net/packet from tailscale.com/wgengine/filter
|
||||
tailscale.com/net/sockstats from tailscale.com/derp/derphttp
|
||||
tailscale.com/net/stun from tailscale.com/cmd/derper
|
||||
tailscale.com/net/tlsdial from tailscale.com/derp/derphttp
|
||||
tailscale.com/net/tsaddr from tailscale.com/ipn+
|
||||
@@ -59,7 +60,7 @@ 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/types/dnstype from tailscale.com/tailcfg
|
||||
tailscale.com/types/empty from tailscale.com/ipn
|
||||
@@ -86,6 +87,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
|
||||
tailscale.com/util/multierr from tailscale.com/health
|
||||
tailscale.com/util/set from tailscale.com/health
|
||||
tailscale.com/util/singleflight from tailscale.com/net/dnscache
|
||||
tailscale.com/util/slicesx from tailscale.com/cmd/derper+
|
||||
tailscale.com/util/vizerror from tailscale.com/tsweb
|
||||
W 💣 tailscale.com/util/winutil from tailscale.com/hostinfo+
|
||||
tailscale.com/version from tailscale.com/derp+
|
||||
@@ -110,7 +112,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
|
||||
|
||||
@@ -23,13 +23,14 @@ var (
|
||||
derpMapURL = flag.String("derp-map", "https://login.tailscale.com/derpmap/default", "URL to DERP map (https:// or file://)")
|
||||
listen = flag.String("listen", ":8030", "HTTP listen address")
|
||||
probeOnce = flag.Bool("once", false, "probe once and print results, then exit; ignores the listen flag")
|
||||
spread = flag.Bool("spread", true, "whether to spread probing over time")
|
||||
interval = flag.Duration("interval", 15*time.Second, "probe interval")
|
||||
)
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
p := prober.New().WithSpread(true).WithOnce(*probeOnce)
|
||||
p := prober.New().WithSpread(*spread).WithOnce(*probeOnce)
|
||||
dp, err := prober.DERP(p, *derpMapURL, *interval, *interval, *interval)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// get-authkey allocates an authkey using an OAuth API client
|
||||
// https://tailscale.com/kb/1215/oauth-clients/ and prints it
|
||||
// https://tailscale.com/s/oauth-clients and prints it
|
||||
// to stdout for scripts to capture and use.
|
||||
package main
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
"github.com/tailscale/hujson"
|
||||
"golang.org/x/oauth2/clientcredentials"
|
||||
"tailscale.com/util/httpm"
|
||||
)
|
||||
|
||||
@@ -42,9 +43,9 @@ func modifiedExternallyError() {
|
||||
}
|
||||
}
|
||||
|
||||
func apply(cache *Cache, tailnet, apiKey string) func(context.Context, []string) error {
|
||||
func apply(cache *Cache, client *http.Client, tailnet, apiKey string) func(context.Context, []string) error {
|
||||
return func(ctx context.Context, args []string) error {
|
||||
controlEtag, err := getACLETag(ctx, tailnet, apiKey)
|
||||
controlEtag, err := getACLETag(ctx, client, tailnet, apiKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -73,7 +74,7 @@ func apply(cache *Cache, tailnet, apiKey string) func(context.Context, []string)
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := applyNewACL(ctx, tailnet, apiKey, *policyFname, controlEtag); err != nil {
|
||||
if err := applyNewACL(ctx, client, tailnet, apiKey, *policyFname, controlEtag); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -83,9 +84,9 @@ func apply(cache *Cache, tailnet, apiKey string) func(context.Context, []string)
|
||||
}
|
||||
}
|
||||
|
||||
func test(cache *Cache, tailnet, apiKey string) func(context.Context, []string) error {
|
||||
func test(cache *Cache, client *http.Client, tailnet, apiKey string) func(context.Context, []string) error {
|
||||
return func(ctx context.Context, args []string) error {
|
||||
controlEtag, err := getACLETag(ctx, tailnet, apiKey)
|
||||
controlEtag, err := getACLETag(ctx, client, tailnet, apiKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -113,16 +114,16 @@ func test(cache *Cache, tailnet, apiKey string) func(context.Context, []string)
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := testNewACLs(ctx, tailnet, apiKey, *policyFname); err != nil {
|
||||
if err := testNewACLs(ctx, client, tailnet, apiKey, *policyFname); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func getChecksums(cache *Cache, tailnet, apiKey string) func(context.Context, []string) error {
|
||||
func getChecksums(cache *Cache, client *http.Client, tailnet, apiKey string) func(context.Context, []string) error {
|
||||
return func(ctx context.Context, args []string) error {
|
||||
controlEtag, err := getACLETag(ctx, tailnet, apiKey)
|
||||
controlEtag, err := getACLETag(ctx, client, tailnet, apiKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -151,8 +152,24 @@ func main() {
|
||||
log.Fatal("set envvar TS_TAILNET to your tailnet's name")
|
||||
}
|
||||
apiKey, ok := os.LookupEnv("TS_API_KEY")
|
||||
if !ok {
|
||||
log.Fatal("set envvar TS_API_KEY to your Tailscale API key")
|
||||
oauthId, oiok := os.LookupEnv("TS_OAUTH_ID")
|
||||
oauthSecret, osok := os.LookupEnv("TS_OAUTH_SECRET")
|
||||
if !ok && (!oiok || !osok) {
|
||||
log.Fatal("set envvar TS_API_KEY to your Tailscale API key or TS_OAUTH_ID and TS_OAUTH_SECRET to your Tailscale OAuth ID and Secret")
|
||||
}
|
||||
if ok && (oiok || osok) {
|
||||
log.Fatal("set either the envvar TS_API_KEY or TS_OAUTH_ID and TS_OAUTH_SECRET")
|
||||
}
|
||||
var client *http.Client
|
||||
if oiok {
|
||||
oauthConfig := &clientcredentials.Config{
|
||||
ClientID: oauthId,
|
||||
ClientSecret: oauthSecret,
|
||||
TokenURL: fmt.Sprintf("https://%s/api/v2/oauth/token", *apiServer),
|
||||
}
|
||||
client = oauthConfig.Client(context.Background())
|
||||
} else {
|
||||
client = http.DefaultClient
|
||||
}
|
||||
cache, err := LoadCache(*cacheFname)
|
||||
if err != nil {
|
||||
@@ -169,7 +186,7 @@ func main() {
|
||||
ShortUsage: "gitops-pusher [options] apply",
|
||||
ShortHelp: "Pushes changes to CONTROL",
|
||||
LongHelp: `Pushes changes to CONTROL`,
|
||||
Exec: apply(cache, tailnet, apiKey),
|
||||
Exec: apply(cache, client, tailnet, apiKey),
|
||||
}
|
||||
|
||||
testCmd := &ffcli.Command{
|
||||
@@ -177,7 +194,7 @@ func main() {
|
||||
ShortUsage: "gitops-pusher [options] test",
|
||||
ShortHelp: "Tests ACL changes",
|
||||
LongHelp: "Tests ACL changes",
|
||||
Exec: test(cache, tailnet, apiKey),
|
||||
Exec: test(cache, client, tailnet, apiKey),
|
||||
}
|
||||
|
||||
cksumCmd := &ffcli.Command{
|
||||
@@ -185,7 +202,7 @@ func main() {
|
||||
ShortUsage: "Shows checksums of ACL files",
|
||||
ShortHelp: "Fetch checksum of CONTROL's ACL and the local ACL for comparison",
|
||||
LongHelp: "Fetch checksum of CONTROL's ACL and the local ACL for comparison",
|
||||
Exec: getChecksums(cache, tailnet, apiKey),
|
||||
Exec: getChecksums(cache, client, tailnet, apiKey),
|
||||
}
|
||||
|
||||
root := &ffcli.Command{
|
||||
@@ -228,7 +245,7 @@ func sumFile(fname string) (string, error) {
|
||||
return fmt.Sprintf("%x", h.Sum(nil)), nil
|
||||
}
|
||||
|
||||
func applyNewACL(ctx context.Context, tailnet, apiKey, policyFname, oldEtag string) error {
|
||||
func applyNewACL(ctx context.Context, client *http.Client, tailnet, apiKey, policyFname, oldEtag string) error {
|
||||
fin, err := os.Open(policyFname)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -244,7 +261,7 @@ func applyNewACL(ctx context.Context, tailnet, apiKey, policyFname, oldEtag stri
|
||||
req.Header.Set("Content-Type", "application/hujson")
|
||||
req.Header.Set("If-Match", `"`+oldEtag+`"`)
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -265,7 +282,7 @@ func applyNewACL(ctx context.Context, tailnet, apiKey, policyFname, oldEtag stri
|
||||
return nil
|
||||
}
|
||||
|
||||
func testNewACLs(ctx context.Context, tailnet, apiKey, policyFname string) error {
|
||||
func testNewACLs(ctx context.Context, client *http.Client, tailnet, apiKey, policyFname string) error {
|
||||
data, err := os.ReadFile(policyFname)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -283,7 +300,7 @@ func testNewACLs(ctx context.Context, tailnet, apiKey, policyFname string) error
|
||||
req.SetBasicAuth(apiKey, "")
|
||||
req.Header.Set("Content-Type", "application/hujson")
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -346,7 +363,7 @@ type ACLTestErrorDetail struct {
|
||||
Errors []string `json:"errors"`
|
||||
}
|
||||
|
||||
func getACLETag(ctx context.Context, tailnet, apiKey string) (string, error) {
|
||||
func getACLETag(ctx context.Context, client *http.Client, tailnet, apiKey string) (string, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, httpm.GET, fmt.Sprintf("https://%s/api/v2/tailnet/%s/acl", *apiServer, tailnet), nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -355,7 +372,7 @@ func getACLETag(ctx context.Context, tailnet, apiKey string) (string, error) {
|
||||
req.SetBasicAuth(apiKey, "")
|
||||
req.Header.Set("Accept", "application/hujson")
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ metadata:
|
||||
name: tailscale-auth-proxy
|
||||
rules:
|
||||
- apiGroups: [""]
|
||||
resources: ["users"]
|
||||
resources: ["users", "groups"]
|
||||
verbs: ["impersonate"]
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
|
||||
@@ -7,8 +7,10 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -25,7 +27,7 @@ import (
|
||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||
"k8s.io/apimachinery/pkg/fields"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/transport"
|
||||
"sigs.k8s.io/controller-runtime/pkg/builder"
|
||||
"sigs.k8s.io/controller-runtime/pkg/cache"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
@@ -99,9 +101,9 @@ func main() {
|
||||
tsClient.HTTPClient = credentials.Client(context.Background())
|
||||
|
||||
if shouldRunAuthProxy {
|
||||
hostinfo.SetPackage("k8s-operator-proxy")
|
||||
hostinfo.SetApp("k8s-operator-proxy")
|
||||
} else {
|
||||
hostinfo.SetPackage("k8s-operator")
|
||||
hostinfo.SetApp("k8s-operator")
|
||||
}
|
||||
|
||||
s := &tsnet.Server{
|
||||
@@ -166,7 +168,7 @@ waitOnline:
|
||||
loginDone = true
|
||||
case "NeedsMachineAuth":
|
||||
if !machineAuthShown {
|
||||
startlog.Infof("Machine authorization required, please visit the admin panel to authorize")
|
||||
startlog.Infof("Machine approval required, please visit the admin panel to approve")
|
||||
machineAuthShown = true
|
||||
}
|
||||
default:
|
||||
@@ -235,15 +237,25 @@ waitOnline:
|
||||
|
||||
startlog.Infof("Startup complete, operator running")
|
||||
if shouldRunAuthProxy {
|
||||
rc, err := rest.TransportFor(restConfig)
|
||||
cfg, err := restConfig.TransportConfig()
|
||||
if err != nil {
|
||||
startlog.Fatalf("could not get rest transport: %v", err)
|
||||
startlog.Fatalf("could not get rest.TransportConfig(): %v", err)
|
||||
}
|
||||
authProxyListener, err := s.Listen("tcp", ":443")
|
||||
|
||||
// Kubernetes uses SPDY for exec and port-forward, however SPDY is
|
||||
// incompatible with HTTP/2; so disable HTTP/2 in the proxy.
|
||||
tr := http.DefaultTransport.(*http.Transport).Clone()
|
||||
tr.TLSClientConfig, err = transport.TLSConfigFor(cfg)
|
||||
if err != nil {
|
||||
startlog.Fatalf("could not listen on :443: %v", err)
|
||||
startlog.Fatalf("could not get transport.TLSConfigFor(): %v", err)
|
||||
}
|
||||
go runAuthProxy(lc, authProxyListener, rc, zlog.Named("auth-proxy").Infof)
|
||||
tr.TLSNextProto = make(map[string]func(authority string, c *tls.Conn) http.RoundTripper)
|
||||
|
||||
rt, err := transport.HTTPWrappersForConfig(cfg, tr)
|
||||
if err != nil {
|
||||
startlog.Fatalf("could not get rest.TransportConfig(): %v", err)
|
||||
}
|
||||
go runAuthProxy(s, rt, zlog.Named("auth-proxy").Infof)
|
||||
}
|
||||
if err := mgr.Start(signals.SetupSignalHandler()); err != nil {
|
||||
startlog.Fatalf("could not start manager: %v", err)
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
@@ -17,6 +16,7 @@ import (
|
||||
|
||||
"tailscale.com/client/tailscale"
|
||||
"tailscale.com/client/tailscale/apitype"
|
||||
"tailscale.com/tsnet"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
@@ -41,23 +41,42 @@ func (h *authProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
h.rp.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
func runAuthProxy(lc *tailscale.LocalClient, ls net.Listener, rt http.RoundTripper, logf logger.Logf) {
|
||||
// runAuthProxy runs an HTTP server that authenticates requests using the
|
||||
// Tailscale LocalAPI and then proxies them to the Kubernetes API.
|
||||
// It listens on :443 and uses the Tailscale HTTPS certificate.
|
||||
// s will be started if it is not already running.
|
||||
// rt is used to proxy requests to the Kubernetes API.
|
||||
//
|
||||
// It never returns.
|
||||
func runAuthProxy(s *tsnet.Server, rt http.RoundTripper, logf logger.Logf) {
|
||||
ln, err := s.Listen("tcp", ":443")
|
||||
if err != nil {
|
||||
log.Fatalf("could not listen on :443: %v", err)
|
||||
}
|
||||
u, err := url.Parse(fmt.Sprintf("https://%s:%s", os.Getenv("KUBERNETES_SERVICE_HOST"), os.Getenv("KUBERNETES_SERVICE_PORT_HTTPS")))
|
||||
if err != nil {
|
||||
log.Fatalf("runAuthProxy: failed to parse URL %v", err)
|
||||
}
|
||||
|
||||
lc, err := s.LocalClient()
|
||||
if err != nil {
|
||||
log.Fatalf("could not get local client: %v", err)
|
||||
}
|
||||
ap := &authProxy{
|
||||
logf: logf,
|
||||
lc: lc,
|
||||
rp: &httputil.ReverseProxy{
|
||||
Director: func(r *http.Request) {
|
||||
// Replace the request with the user's identity.
|
||||
who := r.Context().Value(whoIsKey{}).(*apitype.WhoIsResponse)
|
||||
r.Header.Set("Impersonate-User", who.UserProfile.LoginName)
|
||||
// We want to proxy to the Kubernetes API, but we want to use
|
||||
// the caller's identity to do so. We do this by impersonating
|
||||
// the caller using the Kubernetes User Impersonation feature:
|
||||
// https://kubernetes.io/docs/reference/access-authn-authz/authentication/#user-impersonation
|
||||
|
||||
// Remove all authentication headers.
|
||||
// Out of paranoia, remove all authentication headers that might
|
||||
// have been set by the client.
|
||||
r.Header.Del("Authorization")
|
||||
r.Header.Del("Impersonate-Group")
|
||||
r.Header.Del("Impersonate-User")
|
||||
r.Header.Del("Impersonate-Uid")
|
||||
for k := range r.Header {
|
||||
if strings.HasPrefix(k, "Impersonate-Extra-") {
|
||||
@@ -65,6 +84,19 @@ func runAuthProxy(lc *tailscale.LocalClient, ls net.Listener, rt http.RoundTripp
|
||||
}
|
||||
}
|
||||
|
||||
// Now add the impersonation headers that we want.
|
||||
who := r.Context().Value(whoIsKey{}).(*apitype.WhoIsResponse)
|
||||
if who.Node.IsTagged() {
|
||||
// Use the nodes FQDN as the username, and the nodes tags as the groups.
|
||||
// "Impersonate-Group" requires "Impersonate-User" to be set.
|
||||
r.Header.Set("Impersonate-User", strings.TrimSuffix(who.Node.Name, "."))
|
||||
for _, tag := range who.Node.Tags {
|
||||
r.Header.Add("Impersonate-Group", tag)
|
||||
}
|
||||
} else {
|
||||
r.Header.Set("Impersonate-User", who.UserProfile.LoginName)
|
||||
}
|
||||
|
||||
// Replace the URL with the Kubernetes APIServer.
|
||||
r.URL.Scheme = u.Scheme
|
||||
r.URL.Host = u.Host
|
||||
@@ -72,9 +104,17 @@ func runAuthProxy(lc *tailscale.LocalClient, ls net.Listener, rt http.RoundTripp
|
||||
Transport: rt,
|
||||
},
|
||||
}
|
||||
if err := http.Serve(tls.NewListener(ls, &tls.Config{
|
||||
GetCertificate: lc.GetCertificate,
|
||||
}), ap); err != nil {
|
||||
hs := &http.Server{
|
||||
// Kubernetes uses SPDY for exec and port-forward, however SPDY is
|
||||
// incompatible with HTTP/2; so disable HTTP/2 in the proxy.
|
||||
TLSConfig: &tls.Config{
|
||||
GetCertificate: lc.GetCertificate,
|
||||
NextProtos: []string{"http/1.1"},
|
||||
},
|
||||
TLSNextProto: make(map[string]func(*http.Server, *tls.Conn, http.Handler)),
|
||||
Handler: ap,
|
||||
}
|
||||
if err := hs.ServeTLS(ln, "", ""); err != nil {
|
||||
log.Fatalf("runAuthProxy: failed to serve %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ func main() {
|
||||
|
||||
arch := winres.Arch(os.Args[1])
|
||||
switch arch {
|
||||
case winres.ArchAMD64, winres.ArchARM64, winres.ArchI386, winres.ArchARM:
|
||||
case winres.ArchAMD64, winres.ArchARM64, winres.ArchI386:
|
||||
default:
|
||||
log.Fatalf("unsupported arch: %s", arch)
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ import (
|
||||
jsonv2 "github.com/go-json-experiment/json"
|
||||
"golang.org/x/exp/maps"
|
||||
"golang.org/x/exp/slices"
|
||||
"tailscale.com/logtail"
|
||||
"tailscale.com/types/logid"
|
||||
"tailscale.com/types/netlogtype"
|
||||
"tailscale.com/util/must"
|
||||
)
|
||||
@@ -136,8 +136,8 @@ func processObject(dec *jsonv2.Decoder) {
|
||||
|
||||
type message struct {
|
||||
Logtail struct {
|
||||
ID logtail.PublicID `json:"id"`
|
||||
Logged time.Time `json:"server_time"`
|
||||
ID logid.PublicID `json:"id"`
|
||||
Logged time.Time `json:"server_time"`
|
||||
} `json:"logtail"`
|
||||
Logged time.Time `json:"logged"`
|
||||
netlogtype.Message
|
||||
|
||||
@@ -56,7 +56,7 @@ func main() {
|
||||
return
|
||||
}
|
||||
|
||||
if len(info.Node.Tags) != 0 {
|
||||
if info.Node.IsTagged() {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
log.Printf("node %s is tagged", info.Node.Hostinfo.Hostname())
|
||||
return
|
||||
|
||||
@@ -147,7 +147,7 @@ func getTailscaleUser(ctx context.Context, localClient *tailscale.LocalClient, i
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to identify remote host: %w", err)
|
||||
}
|
||||
if len(whois.Node.Tags) != 0 {
|
||||
if whois.Node.IsTagged() {
|
||||
return nil, fmt.Errorf("tagged nodes are not users")
|
||||
}
|
||||
if whois.UserProfile == nil || whois.UserProfile.LoginName == "" {
|
||||
|
||||
219
cmd/sniproxy/snipproxy.go
Normal file
219
cmd/sniproxy/snipproxy.go
Normal file
@@ -0,0 +1,219 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// The sniproxy is an outbound SNI proxy. It receives TLS connections over
|
||||
// Tailscale on one or more TCP ports and sends them out to the same SNI
|
||||
// hostname & port on the internet. It only does TCP.
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/net/dns/dnsmessage"
|
||||
"inet.af/tcpproxy"
|
||||
"tailscale.com/client/tailscale"
|
||||
"tailscale.com/net/netutil"
|
||||
"tailscale.com/tsnet"
|
||||
"tailscale.com/types/nettype"
|
||||
)
|
||||
|
||||
var (
|
||||
ports = flag.String("ports", "443", "comma-separated list of ports to proxy")
|
||||
promoteHTTPS = flag.Bool("promote-https", true, "promote HTTP to HTTPS")
|
||||
)
|
||||
|
||||
var tsMBox = dnsmessage.MustNewName("support.tailscale.com.")
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
if *ports == "" {
|
||||
log.Fatal("no ports")
|
||||
}
|
||||
|
||||
var s server
|
||||
defer s.ts.Close()
|
||||
|
||||
lc, err := s.ts.LocalClient()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
s.lc = lc
|
||||
|
||||
for _, portStr := range strings.Split(*ports, ",") {
|
||||
ln, err := s.ts.Listen("tcp", ":"+portStr)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
log.Printf("Serving on port %v ...", portStr)
|
||||
go s.serve(ln)
|
||||
}
|
||||
|
||||
ln, err := s.ts.Listen("udp", ":53")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
go s.serveDNS(ln)
|
||||
|
||||
if *promoteHTTPS {
|
||||
ln, err := s.ts.Listen("tcp", ":80")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
log.Printf("Promoting HTTP to HTTPS ...")
|
||||
go s.promoteHTTPS(ln)
|
||||
}
|
||||
|
||||
select {}
|
||||
}
|
||||
|
||||
type server struct {
|
||||
ts tsnet.Server
|
||||
lc *tailscale.LocalClient
|
||||
}
|
||||
|
||||
func (s *server) serve(ln net.Listener) {
|
||||
for {
|
||||
c, err := ln.Accept()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
go s.serveConn(c)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *server) serveDNS(ln net.Listener) {
|
||||
for {
|
||||
c, err := ln.Accept()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
go s.serveDNSConn(c.(nettype.ConnPacketConn))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *server) serveDNSConn(c nettype.ConnPacketConn) {
|
||||
defer c.Close()
|
||||
c.SetReadDeadline(time.Now().Add(5 * time.Second))
|
||||
buf := make([]byte, 1500)
|
||||
n, err := c.Read(buf)
|
||||
if err != nil {
|
||||
log.Printf("c.Read failed: %v\n ", err)
|
||||
return
|
||||
}
|
||||
|
||||
var msg dnsmessage.Message
|
||||
err = msg.Unpack(buf[:n])
|
||||
if err != nil {
|
||||
log.Printf("dnsmessage unpack failed: %v\n ", err)
|
||||
return
|
||||
}
|
||||
|
||||
buf, err = s.dnsResponse(&msg)
|
||||
if err != nil {
|
||||
log.Printf("s.dnsResponse failed: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = c.Write(buf)
|
||||
if err != nil {
|
||||
log.Printf("c.Write failed: %v\n", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (s *server) serveConn(c net.Conn) {
|
||||
addrPortStr := c.LocalAddr().String()
|
||||
_, port, err := net.SplitHostPort(addrPortStr)
|
||||
if err != nil {
|
||||
log.Printf("bogus addrPort %q", addrPortStr)
|
||||
c.Close()
|
||||
return
|
||||
}
|
||||
|
||||
var dialer net.Dialer
|
||||
dialer.Timeout = 5 * time.Second
|
||||
|
||||
var p tcpproxy.Proxy
|
||||
p.ListenFunc = func(net, laddr string) (net.Listener, error) {
|
||||
return netutil.NewOneConnListener(c, nil), nil
|
||||
}
|
||||
p.AddSNIRouteFunc(addrPortStr, func(ctx context.Context, sniName string) (t tcpproxy.Target, ok bool) {
|
||||
return &tcpproxy.DialProxy{
|
||||
Addr: net.JoinHostPort(sniName, port),
|
||||
DialContext: dialer.DialContext,
|
||||
}, true
|
||||
})
|
||||
p.Start()
|
||||
}
|
||||
|
||||
func (s *server) dnsResponse(req *dnsmessage.Message) (buf []byte, err error) {
|
||||
resp := dnsmessage.NewBuilder(buf,
|
||||
dnsmessage.Header{
|
||||
ID: req.Header.ID,
|
||||
Response: true,
|
||||
Authoritative: true,
|
||||
})
|
||||
resp.EnableCompression()
|
||||
|
||||
if len(req.Questions) == 0 {
|
||||
buf, _ = resp.Finish()
|
||||
return
|
||||
}
|
||||
|
||||
q := req.Questions[0]
|
||||
err = resp.StartQuestions()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
resp.Question(q)
|
||||
|
||||
ip4, ip6 := s.ts.TailscaleIPs()
|
||||
err = resp.StartAnswers()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
switch q.Type {
|
||||
case dnsmessage.TypeAAAA:
|
||||
err = resp.AAAAResource(
|
||||
dnsmessage.ResourceHeader{Name: q.Name, Class: q.Class, TTL: 120},
|
||||
dnsmessage.AAAAResource{AAAA: ip6.As16()},
|
||||
)
|
||||
|
||||
case dnsmessage.TypeA:
|
||||
err = resp.AResource(
|
||||
dnsmessage.ResourceHeader{Name: q.Name, Class: q.Class, TTL: 120},
|
||||
dnsmessage.AResource{A: ip4.As4()},
|
||||
)
|
||||
case dnsmessage.TypeSOA:
|
||||
err = resp.SOAResource(
|
||||
dnsmessage.ResourceHeader{Name: q.Name, Class: q.Class, TTL: 120},
|
||||
dnsmessage.SOAResource{NS: q.Name, MBox: tsMBox, Serial: 2023030600,
|
||||
Refresh: 120, Retry: 120, Expire: 120, MinTTL: 60},
|
||||
)
|
||||
case dnsmessage.TypeNS:
|
||||
err = resp.NSResource(
|
||||
dnsmessage.ResourceHeader{Name: q.Name, Class: q.Class, TTL: 120},
|
||||
dnsmessage.NSResource{NS: tsMBox},
|
||||
)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return resp.Finish()
|
||||
}
|
||||
|
||||
func (s *server) promoteHTTPS(ln net.Listener) {
|
||||
err := http.Serve(ln, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, "https://"+r.Host+r.RequestURI, http.StatusFound)
|
||||
}))
|
||||
log.Fatalf("promoteHTTPS http.Serve: %v", err)
|
||||
}
|
||||
@@ -113,12 +113,15 @@ change in the future.
|
||||
loginCmd,
|
||||
logoutCmd,
|
||||
switchCmd,
|
||||
configureCmd,
|
||||
netcheckCmd,
|
||||
ipCmd,
|
||||
statusCmd,
|
||||
pingCmd,
|
||||
ncCmd,
|
||||
sshCmd,
|
||||
funnelCmd,
|
||||
serveCmd,
|
||||
versionCmd,
|
||||
webCmd,
|
||||
fileCmd,
|
||||
@@ -146,12 +149,8 @@ change in the future.
|
||||
switch {
|
||||
case slices.Contains(args, "debug"):
|
||||
rootCmd.Subcommands = append(rootCmd.Subcommands, debugCmd)
|
||||
case slices.Contains(args, "serve"):
|
||||
rootCmd.Subcommands = append(rootCmd.Subcommands, serveCmd)
|
||||
case slices.Contains(args, "update"):
|
||||
rootCmd.Subcommands = append(rootCmd.Subcommands, updateCmd)
|
||||
case slices.Contains(args, "configure"):
|
||||
rootCmd.Subcommands = append(rootCmd.Subcommands, configureCmd)
|
||||
}
|
||||
if runtime.GOOS == "linux" && distro.Get() == distro.Synology {
|
||||
rootCmd.Subcommands = append(rootCmd.Subcommands, configureHostCmd)
|
||||
|
||||
@@ -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",
|
||||
@@ -1071,20 +1078,42 @@ func TestUpdatePrefs(t *testing.T) {
|
||||
},
|
||||
env: upCheckEnv{backendState: "Running"},
|
||||
},
|
||||
{
|
||||
name: "force_reauth_over_ssh_no_risk",
|
||||
flags: []string{"--force-reauth"},
|
||||
sshOverTailscale: true,
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: "https://login.tailscale.com",
|
||||
AllowSingleHosts: true,
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
},
|
||||
env: upCheckEnv{backendState: "Running"},
|
||||
wantErrSubtr: "aborted, no changes made",
|
||||
},
|
||||
{
|
||||
name: "force_reauth_over_ssh",
|
||||
flags: []string{"--force-reauth", "--accept-risk=lose-ssh"},
|
||||
sshOverTailscale: true,
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: "https://login.tailscale.com",
|
||||
AllowSingleHosts: true,
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
},
|
||||
wantJustEditMP: nil,
|
||||
env: upCheckEnv{backendState: "Running"},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if tt.sshOverTailscale {
|
||||
old := getSSHClientEnvVar
|
||||
getSSHClientEnvVar = func() string { return "100.100.100.100 1 1" }
|
||||
t.Cleanup(func() { getSSHClientEnvVar = old })
|
||||
tstest.Replace(t, &getSSHClientEnvVar, func() string { return "100.100.100.100 1 1" })
|
||||
} else if isSSHOverTailscale() {
|
||||
// The test is being executed over a "real" tailscale SSH
|
||||
// session, but sshOverTailscale is unset. Make the test appear
|
||||
// as if it's not over tailscale SSH.
|
||||
old := getSSHClientEnvVar
|
||||
getSSHClientEnvVar = func() string { return "" }
|
||||
t.Cleanup(func() { getSSHClientEnvVar = old })
|
||||
tstest.Replace(t, &getSSHClientEnvVar, func() string { return "" })
|
||||
}
|
||||
if tt.env.goos == "" {
|
||||
tt.env.goos = "linux"
|
||||
|
||||
@@ -26,12 +26,14 @@ func init() {
|
||||
|
||||
var configureKubeconfigCmd = &ffcli.Command{
|
||||
Name: "kubeconfig",
|
||||
ShortHelp: "Configure kubeconfig to use Tailscale",
|
||||
ShortHelp: "[ALPHA] Connect to a Kubernetes cluster using a Tailscale Auth Proxy",
|
||||
ShortUsage: "kubeconfig <hostname-or-fqdn>",
|
||||
LongHelp: strings.TrimSpace(`
|
||||
Run this command to configure your kubeconfig to use Tailscale for authentication to a Kubernetes cluster.
|
||||
Run this command to configure kubectl to connect to a Kubernetes cluster over Tailscale.
|
||||
|
||||
The hostname argument should be set to the Tailscale hostname of the peer running as an auth proxy in the cluster.
|
||||
|
||||
See: https://tailscale.com/s/k8s-auth-proxy
|
||||
`),
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
fs := newFlagSet("kubeconfig")
|
||||
|
||||
@@ -35,13 +35,13 @@ var configureHostCmd = &ffcli.Command{
|
||||
var synologyConfigureCmd = &ffcli.Command{
|
||||
Name: "synology",
|
||||
Exec: runConfigureSynology,
|
||||
ShortHelp: "Configure Synology to enable more Tailscale features",
|
||||
ShortHelp: "Configure Synology to enable outbound connections",
|
||||
LongHelp: strings.TrimSpace(`
|
||||
The 'configure-host' command is intended to run at boot as root
|
||||
to create the /dev/net/tun device and give the tailscaled binary
|
||||
permission to use it.
|
||||
This command is intended to run at boot as root on a Synology device to
|
||||
create the /dev/net/tun device and give the tailscaled binary permission
|
||||
to use it.
|
||||
|
||||
See: https://tailscale.com/kb/1152/synology-outbound/
|
||||
See: https://tailscale.com/s/synology-outbound
|
||||
`),
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
fs := newFlagSet("synology")
|
||||
|
||||
@@ -15,10 +15,10 @@ import (
|
||||
|
||||
var configureCmd = &ffcli.Command{
|
||||
Name: "configure",
|
||||
ShortHelp: "Configure the host to enable more Tailscale features",
|
||||
ShortHelp: "[ALPHA] Configure the host to enable more Tailscale features",
|
||||
LongHelp: strings.TrimSpace(`
|
||||
The 'configure' command is intended to provide a way to configure different
|
||||
services on the host to enable more Tailscale features.
|
||||
The 'configure' set of commands are intended to provide a way to enable different
|
||||
services on the host to use Tailscale in more ways.
|
||||
`),
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
fs := newFlagSet("configure")
|
||||
|
||||
@@ -201,6 +201,23 @@ var debugCmd = &ffcli.Command{
|
||||
return fs
|
||||
})(),
|
||||
},
|
||||
{
|
||||
Name: "portmap",
|
||||
Exec: debugPortmap,
|
||||
ShortHelp: "run portmap debugging debugging",
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
fs := newFlagSet("portmap")
|
||||
fs.DurationVar(&debugPortmapArgs.duration, "duration", 5*time.Second, "timeout for port mapping")
|
||||
fs.StringVar(&debugPortmapArgs.ty, "type", "", `portmap debug type (one of "", "pmp", "pcp", or "upnp")`)
|
||||
fs.StringVar(&debugPortmapArgs.gwSelf, "gw-self", "", `override gateway and self IP (format: "gatewayIP/selfIP")`)
|
||||
return fs
|
||||
})(),
|
||||
},
|
||||
{
|
||||
Name: "peer-endpoint-changes",
|
||||
Exec: runPeerEndpointChanges,
|
||||
ShortHelp: "prints debug information about a peer's endpoint changes",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -789,3 +806,82 @@ func runCapture(ctx context.Context, args []string) error {
|
||||
_, err = io.Copy(f, stream)
|
||||
return err
|
||||
}
|
||||
|
||||
var debugPortmapArgs struct {
|
||||
duration time.Duration
|
||||
gwSelf string
|
||||
ty string
|
||||
}
|
||||
|
||||
func debugPortmap(ctx context.Context, args []string) error {
|
||||
rc, err := localClient.DebugPortmap(ctx,
|
||||
debugPortmapArgs.duration,
|
||||
debugPortmapArgs.ty,
|
||||
debugPortmapArgs.gwSelf,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rc.Close()
|
||||
|
||||
_, err = io.Copy(os.Stdout, rc)
|
||||
return err
|
||||
}
|
||||
|
||||
func runPeerEndpointChanges(ctx context.Context, args []string) error {
|
||||
st, err := localClient.Status(ctx)
|
||||
if err != nil {
|
||||
return fixTailscaledConnectError(err)
|
||||
}
|
||||
description, ok := isRunningOrStarting(st)
|
||||
if !ok {
|
||||
printf("%s\n", description)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if len(args) != 1 || args[0] == "" {
|
||||
return errors.New("usage: peer-status <hostname-or-IP>")
|
||||
}
|
||||
var ip string
|
||||
|
||||
hostOrIP := args[0]
|
||||
ip, self, err := tailscaleIPFromArg(ctx, hostOrIP)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if self {
|
||||
printf("%v is local Tailscale IP\n", ip)
|
||||
return nil
|
||||
}
|
||||
|
||||
if ip != hostOrIP {
|
||||
log.Printf("lookup %q => %q", hostOrIP, ip)
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", "http://local-tailscaled.sock/localapi/v0/debug-peer-endpoint-changes?ip="+ip, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resp, err := localClient.DoLocalRequest(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var dst bytes.Buffer
|
||||
if err := json.Indent(&dst, body, "", " "); err != nil {
|
||||
return fmt.Errorf("indenting returned JSON: %w", err)
|
||||
}
|
||||
|
||||
if ss := dst.String(); !strings.HasSuffix(ss, "\n") {
|
||||
dst.WriteByte('\n')
|
||||
}
|
||||
fmt.Printf("%s", dst.String())
|
||||
return nil
|
||||
}
|
||||
|
||||
138
cmd/tailscale/cli/funnel.go
Normal file
138
cmd/tailscale/cli/funnel.go
Normal file
@@ -0,0 +1,138 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/util/mak"
|
||||
)
|
||||
|
||||
var funnelCmd = newFunnelCommand(&serveEnv{lc: &localClient})
|
||||
|
||||
// newFunnelCommand returns a new "funnel" subcommand using e as its environment.
|
||||
// The funnel subcommand is used to turn on/off the Funnel service.
|
||||
// Funnel is off by default.
|
||||
// Funnel allows you to publish a 'tailscale serve' server publicly, open to the
|
||||
// entire internet.
|
||||
// newFunnelCommand shares the same serveEnv as the "serve" subcommand. See
|
||||
// newServeCommand and serve.go for more details.
|
||||
func newFunnelCommand(e *serveEnv) *ffcli.Command {
|
||||
return &ffcli.Command{
|
||||
Name: "funnel",
|
||||
ShortHelp: "Turn on/off Funnel service",
|
||||
ShortUsage: strings.TrimSpace(`
|
||||
funnel <serve-port> {on|off}
|
||||
funnel status [--json]
|
||||
`),
|
||||
LongHelp: strings.Join([]string{
|
||||
"Funnel allows you to publish a 'tailscale serve'",
|
||||
"server publicly, open to the entire internet.",
|
||||
"",
|
||||
"Turning off Funnel only turns off serving to the internet.",
|
||||
"It does not affect serving to your tailnet.",
|
||||
}, "\n"),
|
||||
Exec: e.runFunnel,
|
||||
UsageFunc: usageFunc,
|
||||
Subcommands: []*ffcli.Command{
|
||||
{
|
||||
Name: "status",
|
||||
Exec: e.runServeStatus,
|
||||
ShortHelp: "show current serve/funnel status",
|
||||
FlagSet: e.newFlags("funnel-status", func(fs *flag.FlagSet) {
|
||||
fs.BoolVar(&e.json, "json", false, "output JSON")
|
||||
}),
|
||||
UsageFunc: usageFunc,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// runFunnel is the entry point for the "tailscale funnel" subcommand and
|
||||
// manages turning on/off funnel. Funnel is off by default.
|
||||
//
|
||||
// Note: funnel is only supported on single DNS name for now. (2022-11-15)
|
||||
func (e *serveEnv) runFunnel(ctx context.Context, args []string) error {
|
||||
if len(args) != 2 {
|
||||
return flag.ErrHelp
|
||||
}
|
||||
|
||||
var on bool
|
||||
switch args[1] {
|
||||
case "on", "off":
|
||||
on = args[1] == "on"
|
||||
default:
|
||||
return flag.ErrHelp
|
||||
}
|
||||
sc, err := e.lc.GetServeConfig(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if sc == nil {
|
||||
sc = new(ipn.ServeConfig)
|
||||
}
|
||||
st, err := e.getLocalClientStatus(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting client status: %w", err)
|
||||
}
|
||||
|
||||
port64, err := strconv.ParseUint(args[0], 10, 16)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
port := uint16(port64)
|
||||
|
||||
if err := ipn.CheckFunnelAccess(port, st.Self.Capabilities); err != nil {
|
||||
return err
|
||||
}
|
||||
dnsName := strings.TrimSuffix(st.Self.DNSName, ".")
|
||||
hp := ipn.HostPort(dnsName + ":" + strconv.Itoa(int(port)))
|
||||
if on == sc.AllowFunnel[hp] {
|
||||
printFunnelWarning(sc)
|
||||
// Nothing to do.
|
||||
return nil
|
||||
}
|
||||
if on {
|
||||
mak.Set(&sc.AllowFunnel, hp, true)
|
||||
} else {
|
||||
delete(sc.AllowFunnel, hp)
|
||||
// clear map mostly for testing
|
||||
if len(sc.AllowFunnel) == 0 {
|
||||
sc.AllowFunnel = nil
|
||||
}
|
||||
}
|
||||
if err := e.lc.SetServeConfig(ctx, sc); err != nil {
|
||||
return err
|
||||
}
|
||||
printFunnelWarning(sc)
|
||||
return nil
|
||||
}
|
||||
|
||||
// printFunnelWarning prints a warning if the Funnel is on but there is no serve
|
||||
// config for its host:port.
|
||||
func printFunnelWarning(sc *ipn.ServeConfig) {
|
||||
var warn bool
|
||||
for hp, a := range sc.AllowFunnel {
|
||||
if !a {
|
||||
continue
|
||||
}
|
||||
_, portStr, _ := net.SplitHostPort(string(hp))
|
||||
p, _ := strconv.ParseUint(portStr, 10, 16)
|
||||
if _, ok := sc.TCP[uint16(p)]; !ok {
|
||||
warn = true
|
||||
fmt.Fprintf(os.Stderr, "Warning: funnel=on for %s, but no serve config\n", hp)
|
||||
}
|
||||
}
|
||||
if warn {
|
||||
fmt.Fprintf(os.Stderr, " run: `tailscale serve --help` to see how to configure handlers\n")
|
||||
}
|
||||
}
|
||||
@@ -47,7 +47,7 @@ var netcheckArgs struct {
|
||||
func runNetcheck(ctx context.Context, args []string) error {
|
||||
c := &netcheck.Client{
|
||||
UDPBindAddr: envknob.String("TS_DEBUG_NETCHECK_UDP_BIND"),
|
||||
PortMapper: portmapper.NewClient(logger.WithPrefix(log.Printf, "portmap: "), nil),
|
||||
PortMapper: portmapper.NewClient(logger.WithPrefix(log.Printf, "portmap: "), nil, nil),
|
||||
}
|
||||
if netcheckArgs.verbose {
|
||||
c.Logf = logger.WithPrefix(log.Printf, "netcheck: ")
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/mattn/go-colorable"
|
||||
"github.com/mattn/go-isatty"
|
||||
@@ -40,7 +41,16 @@ var netlockCmd = &ffcli.Command{
|
||||
nlLogCmd,
|
||||
nlLocalDisableCmd,
|
||||
},
|
||||
Exec: runNetworkLockStatus,
|
||||
Exec: runNetworkLockNoSubcommand,
|
||||
}
|
||||
|
||||
func runNetworkLockNoSubcommand(ctx context.Context, args []string) error {
|
||||
// Detect & handle the deprecated command 'lock tskey-wrap'.
|
||||
if len(args) >= 2 && args[0] == "tskey-wrap" {
|
||||
return runTskeyWrapCmd(ctx, args[1:])
|
||||
}
|
||||
|
||||
return runNetworkLockStatus(ctx, args)
|
||||
}
|
||||
|
||||
var nlInitArgs struct {
|
||||
@@ -230,6 +240,15 @@ func runNetworkLockStatus(ctx context.Context, args []string) error {
|
||||
if k.Key == st.PublicKey {
|
||||
line.WriteString("(self)")
|
||||
}
|
||||
if k.Metadata["purpose"] == "pre-auth key" {
|
||||
if preauthKeyID := k.Metadata["authkey_stableid"]; preauthKeyID != "" {
|
||||
line.WriteString("(pre-auth key ")
|
||||
line.WriteString(preauthKeyID)
|
||||
line.WriteString(")")
|
||||
} else {
|
||||
line.WriteString("(pre-auth key)")
|
||||
}
|
||||
}
|
||||
fmt.Println(line.String())
|
||||
}
|
||||
}
|
||||
@@ -245,11 +264,13 @@ func runNetworkLockStatus(ctx context.Context, args []string) error {
|
||||
for i, addr := range p.TailscaleIPs {
|
||||
line.WriteString(addr.String())
|
||||
if i < len(p.TailscaleIPs)-1 {
|
||||
line.WriteString(", ")
|
||||
line.WriteString(",")
|
||||
}
|
||||
}
|
||||
line.WriteString("\t")
|
||||
line.WriteString(string(p.StableID))
|
||||
line.WriteString("\t")
|
||||
line.WriteString(p.NodeKey.String())
|
||||
fmt.Println(line.String())
|
||||
}
|
||||
}
|
||||
@@ -267,14 +288,78 @@ var nlAddCmd = &ffcli.Command{
|
||||
},
|
||||
}
|
||||
|
||||
var nlRemoveArgs struct {
|
||||
resign bool
|
||||
}
|
||||
|
||||
var nlRemoveCmd = &ffcli.Command{
|
||||
Name: "remove",
|
||||
ShortUsage: "remove <public-key>...",
|
||||
ShortUsage: "remove [--re-sign=false] <public-key>...",
|
||||
ShortHelp: "Removes one or more trusted signing keys from tailnet lock",
|
||||
LongHelp: "Removes one or more trusted signing keys from tailnet lock",
|
||||
Exec: func(ctx context.Context, args []string) error {
|
||||
return runNetworkLockModify(ctx, nil, args)
|
||||
},
|
||||
Exec: runNetworkLockRemove,
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
fs := newFlagSet("lock remove")
|
||||
fs.BoolVar(&nlRemoveArgs.resign, "re-sign", true, "resign signatures which would be invalidated by removal of trusted signing keys")
|
||||
return fs
|
||||
})(),
|
||||
}
|
||||
|
||||
func runNetworkLockRemove(ctx context.Context, args []string) error {
|
||||
removeKeys, _, err := parseNLArgs(args, true, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
st, err := localClient.NetworkLockStatus(ctx)
|
||||
if err != nil {
|
||||
return fixTailscaledConnectError(err)
|
||||
}
|
||||
if !st.Enabled {
|
||||
return errors.New("tailnet lock is not enabled")
|
||||
}
|
||||
|
||||
if nlRemoveArgs.resign {
|
||||
// Validate we are not removing trust in ourselves while resigning. This is because
|
||||
// we resign with our own key, so the signatures would be immediately invalid.
|
||||
for _, k := range removeKeys {
|
||||
kID, err := k.ID()
|
||||
if err != nil {
|
||||
return fmt.Errorf("computing KeyID for key %v: %w", k, err)
|
||||
}
|
||||
if bytes.Equal(st.PublicKey.KeyID(), kID) {
|
||||
return errors.New("cannot remove local trusted signing key while resigning; run command on a different node or with --re-sign=false")
|
||||
}
|
||||
}
|
||||
|
||||
// Resign affected signatures for each of the keys we are removing.
|
||||
for _, k := range removeKeys {
|
||||
kID, _ := k.ID() // err already checked above
|
||||
sigs, err := localClient.NetworkLockAffectedSigs(ctx, kID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("affected sigs for key %X: %w", kID, err)
|
||||
}
|
||||
|
||||
for _, sigBytes := range sigs {
|
||||
var sig tka.NodeKeySignature
|
||||
if err := sig.Unserialize(sigBytes); err != nil {
|
||||
return fmt.Errorf("failed decoding signature: %w", err)
|
||||
}
|
||||
var nodeKey key.NodePublic
|
||||
if err := nodeKey.UnmarshalBinary(sig.Pubkey); err != nil {
|
||||
return fmt.Errorf("failed decoding pubkey for signature: %w", err)
|
||||
}
|
||||
|
||||
// Safety: NetworkLockAffectedSigs() verifies all signatures before
|
||||
// successfully returning.
|
||||
rotationKey, _ := sig.UnverifiedWrappingPublic()
|
||||
if err := localClient.NetworkLockSign(ctx, nodeKey, []byte(rotationKey)); err != nil {
|
||||
return fmt.Errorf("failed to sign %v: %w", nodeKey, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return localClient.NetworkLockModify(ctx, nil, removeKeys)
|
||||
}
|
||||
|
||||
// parseNLArgs parses a slice of strings into slices of tka.Key & disablement
|
||||
@@ -350,13 +435,19 @@ func runNetworkLockModify(ctx context.Context, addArgs, removeArgs []string) err
|
||||
|
||||
var nlSignCmd = &ffcli.Command{
|
||||
Name: "sign",
|
||||
ShortUsage: "sign <node-key> [<rotation-key>]",
|
||||
ShortHelp: "Signs a node key and transmits the signature to the coordination server",
|
||||
LongHelp: "Signs a node key and transmits the signature to the coordination server",
|
||||
Exec: runNetworkLockSign,
|
||||
ShortUsage: "sign <node-key> [<rotation-key>] or sign <auth-key>",
|
||||
ShortHelp: "Signs a node or pre-approved auth key",
|
||||
LongHelp: `Either:
|
||||
- signs a node key and transmits the signature to the coordination server, or
|
||||
- signs a pre-approved auth key, printing it in a form that can be used to bring up nodes under tailnet lock`,
|
||||
Exec: runNetworkLockSign,
|
||||
}
|
||||
|
||||
func runNetworkLockSign(ctx context.Context, args []string) error {
|
||||
if len(args) > 0 && strings.HasPrefix(args[0], "tskey-auth-") {
|
||||
return runTskeyWrapCmd(ctx, args)
|
||||
}
|
||||
|
||||
var (
|
||||
nodeKey key.NodePublic
|
||||
rotationKey key.NLPublic
|
||||
@@ -558,3 +649,56 @@ func runNetworkLockLog(ctx context.Context, args []string) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func runTskeyWrapCmd(ctx context.Context, args []string) error {
|
||||
if len(args) != 1 {
|
||||
return errors.New("usage: lock tskey-wrap <tailscale pre-auth key>")
|
||||
}
|
||||
if strings.Contains(args[0], "--TL") {
|
||||
return errors.New("Error: provided key was already wrapped")
|
||||
}
|
||||
|
||||
st, err := localClient.StatusWithoutPeers(ctx)
|
||||
if err != nil {
|
||||
return fixTailscaledConnectError(err)
|
||||
}
|
||||
|
||||
return wrapAuthKey(ctx, args[0], st)
|
||||
}
|
||||
|
||||
func wrapAuthKey(ctx context.Context, keyStr string, status *ipnstate.Status) error {
|
||||
// Generate a separate tailnet-lock key just for the credential signature.
|
||||
// We use the free-form meta strings to mark a little bit of metadata about this
|
||||
// key.
|
||||
priv := key.NewNLPrivate()
|
||||
m := map[string]string{
|
||||
"purpose": "pre-auth key",
|
||||
"wrapper_stableid": string(status.Self.ID),
|
||||
"wrapper_createtime": fmt.Sprint(time.Now().Unix()),
|
||||
}
|
||||
if strings.HasPrefix(keyStr, "tskey-auth-") && strings.Index(keyStr[len("tskey-auth-"):], "-") > 0 {
|
||||
// We don't want to accidentally embed the nonce part of the authkey in
|
||||
// the event the format changes. As such, we make sure its in the format we
|
||||
// expect (tskey-auth-<stableID, inc CNTRL suffix>-nonce) before we parse
|
||||
// out and embed the stableID.
|
||||
s := strings.TrimPrefix(keyStr, "tskey-auth-")
|
||||
m["authkey_stableid"] = s[:strings.Index(s, "-")]
|
||||
}
|
||||
k := tka.Key{
|
||||
Kind: tka.Key25519,
|
||||
Public: priv.Public().Verifier(),
|
||||
Votes: 1,
|
||||
Meta: m,
|
||||
}
|
||||
|
||||
wrapped, err := localClient.NetworkLockWrapPreauthKey(ctx, keyStr, priv)
|
||||
if err != nil {
|
||||
return fmt.Errorf("wrapping failed: %w", err)
|
||||
}
|
||||
if err := localClient.NetworkLockModify(ctx, []tka.Key{k}, nil); err != nil {
|
||||
return fmt.Errorf("add key failed: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println(wrapped)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -21,10 +21,8 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
"golang.org/x/exp/slices"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/util/mak"
|
||||
"tailscale.com/version"
|
||||
)
|
||||
@@ -35,80 +33,59 @@ 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 [flags] <mount-point> {proxy|path|text} <arg>
|
||||
serve [flags] <sub-command> [sub-flags] <args>`),
|
||||
serve https:<port> <mount-point> <source> [off]
|
||||
serve tcp:<port> tcp://localhost:<local-port> [off]
|
||||
serve tls-terminated-tcp:<port> tcp://localhost:<local-port> [off]
|
||||
serve status [--json]
|
||||
`),
|
||||
LongHelp: strings.TrimSpace(`
|
||||
*** 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
|
||||
your tailnet.
|
||||
your tailnet.
|
||||
|
||||
You can also choose to enable the Tailscale Funnel with:
|
||||
'tailscale serve funnel on'. Funnel allows you to publish
|
||||
'tailscale funnel on'. Funnel allows you to publish
|
||||
a 'tailscale serve' server publicly, open to the entire
|
||||
internet. See https://tailscale.com/funnel.
|
||||
|
||||
EXAMPLES
|
||||
- To proxy requests to a web server at 127.0.0.1:3000:
|
||||
$ tailscale serve / proxy 3000
|
||||
$ tailscale serve https:443 / http://127.0.0.1:3000
|
||||
|
||||
Or, using the default port:
|
||||
$ tailscale serve https / http://127.0.0.1:3000
|
||||
|
||||
- To serve a single file or a directory of files:
|
||||
$ tailscale serve / path /home/alice/blog/index.html
|
||||
$ tailscale serve /images/ path /home/alice/blog/images
|
||||
$ tailscale serve https / /home/alice/blog/index.html
|
||||
$ tailscale serve https /images/ /home/alice/blog/images
|
||||
|
||||
- To serve simple static text:
|
||||
$ tailscale serve / text "Hello, world!"
|
||||
$ tailscale serve https:8080 / text:"Hello, world!"
|
||||
|
||||
- To forward incoming TCP connections on port 2222 to a local TCP server on
|
||||
port 22 (e.g. to run OpenSSH in parallel with Tailscale SSH):
|
||||
$ tailscale serve tcp:2222 tcp://localhost:22
|
||||
|
||||
- To accept TCP TLS connections (terminated within tailscaled) proxied to a
|
||||
local plaintext server on port 80:
|
||||
$ tailscale serve tls-terminated-tcp:443 tcp://localhost:80
|
||||
`),
|
||||
Exec: e.runServe,
|
||||
FlagSet: e.newFlags("serve", func(fs *flag.FlagSet) {
|
||||
fs.BoolVar(&e.remove, "remove", false, "remove an existing serve config")
|
||||
fs.UintVar(&e.servePort, "serve-port", 443, "port to serve on (443, 8443 or 10000)")
|
||||
}),
|
||||
Exec: e.runServe,
|
||||
UsageFunc: usageFunc,
|
||||
Subcommands: []*ffcli.Command{
|
||||
{
|
||||
Name: "status",
|
||||
Exec: e.runServeStatus,
|
||||
ShortHelp: "show current serve status",
|
||||
ShortHelp: "show current serve/funnel status",
|
||||
FlagSet: e.newFlags("serve-status", func(fs *flag.FlagSet) {
|
||||
fs.BoolVar(&e.json, "json", false, "output JSON")
|
||||
}),
|
||||
UsageFunc: usageFunc,
|
||||
},
|
||||
{
|
||||
Name: "tcp",
|
||||
Exec: e.runServeTCP,
|
||||
ShortHelp: "add or remove a TCP port forward",
|
||||
LongHelp: strings.Join([]string{
|
||||
"EXAMPLES",
|
||||
" - Forward TLS over TCP to a local TCP server on port 5432:",
|
||||
" $ tailscale serve tcp 5432",
|
||||
"",
|
||||
" - Forward raw, TLS-terminated TCP packets to a local TCP server on port 5432:",
|
||||
" $ tailscale serve tcp --terminate-tls 5432",
|
||||
}, "\n"),
|
||||
FlagSet: e.newFlags("serve-tcp", func(fs *flag.FlagSet) {
|
||||
fs.BoolVar(&e.terminateTLS, "terminate-tls", false, "terminate TLS before forwarding TCP connection")
|
||||
}),
|
||||
UsageFunc: usageFunc,
|
||||
},
|
||||
{
|
||||
Name: "funnel",
|
||||
Exec: e.runServeFunnel,
|
||||
ShortUsage: "funnel [flags] {on|off}",
|
||||
ShortHelp: "turn Tailscale Funnel on or off",
|
||||
LongHelp: strings.Join([]string{
|
||||
"Funnel allows you to publish a 'tailscale serve'",
|
||||
"server publicly, open to the entire internet.",
|
||||
"",
|
||||
"Turning off Funnel only turns off serving to the internet.",
|
||||
"It does not affect serving to your tailnet.",
|
||||
}, "\n"),
|
||||
UsageFunc: usageFunc,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -145,10 +122,7 @@ type localServeClient interface {
|
||||
// It also contains the flags, as registered with newServeCommand.
|
||||
type serveEnv struct {
|
||||
// flags
|
||||
servePort uint // Port to serve on. Defaults to 443.
|
||||
terminateTLS bool
|
||||
remove bool // remove a serve config
|
||||
json bool // output JSON (status only for now)
|
||||
json bool // output JSON (status only for now)
|
||||
|
||||
lc localServeClient // localClient interface, specific to serve
|
||||
|
||||
@@ -188,28 +162,15 @@ func (e *serveEnv) getLocalClientStatus(ctx context.Context) (*ipnstate.Status,
|
||||
return st, nil
|
||||
}
|
||||
|
||||
// validateServePort returns --serve-port flag value,
|
||||
// or an error if the port is not a valid port to serve on.
|
||||
func (e *serveEnv) validateServePort() (port uint16, err error) {
|
||||
// make sure e.servePort is uint16
|
||||
port = uint16(e.servePort)
|
||||
if uint(port) != e.servePort {
|
||||
return 0, fmt.Errorf("serve-port %d is out of range", e.servePort)
|
||||
}
|
||||
// make sure e.servePort is 443, 8443 or 10000
|
||||
if port != 443 && port != 8443 && port != 10000 {
|
||||
return 0, fmt.Errorf("serve-port %d is invalid; must be 443, 8443 or 10000", e.servePort)
|
||||
}
|
||||
return port, nil
|
||||
}
|
||||
|
||||
// runServe is the entry point for the "serve" subcommand, managing Web
|
||||
// serve config types like proxy, path, and text.
|
||||
//
|
||||
// Examples:
|
||||
// - tailscale serve / proxy 3000
|
||||
// - tailscale serve /images/ path /var/www/images/
|
||||
// - tailscale --serve-port=10000 serve /motd.txt text "Hello, world!"
|
||||
// - tailscale serve https / http://localhost:3000
|
||||
// - tailscale serve https /images/ /var/www/images/
|
||||
// - tailscale serve https:10000 /motd.txt text:"Hello, world!"
|
||||
// - tailscale serve tcp:2222 tcp://localhost:22
|
||||
// - tailscale serve tls-terminated-tcp:443 tcp://localhost:80
|
||||
func (e *serveEnv) runServe(ctx context.Context, args []string) error {
|
||||
if len(args) == 0 {
|
||||
return flag.ErrHelp
|
||||
@@ -229,39 +190,94 @@ func (e *serveEnv) runServe(ctx context.Context, args []string) error {
|
||||
return e.lc.SetServeConfig(ctx, sc)
|
||||
}
|
||||
|
||||
if !(len(args) == 3 || (e.remove && len(args) >= 1)) {
|
||||
parsePort := func(portStr string) (uint16, error) {
|
||||
port64, err := strconv.ParseUint(portStr, 10, 16)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return uint16(port64), nil
|
||||
}
|
||||
|
||||
srcType, srcPortStr, found := strings.Cut(args[0], ":")
|
||||
if !found {
|
||||
if srcType == "https" && srcPortStr == "" {
|
||||
// Default https port to 443.
|
||||
srcPortStr = "443"
|
||||
} else {
|
||||
return flag.ErrHelp
|
||||
}
|
||||
}
|
||||
|
||||
turnOff := "off" == args[len(args)-1]
|
||||
|
||||
if len(args) < 2 || (srcType == "https" && !turnOff && len(args) < 3) {
|
||||
fmt.Fprintf(os.Stderr, "error: invalid number of arguments\n\n")
|
||||
return flag.ErrHelp
|
||||
}
|
||||
|
||||
srvPort, err := e.validateServePort()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
srvPortStr := strconv.Itoa(int(srvPort))
|
||||
|
||||
mount, err := cleanMountPoint(args[0])
|
||||
srcPort, err := parsePort(srcPortStr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if e.remove {
|
||||
return e.handleWebServeRemove(ctx, mount)
|
||||
switch srcType {
|
||||
case "https":
|
||||
mount, err := cleanMountPoint(args[1])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if turnOff {
|
||||
return e.handleWebServeRemove(ctx, srcPort, mount)
|
||||
}
|
||||
return e.handleWebServe(ctx, srcPort, mount, args[2])
|
||||
case "tcp", "tls-terminated-tcp":
|
||||
if turnOff {
|
||||
return e.handleTCPServeRemove(ctx, srcPort)
|
||||
}
|
||||
return e.handleTCPServe(ctx, srcType, srcPort, args[1])
|
||||
default:
|
||||
fmt.Fprintf(os.Stderr, "error: invalid serve type %q\n", srcType)
|
||||
fmt.Fprint(os.Stderr, "must be one of: https:<port>, tcp:<port> or tls-terminated-tcp:<port>\n\n", srcType)
|
||||
return flag.ErrHelp
|
||||
}
|
||||
}
|
||||
|
||||
// handleWebServe handles the "tailscale serve https:..." subcommand.
|
||||
// It configures the serve config to forward HTTPS connections to the
|
||||
// given source.
|
||||
//
|
||||
// Examples:
|
||||
// - tailscale serve https / http://localhost:3000
|
||||
// - tailscale serve https:8443 /files/ /home/alice/shared-files/
|
||||
// - tailscale serve https:10000 /motd.txt text:"Hello, world!"
|
||||
func (e *serveEnv) handleWebServe(ctx context.Context, srvPort uint16, mount, source string) error {
|
||||
h := new(ipn.HTTPHandler)
|
||||
|
||||
switch args[1] {
|
||||
case "path":
|
||||
ts, _, _ := strings.Cut(source, ":")
|
||||
switch {
|
||||
case ts == "text":
|
||||
text := strings.TrimPrefix(source, "text:")
|
||||
if text == "" {
|
||||
return errors.New("unable to serve; text cannot be an empty string")
|
||||
}
|
||||
h.Text = text
|
||||
case isProxyTarget(source):
|
||||
t, err := expandProxyTarget(source)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
h.Proxy = t
|
||||
default: // assume path
|
||||
if version.IsSandboxedMacOS() {
|
||||
// don't allow path serving for now on macOS (2022-11-15)
|
||||
return fmt.Errorf("path serving is not supported if sandboxed on macOS")
|
||||
}
|
||||
if !filepath.IsAbs(args[2]) {
|
||||
if !filepath.IsAbs(source) {
|
||||
fmt.Fprintf(os.Stderr, "error: path must be absolute\n\n")
|
||||
return flag.ErrHelp
|
||||
}
|
||||
fi, err := os.Stat(args[2])
|
||||
source = filepath.Clean(source)
|
||||
fi, err := os.Stat(source)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error: invalid path: %v\n\n", err)
|
||||
return flag.ErrHelp
|
||||
@@ -271,21 +287,7 @@ func (e *serveEnv) runServe(ctx context.Context, args []string) error {
|
||||
// for relative file links to work
|
||||
mount += "/"
|
||||
}
|
||||
h.Path = args[2]
|
||||
case "proxy":
|
||||
t, err := expandProxyTarget(args[2])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
h.Proxy = t
|
||||
case "text":
|
||||
if args[2] == "" {
|
||||
return errors.New("unable to serve; text cannot be an empty string")
|
||||
}
|
||||
h.Text = args[2]
|
||||
default:
|
||||
fmt.Fprintf(os.Stderr, "error: unknown serve type %q\n\n", args[1])
|
||||
return flag.ErrHelp
|
||||
h.Path = source
|
||||
}
|
||||
|
||||
cursc, err := e.lc.GetServeConfig(ctx)
|
||||
@@ -300,7 +302,7 @@ func (e *serveEnv) runServe(ctx context.Context, args []string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
hp := ipn.HostPort(net.JoinHostPort(dnsName, srvPortStr))
|
||||
hp := ipn.HostPort(net.JoinHostPort(dnsName, strconv.Itoa(int(srvPort))))
|
||||
|
||||
if sc.IsTCPForwardingOnPort(srvPort) {
|
||||
fmt.Fprintf(os.Stderr, "error: cannot serve web; already serving TCP\n")
|
||||
@@ -339,12 +341,36 @@ func (e *serveEnv) runServe(ctx context.Context, args []string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *serveEnv) handleWebServeRemove(ctx context.Context, mount string) error {
|
||||
srvPort, err := e.validateServePort()
|
||||
if err != nil {
|
||||
return err
|
||||
// isProxyTarget reports whether source is a valid proxy target.
|
||||
func isProxyTarget(source string) bool {
|
||||
if strings.HasPrefix(source, "http://") ||
|
||||
strings.HasPrefix(source, "https://") ||
|
||||
strings.HasPrefix(source, "https+insecure://") {
|
||||
return true
|
||||
}
|
||||
srvPortStr := strconv.Itoa(int(srvPort))
|
||||
// support "localhost:3000", for example
|
||||
_, portStr, ok := strings.Cut(source, ":")
|
||||
if ok && allNumeric(portStr) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// allNumeric reports whether s only comprises of digits
|
||||
// and has at least one digit.
|
||||
func allNumeric(s string) bool {
|
||||
for i := 0; i < len(s); i++ {
|
||||
if s[i] < '0' || s[i] > '9' {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return s != ""
|
||||
}
|
||||
|
||||
// handleWebServeRemove removes a web handler from the serve config.
|
||||
// The srvPort argument is the serving port and the mount argument is
|
||||
// the mount point or registered path to remove.
|
||||
func (e *serveEnv) handleWebServeRemove(ctx context.Context, srvPort uint16, mount string) error {
|
||||
sc, err := e.lc.GetServeConfig(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -359,9 +385,9 @@ func (e *serveEnv) handleWebServeRemove(ctx context.Context, mount string) error
|
||||
if sc.IsTCPForwardingOnPort(srvPort) {
|
||||
return errors.New("cannot remove web handler; currently serving TCP")
|
||||
}
|
||||
hp := ipn.HostPort(net.JoinHostPort(dnsName, srvPortStr))
|
||||
hp := ipn.HostPort(net.JoinHostPort(dnsName, strconv.Itoa(int(srvPort))))
|
||||
if !sc.WebHandlerExists(hp, mount) {
|
||||
return errors.New("error: serve config does not exist")
|
||||
return errors.New("error: handler does not exist")
|
||||
}
|
||||
// delete existing handler, then cascade delete if empty
|
||||
delete(sc.Web[hp].Handlers, mount)
|
||||
@@ -396,18 +422,11 @@ func cleanMountPoint(mount string) (string, error) {
|
||||
return "", fmt.Errorf("invalid mount point %q", mount)
|
||||
}
|
||||
|
||||
func expandProxyTarget(target string) (string, error) {
|
||||
if allNumeric(target) {
|
||||
p, err := strconv.ParseUint(target, 10, 16)
|
||||
if p == 0 || err != nil {
|
||||
return "", fmt.Errorf("invalid port %q", target)
|
||||
}
|
||||
return "http://127.0.0.1:" + target, nil
|
||||
func expandProxyTarget(source string) (string, error) {
|
||||
if !strings.Contains(source, "://") {
|
||||
source = "http://" + source
|
||||
}
|
||||
if !strings.Contains(target, "://") {
|
||||
target = "http://" + target
|
||||
}
|
||||
u, err := url.ParseRequestURI(target)
|
||||
u, err := url.ParseRequestURI(source)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("parsing url: %w", err)
|
||||
}
|
||||
@@ -417,9 +436,14 @@ func expandProxyTarget(target string) (string, error) {
|
||||
default:
|
||||
return "", fmt.Errorf("must be a URL starting with http://, https://, or https+insecure://")
|
||||
}
|
||||
|
||||
port, err := strconv.ParseUint(u.Port(), 10, 16)
|
||||
if port == 0 || err != nil {
|
||||
return "", fmt.Errorf("invalid port %q: %w", u.Port(), err)
|
||||
}
|
||||
|
||||
host := u.Hostname()
|
||||
switch host {
|
||||
// TODO(shayne,bradfitz): do we want to do this?
|
||||
case "localhost", "127.0.0.1":
|
||||
host = "127.0.0.1"
|
||||
default:
|
||||
@@ -432,16 +456,111 @@ func expandProxyTarget(target string) (string, error) {
|
||||
return url, nil
|
||||
}
|
||||
|
||||
func allNumeric(s string) bool {
|
||||
for i := 0; i < len(s); i++ {
|
||||
if s[i] < '0' || s[i] > '9' {
|
||||
return false
|
||||
// handleTCPServe handles the "tailscale serve tls-terminated-tcp:..." subcommand.
|
||||
// It configures the serve config to forward TCP connections to the
|
||||
// given source.
|
||||
//
|
||||
// Examples:
|
||||
// - tailscale serve tcp:2222 tcp://localhost:22
|
||||
// - tailscale serve tls-terminated-tcp:8443 tcp://localhost:8080
|
||||
func (e *serveEnv) handleTCPServe(ctx context.Context, srcType string, srcPort uint16, dest string) error {
|
||||
var terminateTLS bool
|
||||
switch srcType {
|
||||
case "tcp":
|
||||
terminateTLS = false
|
||||
case "tls-terminated-tcp":
|
||||
terminateTLS = true
|
||||
default:
|
||||
fmt.Fprintf(os.Stderr, "error: invalid TCP source %q\n\n", dest)
|
||||
return flag.ErrHelp
|
||||
}
|
||||
|
||||
dstURL, err := url.Parse(dest)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error: invalid TCP source %q: %v\n\n", dest, err)
|
||||
return flag.ErrHelp
|
||||
}
|
||||
host, dstPortStr, err := net.SplitHostPort(dstURL.Host)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error: invalid TCP source %q: %v\n\n", dest, err)
|
||||
return flag.ErrHelp
|
||||
}
|
||||
|
||||
switch host {
|
||||
case "localhost", "127.0.0.1":
|
||||
// ok
|
||||
default:
|
||||
fmt.Fprintf(os.Stderr, "error: invalid TCP source %q\n", dest)
|
||||
fmt.Fprint(os.Stderr, "must be one of: localhost or 127.0.0.1\n\n", dest)
|
||||
return flag.ErrHelp
|
||||
}
|
||||
|
||||
if p, err := strconv.ParseUint(dstPortStr, 10, 16); p == 0 || err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error: invalid port %q\n\n", dstPortStr)
|
||||
return flag.ErrHelp
|
||||
}
|
||||
|
||||
cursc, err := e.lc.GetServeConfig(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sc := cursc.Clone() // nil if no config
|
||||
if sc == nil {
|
||||
sc = new(ipn.ServeConfig)
|
||||
}
|
||||
|
||||
fwdAddr := "127.0.0.1:" + dstPortStr
|
||||
|
||||
if sc.IsServingWeb(srcPort) {
|
||||
return fmt.Errorf("cannot serve TCP; already serving web on %d", srcPort)
|
||||
}
|
||||
|
||||
mak.Set(&sc.TCP, srcPort, &ipn.TCPPortHandler{TCPForward: fwdAddr})
|
||||
|
||||
dnsName, err := e.getSelfDNSName(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if terminateTLS {
|
||||
sc.TCP[srcPort].TerminateTLS = dnsName
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(cursc, sc) {
|
||||
if err := e.lc.SetServeConfig(ctx, sc); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return s != ""
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// runServeStatus prints the current serve config.
|
||||
// handleTCPServeRemove removes the TCP forwarding configuration for the
|
||||
// given srvPort, or serving port.
|
||||
func (e *serveEnv) handleTCPServeRemove(ctx context.Context, src uint16) error {
|
||||
cursc, err := e.lc.GetServeConfig(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sc := cursc.Clone() // nil if no config
|
||||
if sc == nil {
|
||||
sc = new(ipn.ServeConfig)
|
||||
}
|
||||
if sc.IsServingWeb(src) {
|
||||
return fmt.Errorf("unable to remove; serving web, not TCP forwarding on serve port %d", src)
|
||||
}
|
||||
if ph := sc.GetTCPPortHandler(src); ph != nil {
|
||||
delete(sc.TCP, src)
|
||||
// clear map mostly for testing
|
||||
if len(sc.TCP) == 0 {
|
||||
sc.TCP = nil
|
||||
}
|
||||
return e.lc.SetServeConfig(ctx, sc)
|
||||
}
|
||||
return errors.New("error: serve config does not exist")
|
||||
}
|
||||
|
||||
// runServeStatus is the entry point for the "serve status"
|
||||
// subcommand and prints the current serve config.
|
||||
//
|
||||
// Examples:
|
||||
// - tailscale status
|
||||
@@ -460,6 +579,7 @@ func (e *serveEnv) runServeStatus(ctx context.Context, args []string) error {
|
||||
e.stdout().Write(j)
|
||||
return nil
|
||||
}
|
||||
printFunnelStatus(ctx)
|
||||
if sc == nil || (len(sc.TCP) == 0 && len(sc.Web) == 0 && len(sc.AllowFunnel) == 0) {
|
||||
printf("No serve config\n")
|
||||
return nil
|
||||
@@ -478,17 +598,7 @@ func (e *serveEnv) runServeStatus(ctx context.Context, args []string) error {
|
||||
printWebStatusTree(sc, hp)
|
||||
printf("\n")
|
||||
}
|
||||
// warn when funnel on without handlers
|
||||
for hp, a := range sc.AllowFunnel {
|
||||
if !a {
|
||||
continue
|
||||
}
|
||||
_, portStr, _ := net.SplitHostPort(string(hp))
|
||||
p, _ := strconv.ParseUint(portStr, 10, 16)
|
||||
if _, ok := sc.TCP[uint16(p)]; !ok {
|
||||
printf("WARNING: funnel=on for %s, but no serve config\n", hp)
|
||||
}
|
||||
}
|
||||
printFunnelWarning(sc)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -572,152 +682,3 @@ func elipticallyTruncate(s string, max int) string {
|
||||
}
|
||||
return s[:max-3] + "..."
|
||||
}
|
||||
|
||||
// runServeTCP is the entry point for the "serve tcp" subcommand and
|
||||
// manages the serve config for TCP forwarding.
|
||||
//
|
||||
// Examples:
|
||||
// - tailscale serve tcp 5432
|
||||
// - tailscale serve --serve-port=8443 tcp 4430
|
||||
// - tailscale serve --serve-port=10000 tcp --terminate-tls 8080
|
||||
func (e *serveEnv) runServeTCP(ctx context.Context, args []string) error {
|
||||
if len(args) != 1 {
|
||||
fmt.Fprintf(os.Stderr, "error: invalid number of arguments\n\n")
|
||||
return flag.ErrHelp
|
||||
}
|
||||
|
||||
srvPort, err := e.validateServePort()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
portStr := args[0]
|
||||
p, err := strconv.ParseUint(portStr, 10, 16)
|
||||
if p == 0 || err != nil {
|
||||
fmt.Fprintf(os.Stderr, "error: invalid port %q\n\n", portStr)
|
||||
}
|
||||
|
||||
cursc, err := e.lc.GetServeConfig(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sc := cursc.Clone() // nil if no config
|
||||
if sc == nil {
|
||||
sc = new(ipn.ServeConfig)
|
||||
}
|
||||
|
||||
fwdAddr := "127.0.0.1:" + portStr
|
||||
|
||||
if sc.IsServingWeb(srvPort) {
|
||||
if e.remove {
|
||||
return fmt.Errorf("unable to remove; serving web, not TCP forwarding on serve port %d", srvPort)
|
||||
}
|
||||
return fmt.Errorf("cannot serve TCP; already serving web on %d", srvPort)
|
||||
}
|
||||
|
||||
if e.remove {
|
||||
if ph := sc.GetTCPPortHandler(srvPort); ph != nil && ph.TCPForward == fwdAddr {
|
||||
delete(sc.TCP, srvPort)
|
||||
// clear map mostly for testing
|
||||
if len(sc.TCP) == 0 {
|
||||
sc.TCP = nil
|
||||
}
|
||||
return e.lc.SetServeConfig(ctx, sc)
|
||||
}
|
||||
return errors.New("error: serve config does not exist")
|
||||
}
|
||||
|
||||
mak.Set(&sc.TCP, srvPort, &ipn.TCPPortHandler{TCPForward: fwdAddr})
|
||||
|
||||
dnsName, err := e.getSelfDNSName(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if e.terminateTLS {
|
||||
sc.TCP[srvPort].TerminateTLS = dnsName
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(cursc, sc) {
|
||||
if err := e.lc.SetServeConfig(ctx, sc); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// runServeFunnel is the entry point for the "serve funnel" subcommand and
|
||||
// manages turning on/off funnel. Funnel is off by default.
|
||||
//
|
||||
// Note: funnel is only supported on single DNS name for now. (2022-11-15)
|
||||
func (e *serveEnv) runServeFunnel(ctx context.Context, args []string) error {
|
||||
if len(args) != 1 {
|
||||
return flag.ErrHelp
|
||||
}
|
||||
|
||||
srvPort, err := e.validateServePort()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
srvPortStr := strconv.Itoa(int(srvPort))
|
||||
|
||||
var on bool
|
||||
switch args[0] {
|
||||
case "on", "off":
|
||||
on = args[0] == "on"
|
||||
default:
|
||||
return flag.ErrHelp
|
||||
}
|
||||
sc, err := e.lc.GetServeConfig(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if sc == nil {
|
||||
sc = new(ipn.ServeConfig)
|
||||
}
|
||||
st, err := e.getLocalClientStatus(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting client status: %w", err)
|
||||
}
|
||||
if err := checkHasAccess(st.Self.Capabilities); err != nil {
|
||||
return err
|
||||
}
|
||||
dnsName := strings.TrimSuffix(st.Self.DNSName, ".")
|
||||
hp := ipn.HostPort(dnsName + ":" + srvPortStr)
|
||||
if on == sc.AllowFunnel[hp] {
|
||||
// Nothing to do.
|
||||
return nil
|
||||
}
|
||||
if on {
|
||||
mak.Set(&sc.AllowFunnel, hp, true)
|
||||
} else {
|
||||
delete(sc.AllowFunnel, hp)
|
||||
// clear map mostly for testing
|
||||
if len(sc.AllowFunnel) == 0 {
|
||||
sc.AllowFunnel = nil
|
||||
}
|
||||
}
|
||||
if err := e.lc.SetServeConfig(ctx, sc); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// checkHasAccess checks three things: 1) an invite was used to join the
|
||||
// Funnel alpha; 2) HTTPS is enabled; 3) the node has the "funnel" attribute.
|
||||
// If any of these are false, an error is returned describing the problem.
|
||||
//
|
||||
// The nodeAttrs arg should be the node's Self.Capabilities which should contain
|
||||
// the attribute we're checking for and possibly warning-capabilities for Funnel.
|
||||
func checkHasAccess(nodeAttrs []string) error {
|
||||
if slices.Contains(nodeAttrs, tailcfg.CapabilityWarnFunnelNoInvite) {
|
||||
return errors.New("Funnel not available; an invite is required to join the alpha. See https://tailscale.com/kb/1223/tailscale-funnel/.")
|
||||
}
|
||||
if slices.Contains(nodeAttrs, tailcfg.CapabilityWarnFunnelNoHTTPS) {
|
||||
return errors.New("Funnel not available; HTTPS must be enabled. See https://tailscale.com/kb/1153/enabling-https/.")
|
||||
}
|
||||
if !slices.Contains(nodeAttrs, tailcfg.NodeAttrFunnel) {
|
||||
return errors.New("Funnel not available; \"funnel\" node attribute not set. See https://tailscale.com/kb/1223/tailscale-funnel/.")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/tailcfg"
|
||||
@@ -48,30 +49,6 @@ func TestCleanMountPoint(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckHasAccess(t *testing.T) {
|
||||
tests := []struct {
|
||||
caps []string
|
||||
wantErr bool
|
||||
}{
|
||||
{[]string{}, true}, // No "funnel" attribute
|
||||
{[]string{tailcfg.CapabilityWarnFunnelNoInvite}, true},
|
||||
{[]string{tailcfg.CapabilityWarnFunnelNoHTTPS}, true},
|
||||
{[]string{tailcfg.NodeAttrFunnel}, false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
err := checkHasAccess(tt.caps)
|
||||
switch {
|
||||
case err != nil && tt.wantErr,
|
||||
err == nil && !tt.wantErr:
|
||||
continue
|
||||
case tt.wantErr:
|
||||
t.Fatalf("got no error, want error")
|
||||
case !tt.wantErr:
|
||||
t.Fatalf("got error %v, want no error", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestServeConfigMutations(t *testing.T) {
|
||||
// Stateful mutations, starting from an empty config.
|
||||
type step struct {
|
||||
@@ -80,6 +57,8 @@ func TestServeConfigMutations(t *testing.T) {
|
||||
want *ipn.ServeConfig // non-nil means we want a save of this value
|
||||
wantErr func(error) (badErrMsg string) // nil means no error is wanted
|
||||
line int // line number of addStep call, for error messages
|
||||
|
||||
debugBreak func()
|
||||
}
|
||||
var steps []step
|
||||
add := func(s step) {
|
||||
@@ -90,19 +69,19 @@ func TestServeConfigMutations(t *testing.T) {
|
||||
// funnel
|
||||
add(step{reset: true})
|
||||
add(step{
|
||||
command: cmd("funnel on"),
|
||||
command: cmd("funnel 443 on"),
|
||||
want: &ipn.ServeConfig{AllowFunnel: map[ipn.HostPort]bool{"foo.test.ts.net:443": true}},
|
||||
})
|
||||
add(step{
|
||||
command: cmd("funnel on"),
|
||||
command: cmd("funnel 443 on"),
|
||||
want: nil, // nothing to save
|
||||
})
|
||||
add(step{
|
||||
command: cmd("funnel off"),
|
||||
command: cmd("funnel 443 off"),
|
||||
want: &ipn.ServeConfig{},
|
||||
})
|
||||
add(step{
|
||||
command: cmd("funnel off"),
|
||||
command: cmd("funnel 443 off"),
|
||||
want: nil, // nothing to save
|
||||
})
|
||||
add(step{
|
||||
@@ -113,27 +92,23 @@ func TestServeConfigMutations(t *testing.T) {
|
||||
// https
|
||||
add(step{reset: true})
|
||||
add(step{
|
||||
command: cmd("/ proxy 0"), // invalid port, too low
|
||||
command: cmd("https:443 / http://localhost:0"), // invalid port, too low
|
||||
wantErr: anyErr(),
|
||||
})
|
||||
add(step{
|
||||
command: cmd("/ proxy 65536"), // invalid port, too high
|
||||
command: cmd("https:443 / http://localhost:65536"), // invalid port, too high
|
||||
wantErr: anyErr(),
|
||||
})
|
||||
add(step{
|
||||
command: cmd("/ proxy somehost"), // invalid host
|
||||
command: cmd("https:443 / http://somehost:3000"), // invalid host
|
||||
wantErr: anyErr(),
|
||||
})
|
||||
add(step{
|
||||
command: cmd("/ proxy http://otherhost"), // invalid host
|
||||
command: cmd("https:443 / httpz://127.0.0.1"), // invalid scheme
|
||||
wantErr: anyErr(),
|
||||
})
|
||||
add(step{
|
||||
command: cmd("/ proxy httpz://127.0.0.1"), // invalid scheme
|
||||
wantErr: anyErr(),
|
||||
})
|
||||
add(step{
|
||||
command: cmd("/ proxy 3000"),
|
||||
add(step{ // allow omitting port (default to 443)
|
||||
command: cmd("https / http://localhost:3000"),
|
||||
want: &ipn.ServeConfig{
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
@@ -143,12 +118,33 @@ func TestServeConfigMutations(t *testing.T) {
|
||||
},
|
||||
},
|
||||
})
|
||||
add(step{ // invalid port
|
||||
command: cmd("--serve-port=9999 /abc proxy 3001"),
|
||||
wantErr: anyErr(),
|
||||
add(step{ // support non Funnel port
|
||||
command: cmd("https:9999 /abc http://localhost:3001"),
|
||||
want: &ipn.ServeConfig{
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}, 9999: {HTTPS: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/": {Proxy: "http://127.0.0.1:3000"},
|
||||
}},
|
||||
"foo.test.ts.net:9999": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/abc": {Proxy: "http://127.0.0.1:3001"},
|
||||
}},
|
||||
},
|
||||
},
|
||||
})
|
||||
add(step{
|
||||
command: cmd("--serve-port=8443 /abc proxy 3001"),
|
||||
command: cmd("https:9999 /abc off"),
|
||||
want: &ipn.ServeConfig{
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/": {Proxy: "http://127.0.0.1:3000"},
|
||||
}},
|
||||
},
|
||||
},
|
||||
})
|
||||
add(step{
|
||||
command: cmd("https:8443 /abc http://127.0.0.1:3001"),
|
||||
want: &ipn.ServeConfig{
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}, 8443: {HTTPS: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
@@ -162,7 +158,7 @@ func TestServeConfigMutations(t *testing.T) {
|
||||
},
|
||||
})
|
||||
add(step{
|
||||
command: cmd("--serve-port=10000 / text hi"),
|
||||
command: cmd("https:10000 / text:hi"),
|
||||
want: &ipn.ServeConfig{
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{
|
||||
443: {HTTPS: true}, 8443: {HTTPS: true}, 10000: {HTTPS: true}},
|
||||
@@ -180,12 +176,12 @@ func TestServeConfigMutations(t *testing.T) {
|
||||
},
|
||||
})
|
||||
add(step{
|
||||
command: cmd("--remove /foo"),
|
||||
command: cmd("https:443 /foo off"),
|
||||
want: nil, // nothing to save
|
||||
wantErr: anyErr(),
|
||||
}) // handler doesn't exist, so we get an error
|
||||
add(step{
|
||||
command: cmd("--remove --serve-port=10000 /"),
|
||||
command: cmd("https:10000 / off"),
|
||||
want: &ipn.ServeConfig{
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}, 8443: {HTTPS: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
@@ -199,7 +195,7 @@ func TestServeConfigMutations(t *testing.T) {
|
||||
},
|
||||
})
|
||||
add(step{
|
||||
command: cmd("--remove /"),
|
||||
command: cmd("https:443 / off"),
|
||||
want: &ipn.ServeConfig{
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{8443: {HTTPS: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
@@ -210,11 +206,11 @@ func TestServeConfigMutations(t *testing.T) {
|
||||
},
|
||||
})
|
||||
add(step{
|
||||
command: cmd("--remove --serve-port=8443 /abc"),
|
||||
command: cmd("https:8443 /abc off"),
|
||||
want: &ipn.ServeConfig{},
|
||||
})
|
||||
add(step{
|
||||
command: cmd("bar proxy https://127.0.0.1:8443"),
|
||||
add(step{ // clean mount: "bar" becomes "/bar"
|
||||
command: cmd("https:443 bar https://127.0.0.1:8443"),
|
||||
want: &ipn.ServeConfig{
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
@@ -225,12 +221,12 @@ func TestServeConfigMutations(t *testing.T) {
|
||||
},
|
||||
})
|
||||
add(step{
|
||||
command: cmd("bar proxy https://127.0.0.1:8443"),
|
||||
command: cmd("https:443 bar https://127.0.0.1:8443"),
|
||||
want: nil, // nothing to save
|
||||
})
|
||||
add(step{reset: true})
|
||||
add(step{
|
||||
command: cmd("/ proxy https+insecure://127.0.0.1:3001"),
|
||||
command: cmd("https:443 / https+insecure://127.0.0.1:3001"),
|
||||
want: &ipn.ServeConfig{
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
@@ -242,7 +238,7 @@ func TestServeConfigMutations(t *testing.T) {
|
||||
})
|
||||
add(step{reset: true})
|
||||
add(step{
|
||||
command: cmd("/foo proxy localhost:3000"),
|
||||
command: cmd("https:443 /foo localhost:3000"),
|
||||
want: &ipn.ServeConfig{
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
@@ -253,7 +249,7 @@ func TestServeConfigMutations(t *testing.T) {
|
||||
},
|
||||
})
|
||||
add(step{ // test a second handler on the same port
|
||||
command: cmd("--serve-port=8443 /foo proxy localhost:3000"),
|
||||
command: cmd("https:8443 /foo localhost:3000"),
|
||||
want: &ipn.ServeConfig{
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}, 8443: {HTTPS: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
@@ -269,16 +265,35 @@ func TestServeConfigMutations(t *testing.T) {
|
||||
|
||||
// tcp
|
||||
add(step{reset: true})
|
||||
add(step{ // must include scheme for tcp
|
||||
command: cmd("tls-terminated-tcp:443 localhost:5432"),
|
||||
wantErr: exactErr(flag.ErrHelp, "flag.ErrHelp"),
|
||||
})
|
||||
add(step{ // !somehost, must be localhost or 127.0.0.1
|
||||
command: cmd("tls-terminated-tcp:443 tcp://somehost:5432"),
|
||||
wantErr: exactErr(flag.ErrHelp, "flag.ErrHelp"),
|
||||
})
|
||||
add(step{ // bad target port, too low
|
||||
command: cmd("tls-terminated-tcp:443 tcp://somehost:0"),
|
||||
wantErr: exactErr(flag.ErrHelp, "flag.ErrHelp"),
|
||||
})
|
||||
add(step{ // bad target port, too high
|
||||
command: cmd("tls-terminated-tcp:443 tcp://somehost:65536"),
|
||||
wantErr: exactErr(flag.ErrHelp, "flag.ErrHelp"),
|
||||
})
|
||||
add(step{
|
||||
command: cmd("tcp 5432"),
|
||||
command: cmd("tls-terminated-tcp:443 tcp://localhost:5432"),
|
||||
want: &ipn.ServeConfig{
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{
|
||||
443: {TCPForward: "127.0.0.1:5432"},
|
||||
443: {
|
||||
TCPForward: "127.0.0.1:5432",
|
||||
TerminateTLS: "foo.test.ts.net",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
add(step{
|
||||
command: cmd("tcp -terminate-tls 8443"),
|
||||
command: cmd("tls-terminated-tcp:443 tcp://127.0.0.1:8443"),
|
||||
want: &ipn.ServeConfig{
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{
|
||||
443: {
|
||||
@@ -289,11 +304,11 @@ func TestServeConfigMutations(t *testing.T) {
|
||||
},
|
||||
})
|
||||
add(step{
|
||||
command: cmd("tcp -terminate-tls 8443"),
|
||||
command: cmd("tls-terminated-tcp:443 tcp://127.0.0.1:8443"),
|
||||
want: nil, // nothing to save
|
||||
})
|
||||
add(step{
|
||||
command: cmd("tcp --terminate-tls 8444"),
|
||||
command: cmd("tls-terminated-tcp:443 tcp://localhost:8444"),
|
||||
want: &ipn.ServeConfig{
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{
|
||||
443: {
|
||||
@@ -304,35 +319,41 @@ func TestServeConfigMutations(t *testing.T) {
|
||||
},
|
||||
})
|
||||
add(step{
|
||||
command: cmd("tcp -terminate-tls=false 8445"),
|
||||
command: cmd("tls-terminated-tcp:443 tcp://127.0.0.1:8445"),
|
||||
want: &ipn.ServeConfig{
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{
|
||||
443: {TCPForward: "127.0.0.1:8445"},
|
||||
443: {
|
||||
TCPForward: "127.0.0.1:8445",
|
||||
TerminateTLS: "foo.test.ts.net",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
add(step{reset: true})
|
||||
add(step{
|
||||
command: cmd("tcp 123"),
|
||||
command: cmd("tls-terminated-tcp:443 tcp://localhost:123"),
|
||||
want: &ipn.ServeConfig{
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{
|
||||
443: {TCPForward: "127.0.0.1:123"},
|
||||
443: {
|
||||
TCPForward: "127.0.0.1:123",
|
||||
TerminateTLS: "foo.test.ts.net",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
add(step{
|
||||
command: cmd("--remove tcp 321"),
|
||||
add(step{ // handler doesn't exist, so we get an error
|
||||
command: cmd("tls-terminated-tcp:8443 off"),
|
||||
wantErr: anyErr(),
|
||||
}) // handler doesn't exist, so we get an error
|
||||
})
|
||||
add(step{
|
||||
command: cmd("--remove tcp 123"),
|
||||
command: cmd("tls-terminated-tcp:443 off"),
|
||||
want: &ipn.ServeConfig{},
|
||||
})
|
||||
|
||||
// text
|
||||
add(step{reset: true})
|
||||
add(step{
|
||||
command: cmd("/ text hello"),
|
||||
command: cmd("https:443 / text:hello"),
|
||||
want: &ipn.ServeConfig{
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
@@ -353,7 +374,7 @@ func TestServeConfigMutations(t *testing.T) {
|
||||
add(step{reset: true})
|
||||
writeFile("foo", "this is foo")
|
||||
add(step{
|
||||
command: cmd("/ path " + filepath.Join(td, "foo")),
|
||||
command: cmd("https:443 / " + filepath.Join(td, "foo")),
|
||||
want: &ipn.ServeConfig{
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
@@ -366,7 +387,7 @@ func TestServeConfigMutations(t *testing.T) {
|
||||
os.MkdirAll(filepath.Join(td, "subdir"), 0700)
|
||||
writeFile("subdir/file-a", "this is A")
|
||||
add(step{
|
||||
command: cmd("/some/where path " + filepath.Join(td, "subdir/file-a")),
|
||||
command: cmd("https:443 /some/where " + filepath.Join(td, "subdir/file-a")),
|
||||
want: &ipn.ServeConfig{
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
@@ -377,13 +398,13 @@ func TestServeConfigMutations(t *testing.T) {
|
||||
},
|
||||
},
|
||||
})
|
||||
add(step{
|
||||
command: cmd("/ path missing"),
|
||||
add(step{ // bad path
|
||||
command: cmd("https:443 / bad/path"),
|
||||
wantErr: exactErr(flag.ErrHelp, "flag.ErrHelp"),
|
||||
})
|
||||
add(step{reset: true})
|
||||
add(step{
|
||||
command: cmd("/ path " + filepath.Join(td, "subdir")),
|
||||
command: cmd("https:443 / " + filepath.Join(td, "subdir")),
|
||||
want: &ipn.ServeConfig{
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
@@ -394,14 +415,14 @@ func TestServeConfigMutations(t *testing.T) {
|
||||
},
|
||||
})
|
||||
add(step{
|
||||
command: cmd("--remove /"),
|
||||
command: cmd("https:443 / off"),
|
||||
want: &ipn.ServeConfig{},
|
||||
})
|
||||
|
||||
// combos
|
||||
add(step{reset: true})
|
||||
add(step{
|
||||
command: cmd("/ proxy 3000"),
|
||||
command: cmd("https:443 / localhost:3000"),
|
||||
want: &ipn.ServeConfig{
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
@@ -412,7 +433,7 @@ func TestServeConfigMutations(t *testing.T) {
|
||||
},
|
||||
})
|
||||
add(step{
|
||||
command: cmd("funnel on"),
|
||||
command: cmd("funnel 443 on"),
|
||||
want: &ipn.ServeConfig{
|
||||
AllowFunnel: map[ipn.HostPort]bool{"foo.test.ts.net:443": true},
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
||||
@@ -424,7 +445,7 @@ func TestServeConfigMutations(t *testing.T) {
|
||||
},
|
||||
})
|
||||
add(step{ // serving on secondary port doesn't change funnel
|
||||
command: cmd("--serve-port=8443 /bar proxy 3001"),
|
||||
command: cmd("https:8443 /bar localhost:3001"),
|
||||
want: &ipn.ServeConfig{
|
||||
AllowFunnel: map[ipn.HostPort]bool{"foo.test.ts.net:443": true},
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}, 8443: {HTTPS: true}},
|
||||
@@ -439,7 +460,7 @@ func TestServeConfigMutations(t *testing.T) {
|
||||
},
|
||||
})
|
||||
add(step{ // turn funnel on for secondary port
|
||||
command: cmd("--serve-port=8443 funnel on"),
|
||||
command: cmd("funnel 8443 on"),
|
||||
want: &ipn.ServeConfig{
|
||||
AllowFunnel: map[ipn.HostPort]bool{"foo.test.ts.net:443": true, "foo.test.ts.net:8443": true},
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}, 8443: {HTTPS: true}},
|
||||
@@ -454,7 +475,7 @@ func TestServeConfigMutations(t *testing.T) {
|
||||
},
|
||||
})
|
||||
add(step{ // turn funnel off for primary port 443
|
||||
command: cmd("funnel off"),
|
||||
command: cmd("funnel 443 off"),
|
||||
want: &ipn.ServeConfig{
|
||||
AllowFunnel: map[ipn.HostPort]bool{"foo.test.ts.net:8443": true},
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}, 8443: {HTTPS: true}},
|
||||
@@ -469,7 +490,7 @@ func TestServeConfigMutations(t *testing.T) {
|
||||
},
|
||||
})
|
||||
add(step{ // remove secondary port
|
||||
command: cmd("--serve-port=8443 --remove /bar"),
|
||||
command: cmd("https:8443 /bar off"),
|
||||
want: &ipn.ServeConfig{
|
||||
AllowFunnel: map[ipn.HostPort]bool{"foo.test.ts.net:8443": true},
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
||||
@@ -481,7 +502,7 @@ func TestServeConfigMutations(t *testing.T) {
|
||||
},
|
||||
})
|
||||
add(step{ // start a tcp forwarder on 8443
|
||||
command: cmd("--serve-port=8443 tcp 5432"),
|
||||
command: cmd("tcp:8443 tcp://localhost:5432"),
|
||||
want: &ipn.ServeConfig{
|
||||
AllowFunnel: map[ipn.HostPort]bool{"foo.test.ts.net:8443": true},
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}, 8443: {TCPForward: "127.0.0.1:5432"}},
|
||||
@@ -493,27 +514,27 @@ func TestServeConfigMutations(t *testing.T) {
|
||||
},
|
||||
})
|
||||
add(step{ // remove primary port http handler
|
||||
command: cmd("--remove /"),
|
||||
command: cmd("https:443 / off"),
|
||||
want: &ipn.ServeConfig{
|
||||
AllowFunnel: map[ipn.HostPort]bool{"foo.test.ts.net:8443": true},
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{8443: {TCPForward: "127.0.0.1:5432"}},
|
||||
},
|
||||
})
|
||||
add(step{ // remove tcp forwarder
|
||||
command: cmd("--serve-port=8443 --remove tcp 5432"),
|
||||
command: cmd("tls-terminated-tcp:8443 off"),
|
||||
want: &ipn.ServeConfig{
|
||||
AllowFunnel: map[ipn.HostPort]bool{"foo.test.ts.net:8443": true},
|
||||
},
|
||||
})
|
||||
add(step{ // turn off funnel
|
||||
command: cmd("--serve-port=8443 funnel off"),
|
||||
command: cmd("funnel 8443 off"),
|
||||
want: &ipn.ServeConfig{},
|
||||
})
|
||||
|
||||
// tricky steps
|
||||
add(step{reset: true})
|
||||
add(step{ // a directory with a trailing slash mount point
|
||||
command: cmd("/dir path " + filepath.Join(td, "subdir")),
|
||||
command: cmd("https:443 /dir " + filepath.Join(td, "subdir")),
|
||||
want: &ipn.ServeConfig{
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
@@ -524,7 +545,7 @@ func TestServeConfigMutations(t *testing.T) {
|
||||
},
|
||||
})
|
||||
add(step{ // this should overwrite the previous one
|
||||
command: cmd("/dir path " + filepath.Join(td, "foo")),
|
||||
command: cmd("https:443 /dir " + filepath.Join(td, "foo")),
|
||||
want: &ipn.ServeConfig{
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
@@ -536,7 +557,7 @@ func TestServeConfigMutations(t *testing.T) {
|
||||
})
|
||||
add(step{reset: true}) // reset and do the opposite
|
||||
add(step{ // a file without a trailing slash mount point
|
||||
command: cmd("/dir path " + filepath.Join(td, "foo")),
|
||||
command: cmd("https:443 /dir " + filepath.Join(td, "foo")),
|
||||
want: &ipn.ServeConfig{
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
@@ -547,7 +568,7 @@ func TestServeConfigMutations(t *testing.T) {
|
||||
},
|
||||
})
|
||||
add(step{ // this should overwrite the previous one
|
||||
command: cmd("/dir path " + filepath.Join(td, "subdir")),
|
||||
command: cmd("https:443 /dir " + filepath.Join(td, "subdir")),
|
||||
want: &ipn.ServeConfig{
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
@@ -560,37 +581,24 @@ func TestServeConfigMutations(t *testing.T) {
|
||||
|
||||
// error states
|
||||
add(step{reset: true})
|
||||
add(step{ // make sure we can't add "tcp" as if it was a mount
|
||||
command: cmd("tcp text foo"),
|
||||
wantErr: exactErr(flag.ErrHelp, "flag.ErrHelp"),
|
||||
})
|
||||
add(step{ // "/tcp" is fine though as a mount
|
||||
command: cmd("/tcp text foo"),
|
||||
want: &ipn.ServeConfig{
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
"foo.test.ts.net:443": {Handlers: map[string]*ipn.HTTPHandler{
|
||||
"/tcp": {Text: "foo"},
|
||||
}},
|
||||
},
|
||||
},
|
||||
})
|
||||
add(step{reset: true})
|
||||
add(step{ // tcp forward 5432 on serve port 443
|
||||
command: cmd("tcp 5432"),
|
||||
command: cmd("tls-terminated-tcp:443 tcp://localhost:5432"),
|
||||
want: &ipn.ServeConfig{
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{
|
||||
443: {TCPForward: "127.0.0.1:5432"},
|
||||
443: {
|
||||
TCPForward: "127.0.0.1:5432",
|
||||
TerminateTLS: "foo.test.ts.net",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
add(step{ // try to start a web handler on the same port
|
||||
command: cmd("/ proxy 3000"),
|
||||
command: cmd("https:443 / localhost:3000"),
|
||||
wantErr: exactErr(flag.ErrHelp, "flag.ErrHelp"),
|
||||
})
|
||||
add(step{reset: true})
|
||||
add(step{ // start a web handler on port 443
|
||||
command: cmd("/ proxy 3000"),
|
||||
command: cmd("https:443 / localhost:3000"),
|
||||
want: &ipn.ServeConfig{
|
||||
TCP: map[uint16]*ipn.TCPPortHandler{443: {HTTPS: true}},
|
||||
Web: map[ipn.HostPort]*ipn.WebServerConfig{
|
||||
@@ -600,14 +608,17 @@ func TestServeConfigMutations(t *testing.T) {
|
||||
},
|
||||
},
|
||||
})
|
||||
add(step{ // try to start a tcp forwarder on the same serve port (443 default)
|
||||
command: cmd("tcp 5432"),
|
||||
add(step{ // try to start a tcp forwarder on the same serve port
|
||||
command: cmd("tls-terminated-tcp:443 tcp://localhost:5432"),
|
||||
wantErr: anyErr(),
|
||||
})
|
||||
|
||||
lc := &fakeLocalServeClient{}
|
||||
// And now run the steps above.
|
||||
for i, st := range steps {
|
||||
if st.debugBreak != nil {
|
||||
st.debugBreak()
|
||||
}
|
||||
if st.reset {
|
||||
t.Logf("Executing step #%d, line %v: [reset]", i, st.line)
|
||||
lc.config = nil
|
||||
@@ -625,8 +636,16 @@ func TestServeConfigMutations(t *testing.T) {
|
||||
testStdout: &stdout,
|
||||
}
|
||||
lastCount := lc.setCount
|
||||
cmd := newServeCommand(e)
|
||||
err := cmd.ParseAndRun(context.Background(), st.command)
|
||||
var cmd *ffcli.Command
|
||||
var args []string
|
||||
if st.command[0] == "funnel" {
|
||||
cmd = newFunnelCommand(e)
|
||||
args = st.command[1:]
|
||||
} else {
|
||||
cmd = newServeCommand(e)
|
||||
args = st.command
|
||||
}
|
||||
err := cmd.ParseAndRun(context.Background(), args)
|
||||
if flagOut.Len() > 0 {
|
||||
t.Logf("flag package output: %q", flagOut.Bytes())
|
||||
}
|
||||
@@ -677,7 +696,7 @@ var fakeStatus = &ipnstate.Status{
|
||||
BackendState: ipn.Running.String(),
|
||||
Self: &ipnstate.PeerStatus{
|
||||
DNSName: "foo.test.ts.net",
|
||||
Capabilities: []string{tailcfg.NodeAttrFunnel},
|
||||
Capabilities: []string{tailcfg.NodeAttrFunnel, tailcfg.CapabilityFunnelPorts + "?ports=443,8443"},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -717,7 +736,5 @@ func anyErr() func(error) string {
|
||||
}
|
||||
|
||||
func cmd(s string) []string {
|
||||
cmds := strings.Fields(s)
|
||||
fmt.Printf("cmd: %v", cmds)
|
||||
return cmds
|
||||
return strings.Fields(s)
|
||||
}
|
||||
|
||||
@@ -258,6 +258,7 @@ func printFunnelStatus(ctx context.Context) {
|
||||
}
|
||||
printf("# - %s\n", url)
|
||||
}
|
||||
outln()
|
||||
}
|
||||
|
||||
// isRunningOrStarting reports whether st is in state Running or Starting.
|
||||
@@ -275,7 +276,7 @@ func isRunningOrStarting(st *ipnstate.Status) (description string, ok bool) {
|
||||
}
|
||||
return s, false
|
||||
case ipn.NeedsMachineAuth.String():
|
||||
return "Machine is not yet authorized by tailnet admin.", false
|
||||
return "Machine is not yet approved by tailnet admin.", false
|
||||
case ipn.Running.String(), ipn.Starting.String():
|
||||
return st.BackendState, 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()
|
||||
@@ -409,6 +410,12 @@ func updatePrefs(prefs, curPrefs *ipn.Prefs, env upCheckEnv) (simpleUp bool, jus
|
||||
return false, nil, err
|
||||
}
|
||||
|
||||
if env.upArgs.forceReauth && isSSHOverTailscale() {
|
||||
if err := presentRiskToUser(riskLoseSSH, `You are connected over Tailscale; this action will result in your SSH session disconnecting.`, env.upArgs.acceptedRisks); err != nil {
|
||||
return false, nil, err
|
||||
}
|
||||
}
|
||||
|
||||
tagsChanged := !reflect.DeepEqual(curPrefs.AdvertiseTags, prefs.AdvertiseTags)
|
||||
|
||||
simpleUp = env.flagSet.NFlag() == 0 &&
|
||||
@@ -584,7 +591,7 @@ func runUp(ctx context.Context, cmd string, args []string, upArgs upArgsT) (retE
|
||||
if env.upArgs.json {
|
||||
printUpDoneJSON(ipn.NeedsMachineAuth, "")
|
||||
} else {
|
||||
fmt.Fprintf(Stderr, "\nTo authorize your machine, visit (as admin):\n\n\t%s\n\n", prefs.AdminPageURL())
|
||||
fmt.Fprintf(Stderr, "\nTo approve your machine, visit (as admin):\n\n\t%s\n\n", prefs.AdminPageURL())
|
||||
}
|
||||
case ipn.Running:
|
||||
// Done full authentication process
|
||||
|
||||
@@ -145,11 +145,11 @@ func newUpdater() (*updater, error) {
|
||||
case strings.HasSuffix(os.Getenv("HOME"), "/io.tailscale.ipn.macsys/Data"):
|
||||
up.update = up.updateMacSys
|
||||
default:
|
||||
return nil, errors.New("This is the macOS App Store version of Tailscale; update in the App Store, or see https://tailscale.com/kb/1083/install-unstable/ to use TestFlight or to install the non-App Store version")
|
||||
return nil, errors.New("This is the macOS App Store version of Tailscale; update in the App Store, or see https://tailscale.com/s/unstable-clients to use TestFlight or to install the non-App Store version")
|
||||
}
|
||||
}
|
||||
if up.update == nil {
|
||||
return nil, errors.New("The 'update' command is not supported on this platform; see https://tailscale.com/kb/1067/update/")
|
||||
return nil, errors.New("The 'update' command is not supported on this platform; see https://tailscale.com/s/client-updates")
|
||||
}
|
||||
return up, nil
|
||||
}
|
||||
|
||||
@@ -228,33 +228,48 @@ func qnapAuthn(r *http.Request) (string, *qnapAuthResponse, error) {
|
||||
return "", nil, fmt.Errorf("not authenticated by any mechanism")
|
||||
}
|
||||
|
||||
// qnapAuthnURL returns the auth URL to use by inferring where the UI is
|
||||
// running based on the request URL. This is necessary because QNAP has so
|
||||
// many options, see https://github.com/tailscale/tailscale/issues/7108
|
||||
// and https://github.com/tailscale/tailscale/issues/6903
|
||||
func qnapAuthnURL(requestUrl string, query url.Values) string {
|
||||
in, err := url.Parse(requestUrl)
|
||||
scheme := ""
|
||||
host := ""
|
||||
if err != nil || in.Scheme == "" {
|
||||
log.Printf("Cannot parse QNAP login URL %v", err)
|
||||
|
||||
// try localhost and hope for the best
|
||||
scheme = "http"
|
||||
host = "localhost"
|
||||
} else {
|
||||
scheme = in.Scheme
|
||||
host = in.Host
|
||||
}
|
||||
|
||||
u := url.URL{
|
||||
Scheme: scheme,
|
||||
Host: host,
|
||||
Path: "/cgi-bin/authLogin.cgi",
|
||||
RawQuery: query.Encode(),
|
||||
}
|
||||
|
||||
return u.String()
|
||||
}
|
||||
|
||||
func qnapAuthnQtoken(r *http.Request, user, token string) (string, *qnapAuthResponse, error) {
|
||||
query := url.Values{
|
||||
"qtoken": []string{token},
|
||||
"user": []string{user},
|
||||
}
|
||||
u := url.URL{
|
||||
Scheme: "http",
|
||||
Host: "127.0.0.1:8080",
|
||||
Path: "/cgi-bin/authLogin.cgi",
|
||||
RawQuery: query.Encode(),
|
||||
}
|
||||
|
||||
return qnapAuthnFinish(user, u.String())
|
||||
return qnapAuthnFinish(user, qnapAuthnURL(r.URL.String(), query))
|
||||
}
|
||||
|
||||
func qnapAuthnSid(r *http.Request, user, sid string) (string, *qnapAuthResponse, error) {
|
||||
query := url.Values{
|
||||
"sid": []string{sid},
|
||||
}
|
||||
u := url.URL{
|
||||
Scheme: "http",
|
||||
Host: "127.0.0.1:8080",
|
||||
Path: "/cgi-bin/authLogin.cgi",
|
||||
RawQuery: query.Encode(),
|
||||
}
|
||||
|
||||
return qnapAuthnFinish(user, u.String())
|
||||
return qnapAuthnFinish(user, qnapAuthnURL(r.URL.String(), query))
|
||||
}
|
||||
|
||||
func qnapAuthnFinish(user, url string) (string, *qnapAuthResponse, error) {
|
||||
|
||||
@@ -3,7 +3,10 @@
|
||||
|
||||
package cli
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"net/url"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestUrlOfListenAddr(t *testing.T) {
|
||||
tests := []struct {
|
||||
@@ -34,9 +37,64 @@ func TestUrlOfListenAddr(t *testing.T) {
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
url := urlOfListenAddr(tt.in)
|
||||
if url != tt.want {
|
||||
t.Errorf("expected url: %q, got: %q", tt.want, url)
|
||||
u := urlOfListenAddr(tt.in)
|
||||
if u != tt.want {
|
||||
t.Errorf("expected url: %q, got: %q", tt.want, u)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestQnapAuthnURL(t *testing.T) {
|
||||
query := url.Values{
|
||||
"qtoken": []string{"token"},
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
in string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "localhost http",
|
||||
in: "http://localhost:8088/",
|
||||
want: "http://localhost:8088/cgi-bin/authLogin.cgi?qtoken=token",
|
||||
},
|
||||
{
|
||||
name: "localhost https",
|
||||
in: "https://localhost:5000/",
|
||||
want: "https://localhost:5000/cgi-bin/authLogin.cgi?qtoken=token",
|
||||
},
|
||||
{
|
||||
name: "IP http",
|
||||
in: "http://10.1.20.4:80/",
|
||||
want: "http://10.1.20.4:80/cgi-bin/authLogin.cgi?qtoken=token",
|
||||
},
|
||||
{
|
||||
name: "IP6 https",
|
||||
in: "https://[ff7d:0:1:2::1]/",
|
||||
want: "https://[ff7d:0:1:2::1]/cgi-bin/authLogin.cgi?qtoken=token",
|
||||
},
|
||||
{
|
||||
name: "hostname https",
|
||||
in: "https://qnap.example.com/",
|
||||
want: "https://qnap.example.com/cgi-bin/authLogin.cgi?qtoken=token",
|
||||
},
|
||||
{
|
||||
name: "invalid URL",
|
||||
in: "This is not a URL, it is a really really really really really really really really really really really really long string to exercise the URL truncation code in the error path.",
|
||||
want: "http://localhost/cgi-bin/authLogin.cgi?qtoken=token",
|
||||
},
|
||||
{
|
||||
name: "err != nil",
|
||||
in: "http://192.168.0.%31/",
|
||||
want: "http://localhost/cgi-bin/authLogin.cgi?qtoken=token",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
u := qnapAuthnURL(tt.in, query)
|
||||
if u != tt.want {
|
||||
t.Errorf("expected url: %q, got: %q", tt.want, u)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -79,6 +79,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
tailscale.com/net/packet from tailscale.com/wgengine/filter
|
||||
tailscale.com/net/ping from tailscale.com/net/netcheck
|
||||
tailscale.com/net/portmapper from tailscale.com/net/netcheck+
|
||||
tailscale.com/net/sockstats from tailscale.com/control/controlhttp+
|
||||
tailscale.com/net/stun from tailscale.com/net/netcheck
|
||||
tailscale.com/net/tlsdial from tailscale.com/derp/derphttp+
|
||||
tailscale.com/net/tsaddr from tailscale.com/net/interfaces+
|
||||
@@ -91,7 +92,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+
|
||||
@@ -121,6 +122,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
tailscale.com/util/quarantine from tailscale.com/cmd/tailscale/cli
|
||||
tailscale.com/util/set from tailscale.com/health+
|
||||
tailscale.com/util/singleflight from tailscale.com/net/dnscache
|
||||
tailscale.com/util/slicesx from tailscale.com/net/dnscache+
|
||||
💣 tailscale.com/util/winutil from tailscale.com/hostinfo+
|
||||
tailscale.com/version from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/version/distro from tailscale.com/cmd/tailscale/cli+
|
||||
|
||||
@@ -6,4 +6,3 @@ package main
|
||||
//go:generate go run tailscale.com/cmd/mkmanifest amd64 windows-manifest.xml manifest_windows_amd64.syso
|
||||
//go:generate go run tailscale.com/cmd/mkmanifest 386 windows-manifest.xml manifest_windows_386.syso
|
||||
//go:generate go run tailscale.com/cmd/mkmanifest arm64 windows-manifest.xml manifest_windows_arm64.syso
|
||||
//go:generate go run tailscale.com/cmd/mkmanifest arm windows-manifest.xml manifest_windows_arm.syso
|
||||
|
||||
Binary file not shown.
@@ -14,24 +14,18 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptrace"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"tailscale.com/derp/derphttp"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/net/interfaces"
|
||||
"tailscale.com/net/portmapper"
|
||||
"tailscale.com/net/tshttpproxy"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/wgengine/monitor"
|
||||
)
|
||||
|
||||
@@ -229,95 +223,5 @@ func checkDerp(ctx context.Context, derpRegion string) (err error) {
|
||||
}
|
||||
|
||||
func debugPortmap(ctx context.Context) error {
|
||||
ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
|
||||
defer cancel()
|
||||
|
||||
portmapper.VerboseLogs = true
|
||||
switch envknob.String("TS_DEBUG_PORTMAP_TYPE") {
|
||||
case "":
|
||||
case "pmp":
|
||||
portmapper.DisablePCP = true
|
||||
portmapper.DisableUPnP = true
|
||||
case "pcp":
|
||||
portmapper.DisablePMP = true
|
||||
portmapper.DisableUPnP = true
|
||||
case "upnp":
|
||||
portmapper.DisablePCP = true
|
||||
portmapper.DisablePMP = true
|
||||
default:
|
||||
log.Fatalf("TS_DEBUG_PORTMAP_TYPE must be one of pmp,pcp,upnp")
|
||||
}
|
||||
|
||||
done := make(chan bool, 1)
|
||||
|
||||
var c *portmapper.Client
|
||||
logf := log.Printf
|
||||
c = portmapper.NewClient(logger.WithPrefix(logf, "portmapper: "), func() {
|
||||
logf("portmapping changed.")
|
||||
logf("have mapping: %v", c.HaveMapping())
|
||||
|
||||
if ext, ok := c.GetCachedMappingOrStartCreatingOne(); ok {
|
||||
logf("cb: mapping: %v", ext)
|
||||
select {
|
||||
case done <- true:
|
||||
default:
|
||||
}
|
||||
return
|
||||
}
|
||||
logf("cb: no mapping")
|
||||
})
|
||||
linkMon, err := monitor.New(logger.WithPrefix(logf, "monitor: "))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
gatewayAndSelfIP := func() (gw, self netip.Addr, ok bool) {
|
||||
if v := os.Getenv("TS_DEBUG_GW_SELF"); strings.Contains(v, "/") {
|
||||
i := strings.Index(v, "/")
|
||||
gw = netip.MustParseAddr(v[:i])
|
||||
self = netip.MustParseAddr(v[i+1:])
|
||||
return gw, self, true
|
||||
}
|
||||
return linkMon.GatewayAndSelfIP()
|
||||
}
|
||||
|
||||
c.SetGatewayLookupFunc(gatewayAndSelfIP)
|
||||
|
||||
gw, selfIP, ok := gatewayAndSelfIP()
|
||||
if !ok {
|
||||
logf("no gateway or self IP; %v", linkMon.InterfaceState())
|
||||
return nil
|
||||
}
|
||||
logf("gw=%v; self=%v", gw, selfIP)
|
||||
|
||||
uc, err := net.ListenPacket("udp", "0.0.0.0:0")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer uc.Close()
|
||||
c.SetLocalPort(uint16(uc.LocalAddr().(*net.UDPAddr).Port))
|
||||
|
||||
res, err := c.Probe(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Probe: %v", err)
|
||||
}
|
||||
logf("Probe: %+v", res)
|
||||
|
||||
if !res.PCP && !res.PMP && !res.UPnP {
|
||||
logf("no portmapping services available")
|
||||
return nil
|
||||
}
|
||||
|
||||
if ext, ok := c.GetCachedMappingOrStartCreatingOne(); ok {
|
||||
logf("mapping: %v", ext)
|
||||
} else {
|
||||
logf("no mapping")
|
||||
}
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
return nil
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
return fmt.Errorf("this flag has been deprecated in favour of 'tailscale debug portmap'")
|
||||
}
|
||||
|
||||
@@ -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+
|
||||
@@ -212,17 +214,18 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/ipn/ipnstate from tailscale.com/control/controlclient+
|
||||
tailscale.com/ipn/localapi from tailscale.com/ipn/ipnserver
|
||||
tailscale.com/ipn/policy from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/ipn/store from tailscale.com/cmd/tailscaled
|
||||
tailscale.com/ipn/store from tailscale.com/cmd/tailscaled+
|
||||
L tailscale.com/ipn/store/awsstore from tailscale.com/ipn/store
|
||||
L tailscale.com/ipn/store/kubestore from tailscale.com/ipn/store
|
||||
tailscale.com/ipn/store/mem from tailscale.com/ipn/store+
|
||||
L tailscale.com/kube from tailscale.com/ipn/store/kubestore
|
||||
tailscale.com/log/filelogger from tailscale.com/logpolicy
|
||||
tailscale.com/log/logheap from tailscale.com/control/controlclient
|
||||
tailscale.com/log/sockstatlog from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/logpolicy from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/logtail from tailscale.com/control/controlclient+
|
||||
tailscale.com/logtail/backoff from tailscale.com/control/controlclient+
|
||||
tailscale.com/logtail/filch from tailscale.com/logpolicy
|
||||
tailscale.com/logtail/filch from tailscale.com/logpolicy+
|
||||
tailscale.com/metrics from tailscale.com/derp+
|
||||
tailscale.com/net/connstats from tailscale.com/net/tstun+
|
||||
tailscale.com/net/dns from tailscale.com/ipn/ipnlocal+
|
||||
@@ -246,12 +249,14 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/net/proxymux from tailscale.com/cmd/tailscaled
|
||||
tailscale.com/net/routetable from tailscale.com/doctor/routetable
|
||||
tailscale.com/net/socks5 from tailscale.com/cmd/tailscaled
|
||||
tailscale.com/net/sockstats from tailscale.com/control/controlclient+
|
||||
tailscale.com/net/stun from tailscale.com/net/netcheck+
|
||||
tailscale.com/net/tlsdial from tailscale.com/control/controlclient+
|
||||
tailscale.com/net/tsaddr from tailscale.com/ipn+
|
||||
tailscale.com/net/tsdial from tailscale.com/control/controlclient+
|
||||
💣 tailscale.com/net/tshttpproxy from tailscale.com/control/controlclient+
|
||||
tailscale.com/net/tstun from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/net/tstun/table from tailscale.com/net/tstun
|
||||
tailscale.com/net/wsconn from tailscale.com/control/controlhttp+
|
||||
tailscale.com/paths from tailscale.com/ipn/ipnlocal+
|
||||
💣 tailscale.com/portlist from tailscale.com/ipn/ipnlocal
|
||||
@@ -260,12 +265,13 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
LD 💣 tailscale.com/ssh/tailssh from tailscale.com/cmd/tailscaled
|
||||
tailscale.com/syncs from tailscale.com/net/netcheck+
|
||||
tailscale.com/tailcfg from tailscale.com/client/tailscale/apitype+
|
||||
💣 tailscale.com/tempfork/device from tailscale.com/net/tstun/table
|
||||
LD tailscale.com/tempfork/gliderlabs/ssh from tailscale.com/ssh/tailssh
|
||||
tailscale.com/tka from tailscale.com/ipn/ipnlocal+
|
||||
W tailscale.com/tsconst from tailscale.com/net/interfaces
|
||||
tailscale.com/tstime from tailscale.com/wgengine/magicsock
|
||||
💣 tailscale.com/tstime/mono from tailscale.com/net/tstun+
|
||||
tailscale.com/tstime/rate from tailscale.com/wgengine/filter
|
||||
tailscale.com/tstime/rate from tailscale.com/wgengine/filter+
|
||||
tailscale.com/tsweb from tailscale.com/cmd/tailscaled
|
||||
tailscale.com/types/dnstype from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/types/empty from tailscale.com/control/controlclient+
|
||||
@@ -298,15 +304,20 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/util/lineread from tailscale.com/hostinfo+
|
||||
tailscale.com/util/mak from tailscale.com/control/controlclient+
|
||||
tailscale.com/util/multierr from tailscale.com/control/controlclient+
|
||||
tailscale.com/util/must from tailscale.com/logpolicy
|
||||
tailscale.com/util/osshare from tailscale.com/ipn/ipnlocal+
|
||||
W tailscale.com/util/pidowner from tailscale.com/ipn/ipnauth
|
||||
tailscale.com/util/racebuild from tailscale.com/logpolicy
|
||||
tailscale.com/util/ringbuffer from tailscale.com/wgengine/magicsock
|
||||
tailscale.com/util/set from tailscale.com/health+
|
||||
tailscale.com/util/singleflight from tailscale.com/control/controlclient+
|
||||
tailscale.com/util/slicesx from tailscale.com/net/dnscache+
|
||||
tailscale.com/util/sysresources from tailscale.com/wgengine/magicsock
|
||||
tailscale.com/util/systemd from tailscale.com/control/controlclient+
|
||||
tailscale.com/util/uniq from tailscale.com/wgengine/magicsock+
|
||||
tailscale.com/util/vizerror from tailscale.com/tsweb
|
||||
💣 tailscale.com/util/winutil from tailscale.com/control/controlclient+
|
||||
W tailscale.com/util/winutil/policy from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/version from tailscale.com/derp+
|
||||
tailscale.com/version/distro from tailscale.com/hostinfo+
|
||||
W tailscale.com/wf from tailscale.com/cmd/tailscaled
|
||||
@@ -340,13 +351,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+
|
||||
@@ -410,7 +421,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
encoding/xml from github.com/tailscale/goupnp+
|
||||
errors from bufio+
|
||||
expvar from tailscale.com/derp+
|
||||
flag from tailscale.com/control/controlclient+
|
||||
flag from net/http/httptest+
|
||||
fmt from compress/flate+
|
||||
hash from crypto+
|
||||
hash/adler32 from tailscale.com/ipn/ipnlocal
|
||||
|
||||
@@ -6,4 +6,3 @@ package main
|
||||
//go:generate go run tailscale.com/cmd/mkmanifest amd64 windows-manifest.xml manifest_windows_amd64.syso
|
||||
//go:generate go run tailscale.com/cmd/mkmanifest 386 windows-manifest.xml manifest_windows_386.syso
|
||||
//go:generate go run tailscale.com/cmd/mkmanifest arm64 windows-manifest.xml manifest_windows_arm64.syso
|
||||
//go:generate go run tailscale.com/cmd/mkmanifest arm windows-manifest.xml manifest_windows_arm.syso
|
||||
|
||||
Binary file not shown.
@@ -43,6 +43,7 @@ import (
|
||||
"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"
|
||||
@@ -51,6 +52,7 @@ import (
|
||||
"tailscale.com/tsweb"
|
||||
"tailscale.com/types/flagtype"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/logid"
|
||||
"tailscale.com/util/clientmetric"
|
||||
"tailscale.com/util/multierr"
|
||||
"tailscale.com/util/osshare"
|
||||
@@ -377,11 +379,10 @@ func run() error {
|
||||
debugMux = newDebugMux()
|
||||
}
|
||||
|
||||
logid := pol.PublicID.String()
|
||||
return startIPNServer(context.Background(), logf, logid)
|
||||
return startIPNServer(context.Background(), logf, pol.PublicID)
|
||||
}
|
||||
|
||||
func startIPNServer(ctx context.Context, logf logger.Logf, logid string) error {
|
||||
func startIPNServer(ctx context.Context, logf logger.Logf, logID logid.PublicID) error {
|
||||
ln, err := safesocket.Listen(args.socketpath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("safesocket.Listen: %v", err)
|
||||
@@ -407,7 +408,7 @@ func startIPNServer(ctx context.Context, logf logger.Logf, logid string) error {
|
||||
}
|
||||
}()
|
||||
|
||||
srv := ipnserver.New(logf, logid)
|
||||
srv := ipnserver.New(logf, logID)
|
||||
if debugMux != nil {
|
||||
debugMux.HandleFunc("/debug/ipn", srv.ServeHTMLStatus)
|
||||
}
|
||||
@@ -425,7 +426,7 @@ func startIPNServer(ctx context.Context, logf logger.Logf, logid string) error {
|
||||
return
|
||||
}
|
||||
}
|
||||
lb, err := getLocalBackend(ctx, logf, logid)
|
||||
lb, err := getLocalBackend(ctx, logf, logID)
|
||||
if err == nil {
|
||||
logf("got LocalBackend in %v", time.Since(t0).Round(time.Millisecond))
|
||||
srv.SetLocalBackend(lb)
|
||||
@@ -449,7 +450,7 @@ func startIPNServer(ctx context.Context, logf logger.Logf, logid string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func getLocalBackend(ctx context.Context, logf logger.Logf, logid string) (_ *ipnlocal.LocalBackend, retErr error) {
|
||||
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)
|
||||
@@ -494,11 +495,13 @@ func getLocalBackend(ctx context.Context, logf logger.Logf, logid string) (_ *ip
|
||||
}
|
||||
}
|
||||
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 +511,9 @@ func getLocalBackend(ctx context.Context, logf logger.Logf, logid string) (_ *ip
|
||||
go func() {
|
||||
log.Fatalf("SOCKS5 server exited: %v", ss.Serve(socksListener))
|
||||
}()
|
||||
addrs = append(addrs, socksListener.Addr().String())
|
||||
}
|
||||
tshttpproxy.SetSelfProxy(addrs...)
|
||||
}
|
||||
|
||||
e = wgengine.NewWatchdog(e)
|
||||
@@ -520,7 +525,7 @@ func getLocalBackend(ctx context.Context, logf logger.Logf, logid string) (_ *ip
|
||||
return nil, fmt.Errorf("store.New: %w", err)
|
||||
}
|
||||
|
||||
lb, err := ipnlocal.NewLocalBackend(logf, logid, store, dialer, e, opts.LoginFlags)
|
||||
lb, err := ipnlocal.NewLocalBackend(logf, logID, store, dialer, e, opts.LoginFlags)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ipnlocal.NewLocalBackend: %w", err)
|
||||
}
|
||||
|
||||
@@ -47,6 +47,7 @@ import (
|
||||
"tailscale.com/net/dns"
|
||||
"tailscale.com/net/tstun"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/logid"
|
||||
"tailscale.com/util/winutil"
|
||||
"tailscale.com/version"
|
||||
"tailscale.com/wf"
|
||||
@@ -262,13 +263,13 @@ func beWindowsSubprocess() bool {
|
||||
if len(os.Args) != 3 || os.Args[1] != "/subproc" {
|
||||
return false
|
||||
}
|
||||
logid := os.Args[2]
|
||||
logID := os.Args[2]
|
||||
|
||||
// Remove the date/time prefix; the logtail + file loggers add it.
|
||||
log.SetFlags(0)
|
||||
|
||||
log.Printf("Program starting: v%v: %#v", version.Long(), os.Args)
|
||||
log.Printf("subproc mode: logid=%v", logid)
|
||||
log.Printf("subproc mode: logid=%v", logID)
|
||||
if err := envknob.ApplyDiskConfigError(); err != nil {
|
||||
log.Printf("Error reading environment config: %v", err)
|
||||
}
|
||||
@@ -290,7 +291,8 @@ func beWindowsSubprocess() bool {
|
||||
}
|
||||
}()
|
||||
|
||||
err := startIPNServer(ctx, log.Printf, logid)
|
||||
publicLogID, _ := logid.ParsePublicID(logID)
|
||||
err := startIPNServer(ctx, log.Printf, publicLogID)
|
||||
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
|
||||
|
||||
@@ -38,13 +38,31 @@ class App extends Component<{}, AppState> {
|
||||
if (ipnState === "NeedsMachineAuth") {
|
||||
machineAuthInstructions = (
|
||||
<div class="container mx-auto px-4 text-center">
|
||||
An administrator needs to authorize this device.
|
||||
An administrator needs to approve this device.
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const lockedOut = netMap?.lockedOut
|
||||
let lockedOutInstructions
|
||||
if (lockedOut) {
|
||||
lockedOutInstructions = (
|
||||
<div class="container mx-auto px-4 text-center space-y-4">
|
||||
<p>This instance of Tailscale Connect needs to be signed, due to
|
||||
{" "}<a href="https://tailscale.com/kb/1226/tailnet-lock/" class="link">tailnet lock</a>{" "}
|
||||
being enabled on this domain.
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Run the following command on a device with a trusted tailnet lock key:
|
||||
<pre>tailscale lock sign {netMap.self.nodeKey}</pre>
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
let ssh
|
||||
if (ipn && ipnState === "Running" && netMap) {
|
||||
if (ipn && ipnState === "Running" && netMap && !lockedOut) {
|
||||
ssh = <SSH netMap={netMap} ipn={ipn} />
|
||||
}
|
||||
|
||||
@@ -55,6 +73,7 @@ class App extends Component<{}, AppState> {
|
||||
<div class="flex-grow flex flex-col justify-center overflow-hidden">
|
||||
{urlDisplay}
|
||||
{machineAuthInstructions}
|
||||
{lockedOutInstructions}
|
||||
{ssh}
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -30,7 +30,7 @@ const STATE_LABELS = {
|
||||
NoState: "Initializing…",
|
||||
InUseOtherUser: "In-use by another user",
|
||||
NeedsLogin: "Needs login",
|
||||
NeedsMachineAuth: "Needs authorization",
|
||||
NeedsMachineAuth: "Needs approval",
|
||||
Stopped: "Stopped",
|
||||
Starting: "Starting…",
|
||||
Running: "Running",
|
||||
|
||||
@@ -60,11 +60,11 @@ function SSHSession({
|
||||
function NoSSHPeers() {
|
||||
return (
|
||||
<div class="container mx-auto px-4 text-center">
|
||||
None of your machines have
|
||||
None of your machines have{" "}
|
||||
<a href="https://tailscale.com/kb/1193/tailscale-ssh/" class="link">
|
||||
Tailscale SSH
|
||||
</a>
|
||||
enabled. Give it a try!
|
||||
{" "}enabled. Give it a try!
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
1
cmd/tsconnect/src/types/wasm_js.d.ts
vendored
1
cmd/tsconnect/src/types/wasm_js.d.ts
vendored
@@ -63,6 +63,7 @@ declare global {
|
||||
type IPNNetMap = {
|
||||
self: IPNNetMapSelfNode
|
||||
peers: IPNNetMapPeerNode[]
|
||||
lockedOut: boolean
|
||||
}
|
||||
|
||||
type IPNNetMapNode = {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -122,7 +122,7 @@ func newIPN(jsConfig js.Value) map[string]any {
|
||||
return ns.DialContextTCP(ctx, dst)
|
||||
}
|
||||
|
||||
logid := lpc.PublicID.String()
|
||||
logid := lpc.PublicID
|
||||
srv := ipnserver.New(logf, logid)
|
||||
lb, err := ipnlocal.NewLocalBackend(logf, logid, store, dialer, eng, controlclient.LoginEphemeral)
|
||||
if err != nil {
|
||||
@@ -272,6 +272,7 @@ func (i *jsIPN) run(jsCallbacks js.Value) {
|
||||
TailscaleSSHEnabled: p.Hostinfo.TailscaleSSHEnabled(),
|
||||
}
|
||||
}),
|
||||
LockedOut: nm.TKAEnabled && len(nm.SelfNode.KeySignature) == 0,
|
||||
}
|
||||
if jsonNetMap, err := json.Marshal(jsNetMap); err == nil {
|
||||
jsCallbacks.Call("notifyNetMap", string(jsonNetMap))
|
||||
@@ -521,8 +522,9 @@ func (w termWriter) Write(p []byte) (n int, err error) {
|
||||
}
|
||||
|
||||
type jsNetMap struct {
|
||||
Self jsNetMapSelfNode `json:"self"`
|
||||
Peers []jsNetMapPeerNode `json:"peers"`
|
||||
Self jsNetMapSelfNode `json:"self"`
|
||||
Peers []jsNetMapPeerNode `json:"peers"`
|
||||
LockedOut bool `json:"lockedOut"`
|
||||
}
|
||||
|
||||
type jsNetMapNode struct {
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
|
||||
"tailscale.com/health"
|
||||
"tailscale.com/logtail/backoff"
|
||||
"tailscale.com/net/sockstats"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/empty"
|
||||
"tailscale.com/types/key"
|
||||
@@ -58,15 +59,17 @@ type Auto struct {
|
||||
|
||||
mu sync.Mutex // mutex guards the following fields
|
||||
|
||||
paused bool // whether we should stop making HTTP requests
|
||||
unpauseWaiters []chan struct{}
|
||||
loggedIn bool // true if currently logged in
|
||||
loginGoal *LoginGoal // non-nil if some login activity is desired
|
||||
synced bool // true if our netmap is up-to-date
|
||||
inPollNetMap bool // true if currently running a PollNetMap
|
||||
inLiteMapUpdate bool // true if a lite (non-streaming) map request is outstanding
|
||||
inSendStatus int // number of sendStatus calls currently in progress
|
||||
state State
|
||||
paused bool // whether we should stop making HTTP requests
|
||||
unpauseWaiters []chan struct{}
|
||||
loggedIn bool // true if currently logged in
|
||||
loginGoal *LoginGoal // non-nil if some login activity is desired
|
||||
synced bool // true if our netmap is up-to-date
|
||||
inPollNetMap bool // true if currently running a PollNetMap
|
||||
inLiteMapUpdate bool // true if a lite (non-streaming) map request is outstanding
|
||||
liteMapUpdateCancel context.CancelFunc // cancels a lite map update, may be nil
|
||||
liteMapUpdateCancels int // how many times we've canceled a lite map update
|
||||
inSendStatus int // number of sendStatus calls currently in progress
|
||||
state State
|
||||
|
||||
authCtx context.Context // context used for auth requests
|
||||
mapCtx context.Context // context used for netmap requests
|
||||
@@ -118,7 +121,11 @@ 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.mapCtx, c.mapCancel = context.WithCancel(context.Background())
|
||||
c.mapCtx = sockstats.WithSockStats(c.mapCtx, sockstats.LabelControlClientAuto)
|
||||
|
||||
c.unregisterHealthWatch = health.RegisterWatcher(direct.ReportHealthChange)
|
||||
return c, nil
|
||||
|
||||
@@ -163,28 +170,56 @@ func (c *Auto) Start() {
|
||||
func (c *Auto) sendNewMapRequest() {
|
||||
c.mu.Lock()
|
||||
|
||||
// If we're not already streaming a netmap, or if we're already stuck
|
||||
// in a lite update, then tear down everything and start a new stream
|
||||
// (which starts by sending a new map request)
|
||||
if !c.inPollNetMap || c.inLiteMapUpdate || !c.loggedIn {
|
||||
// If we're not already streaming a netmap, then tear down everything
|
||||
// and start a new stream (which starts by sending a new map request)
|
||||
if !c.inPollNetMap || !c.loggedIn {
|
||||
c.mu.Unlock()
|
||||
c.cancelMapSafely()
|
||||
return
|
||||
}
|
||||
|
||||
// If we are already in process of doing a LiteMapUpdate, cancel it and
|
||||
// try a new one. If this is the 10th time we have done this
|
||||
// cancelation, tear down everything and start again.
|
||||
const maxLiteMapUpdateAttempts = 10
|
||||
if c.inLiteMapUpdate {
|
||||
// Always cancel the in-flight lite map update, regardless of
|
||||
// whether we cancel the streaming map request or not.
|
||||
c.liteMapUpdateCancel()
|
||||
c.inLiteMapUpdate = false
|
||||
|
||||
if c.liteMapUpdateCancels >= maxLiteMapUpdateAttempts {
|
||||
// Not making progress
|
||||
c.mu.Unlock()
|
||||
c.cancelMapSafely()
|
||||
return
|
||||
}
|
||||
|
||||
// Increment our cancel counter and continue below to start a
|
||||
// new lite update.
|
||||
c.liteMapUpdateCancels++
|
||||
}
|
||||
|
||||
// Otherwise, send a lite update that doesn't keep a
|
||||
// long-running stream response.
|
||||
defer c.mu.Unlock()
|
||||
c.inLiteMapUpdate = true
|
||||
ctx, cancel := context.WithTimeout(c.mapCtx, 10*time.Second)
|
||||
c.liteMapUpdateCancel = cancel
|
||||
go func() {
|
||||
defer cancel()
|
||||
t0 := time.Now()
|
||||
err := c.direct.SendLiteMapUpdate(ctx)
|
||||
d := time.Since(t0).Round(time.Millisecond)
|
||||
|
||||
c.mu.Lock()
|
||||
c.inLiteMapUpdate = false
|
||||
c.liteMapUpdateCancel = nil
|
||||
if err == nil {
|
||||
c.liteMapUpdateCancels = 0
|
||||
}
|
||||
c.mu.Unlock()
|
||||
|
||||
if err == nil {
|
||||
c.logf("[v1] successful lite map update in %v", d)
|
||||
return
|
||||
@@ -192,10 +227,13 @@ func (c *Auto) sendNewMapRequest() {
|
||||
if ctx.Err() == nil {
|
||||
c.logf("lite map update after %v: %v", d, err)
|
||||
}
|
||||
// Fall back to restarting the long-polling map
|
||||
// request (the old heavy way) if the lite update
|
||||
// failed for any reason.
|
||||
c.cancelMapSafely()
|
||||
if !errors.Is(ctx.Err(), context.Canceled) {
|
||||
// Fall back to restarting the long-polling map
|
||||
// request (the old heavy way) if the lite update
|
||||
// failed for reasons other than the context being
|
||||
// canceled.
|
||||
c.cancelMapSafely()
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
@@ -206,6 +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.mu.Unlock()
|
||||
}
|
||||
@@ -216,6 +255,8 @@ func (c *Auto) cancelMapLocked() {
|
||||
}
|
||||
if !c.closed {
|
||||
c.mapCtx, c.mapCancel = context.WithCancel(context.Background())
|
||||
c.mapCtx = sockstats.WithSockStats(c.mapCtx, sockstats.LabelControlClientAuto)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -229,6 +270,12 @@ func (c *Auto) cancelMapSafely() {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
// Always reset our lite map cancels counter if we're canceling
|
||||
// everything, since we're about to restart with a new map update; this
|
||||
// allows future calls to sendNewMapRequest to retry sending lite
|
||||
// updates.
|
||||
c.liteMapUpdateCancels = 0
|
||||
|
||||
c.logf("[v1] cancelMapSafely: synced=%v", c.synced)
|
||||
|
||||
if c.inPollNetMap {
|
||||
@@ -360,7 +407,13 @@ func (c *Auto) authRoutine() {
|
||||
c.mu.Unlock()
|
||||
|
||||
c.sendStatus("authRoutine-url", err, url, nil)
|
||||
bo.BackOff(ctx, err)
|
||||
if goal.url == url {
|
||||
// The server sent us the same URL we already tried,
|
||||
// backoff to avoid a busy loop.
|
||||
bo.BackOff(ctx, errors.New("login URL not changing"))
|
||||
} else {
|
||||
bo.BackOff(ctx, nil)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
@@ -7,10 +7,11 @@ import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/ed25519"
|
||||
"encoding/base64"
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
@@ -87,16 +88,15 @@ type Direct struct {
|
||||
sfGroup singleflight.Group[struct{}, *NoiseClient] // protects noiseClient creation.
|
||||
noiseClient *NoiseClient
|
||||
|
||||
persist persist.PersistView
|
||||
authKey string
|
||||
tryingNewKey key.NodePrivate
|
||||
expiry *time.Time
|
||||
hostinfo *tailcfg.Hostinfo // always non-nil
|
||||
netinfo *tailcfg.NetInfo
|
||||
endpoints []tailcfg.Endpoint
|
||||
tkaHead string
|
||||
everEndpoints bool // whether we've ever had non-empty endpoints
|
||||
lastPingURL string // last PingRequest.URL received, for dup suppression
|
||||
persist persist.PersistView
|
||||
authKey string
|
||||
tryingNewKey key.NodePrivate
|
||||
expiry *time.Time
|
||||
hostinfo *tailcfg.Hostinfo // always non-nil
|
||||
netinfo *tailcfg.NetInfo
|
||||
endpoints []tailcfg.Endpoint
|
||||
tkaHead string
|
||||
lastPingURL string // last PingRequest.URL received, for dup suppression
|
||||
}
|
||||
|
||||
type Options struct {
|
||||
@@ -212,6 +212,7 @@ func NewDirect(opts Options) (*Direct, error) {
|
||||
Forward: dnscache.Get().Forward, // use default cache's forwarder
|
||||
UseLastGood: true,
|
||||
LookupIPFallback: dnsfallback.Lookup,
|
||||
Logf: opts.Logf,
|
||||
}
|
||||
tr := http.DefaultTransport.(*http.Transport).Clone()
|
||||
tr.Proxy = tshttpproxy.ProxyFromEnvironment
|
||||
@@ -424,7 +425,7 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
|
||||
tryingNewKey := c.tryingNewKey
|
||||
serverKey := c.serverKey
|
||||
serverNoiseKey := c.serverNoiseKey
|
||||
authKey := c.authKey
|
||||
authKey, isWrapped, wrappedSig, wrappedKey := decodeWrappedAuthkey(c.authKey, c.logf)
|
||||
hi := c.hostInfoLocked()
|
||||
backendLogID := hi.BackendLogID
|
||||
expired := c.expiry != nil && !c.expiry.IsZero() && c.expiry.Before(c.timeNow())
|
||||
@@ -510,6 +511,22 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
|
||||
if nodeKeySignature, err = resignNKS(persist.NetworkLockKey, tryingNewKey.Public(), opt.OldNodeKeySignature); err != nil {
|
||||
c.logf("Failed re-signing node-key signature: %v", err)
|
||||
}
|
||||
} else if isWrapped {
|
||||
// We were given a wrapped pre-auth key, which means that in addition
|
||||
// to being a regular pre-auth key there was a suffix with information to
|
||||
// generate a tailnet-lock signature.
|
||||
nk, err := tryingNewKey.Public().MarshalBinary()
|
||||
if err != nil {
|
||||
return false, "", nil, fmt.Errorf("marshalling node-key: %w", err)
|
||||
}
|
||||
sig := &tka.NodeKeySignature{
|
||||
SigKind: tka.SigRotation,
|
||||
Pubkey: nk,
|
||||
Nested: wrappedSig,
|
||||
}
|
||||
sigHash := sig.SigHash()
|
||||
sig.Signature = ed25519.Sign(wrappedKey, sigHash[:])
|
||||
nodeKeySignature = sig.Serialize()
|
||||
}
|
||||
|
||||
if backendLogID == "" {
|
||||
@@ -735,9 +752,6 @@ func (c *Direct) newEndpoints(endpoints []tailcfg.Endpoint) (changed bool) {
|
||||
}
|
||||
c.logf("[v2] client.newEndpoints(%v)", epStrs)
|
||||
c.endpoints = append(c.endpoints[:0], endpoints...)
|
||||
if len(endpoints) > 0 {
|
||||
c.everEndpoints = true
|
||||
}
|
||||
return true // changed
|
||||
}
|
||||
|
||||
@@ -750,8 +764,6 @@ func (c *Direct) SetEndpoints(endpoints []tailcfg.Endpoint) (changed bool) {
|
||||
return c.newEndpoints(endpoints)
|
||||
}
|
||||
|
||||
func inTest() bool { return flag.Lookup("test.v") != nil }
|
||||
|
||||
// PollNetMap makes a /map request to download the network map, calling cb with
|
||||
// each new netmap.
|
||||
func (c *Direct) PollNetMap(ctx context.Context, cb func(*netmap.NetworkMap)) error {
|
||||
@@ -806,7 +818,6 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, readOnly bool
|
||||
epStrs = append(epStrs, ep.Addr.String())
|
||||
epTypes = append(epTypes, ep.Type)
|
||||
}
|
||||
everEndpoints := c.everEndpoints
|
||||
c.mu.Unlock()
|
||||
|
||||
machinePrivKey, err := c.getMachinePrivKey()
|
||||
@@ -847,15 +858,17 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, readOnly bool
|
||||
OmitPeers: cb == nil,
|
||||
TKAHead: c.tkaHead,
|
||||
|
||||
// On initial startup before we know our endpoints, set the ReadOnly flag
|
||||
// to tell the control server not to distribute out our (empty) endpoints to peers.
|
||||
// Presumably we'll learn our endpoints in a half second and do another post
|
||||
// with useful results. The first POST just gets us the DERP map which we
|
||||
// need to do the STUN queries to discover our endpoints.
|
||||
// TODO(bradfitz): we skip this optimization in tests, though,
|
||||
// because the e2e tests are currently hyper-specific about the
|
||||
// ordering of things. The e2e tests need love.
|
||||
ReadOnly: readOnly || (len(epStrs) == 0 && !everEndpoints && !inTest()),
|
||||
// Previously we'd set ReadOnly to true if we didn't have any endpoints
|
||||
// yet as we expected to learn them in a half second and restart the full
|
||||
// streaming map poll, however as we are trying to reduce the number of
|
||||
// times we restart the full streaming map poll we now just set ReadOnly
|
||||
// false when we're doing a full streaming map poll.
|
||||
//
|
||||
// TODO(maisem/bradfitz): really ReadOnly should be set to true if for
|
||||
// all streams and we should only do writes via lite map updates.
|
||||
// However that requires an audit and a bunch of testing to make sure we
|
||||
// don't break anything.
|
||||
ReadOnly: readOnly && !allowStream,
|
||||
}
|
||||
var extraDebugFlags []string
|
||||
if hi != nil && c.linkMon != nil && !c.skipIPForwardingCheck &&
|
||||
@@ -1713,6 +1726,43 @@ func (c *Direct) ReportHealthChange(sys health.Subsystem, sysErr error) {
|
||||
res.Body.Close()
|
||||
}
|
||||
|
||||
// decodeWrappedAuthkey separates wrapping information from an authkey, if any.
|
||||
// In all cases the authkey is returned, sans wrapping information if any.
|
||||
//
|
||||
// If the authkey is wrapped, isWrapped returns true, along with the wrapping signature
|
||||
// and private key.
|
||||
func decodeWrappedAuthkey(key string, logf logger.Logf) (authKey string, isWrapped bool, sig *tka.NodeKeySignature, priv ed25519.PrivateKey) {
|
||||
authKey, suffix, found := strings.Cut(key, "--TL")
|
||||
if !found {
|
||||
return key, false, nil, nil
|
||||
}
|
||||
sigBytes, privBytes, found := strings.Cut(suffix, "-")
|
||||
if !found {
|
||||
logf("decoding wrapped auth-key: did not find delimiter")
|
||||
return key, false, nil, nil
|
||||
}
|
||||
|
||||
rawSig, err := base64.RawStdEncoding.DecodeString(sigBytes)
|
||||
if err != nil {
|
||||
logf("decoding wrapped auth-key: signature decode: %v", err)
|
||||
return key, false, nil, nil
|
||||
}
|
||||
rawPriv, err := base64.RawStdEncoding.DecodeString(privBytes)
|
||||
if err != nil {
|
||||
logf("decoding wrapped auth-key: priv decode: %v", err)
|
||||
return key, false, nil, nil
|
||||
}
|
||||
|
||||
sig = new(tka.NodeKeySignature)
|
||||
if err := sig.Unserialize([]byte(rawSig)); err != nil {
|
||||
logf("decoding wrapped auth-key: signature: %v", err)
|
||||
return key, false, nil, nil
|
||||
}
|
||||
priv = ed25519.PrivateKey(rawPriv)
|
||||
|
||||
return authKey, true, sig, priv
|
||||
}
|
||||
|
||||
var (
|
||||
metricMapRequestsActive = clientmetric.NewGauge("controlclient_map_requests_active")
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
package controlclient
|
||||
|
||||
import (
|
||||
"crypto/ed25519"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
@@ -142,3 +143,42 @@ func TestTsmpPing(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeWrappedAuthkey(t *testing.T) {
|
||||
k, isWrapped, sig, priv := decodeWrappedAuthkey("tskey-32mjsdkdsffds9o87dsfkjlh", nil)
|
||||
if want := "tskey-32mjsdkdsffds9o87dsfkjlh"; k != want {
|
||||
t.Errorf("decodeWrappedAuthkey(<unwrapped-key>).key = %q, want %q", k, want)
|
||||
}
|
||||
if isWrapped {
|
||||
t.Error("decodeWrappedAuthkey(<unwrapped-key>).isWrapped = true, want false")
|
||||
}
|
||||
if sig != nil {
|
||||
t.Errorf("decodeWrappedAuthkey(<unwrapped-key>).sig = %v, want nil", sig)
|
||||
}
|
||||
if priv != nil {
|
||||
t.Errorf("decodeWrappedAuthkey(<unwrapped-key>).priv = %v, want nil", priv)
|
||||
}
|
||||
|
||||
k, isWrapped, sig, priv = decodeWrappedAuthkey("tskey-auth-k7UagY1CNTRL-ZZZZZ--TLpAEDA1ggnXuw4/fWnNWUwcoOjLemhOvml1juMl5lhLmY5sBUsj8EWEAfL2gdeD9g8VDw5tgcxCiHGlEb67BgU2DlFzZApi4LheLJraA+pYjTGChVhpZz1iyiBPD+U2qxDQAbM3+WFY0EBlggxmVqG53Hu0Rg+KmHJFMlUhfgzo+AQP6+Kk9GzvJJOs4-k36RdoSFqaoARfQo0UncHAV0t3YTqrkD5r/z2jTrE43GZWobnce7RGD4qYckUyVSF+DOj4BA/r4qT0bO8kk6zg", nil)
|
||||
if want := "tskey-auth-k7UagY1CNTRL-ZZZZZ"; k != want {
|
||||
t.Errorf("decodeWrappedAuthkey(<wrapped-key>).key = %q, want %q", k, want)
|
||||
}
|
||||
if !isWrapped {
|
||||
t.Error("decodeWrappedAuthkey(<wrapped-key>).isWrapped = false, want true")
|
||||
}
|
||||
|
||||
if sig == nil {
|
||||
t.Fatal("decodeWrappedAuthkey(<wrapped-key>).sig = nil, want non-nil signature")
|
||||
}
|
||||
sigHash := sig.SigHash()
|
||||
if !ed25519.Verify(sig.KeyID, sigHash[:], sig.Signature) {
|
||||
t.Error("signature failed to verify")
|
||||
}
|
||||
|
||||
// Make sure the private is correct by using it.
|
||||
someSig := ed25519.Sign(priv, []byte{1, 2, 3, 4})
|
||||
if !ed25519.Verify(sig.WrappingPubkey, []byte{1, 2, 3, 4}, someSig) {
|
||||
t.Error("failed to use priv")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
|
||||
"go4.org/mem"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tstest"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/netmap"
|
||||
"tailscale.com/types/opt"
|
||||
@@ -21,12 +22,11 @@ import (
|
||||
)
|
||||
|
||||
func TestUndeltaPeers(t *testing.T) {
|
||||
defer func(old func() time.Time) { clockNow = old }(clockNow)
|
||||
|
||||
var curTime time.Time
|
||||
clockNow = func() time.Time {
|
||||
tstest.Replace(t, &clockNow, func() time.Time {
|
||||
return curTime
|
||||
}
|
||||
})
|
||||
|
||||
online := func(v bool) func(*tailcfg.Node) {
|
||||
return func(n *tailcfg.Node) {
|
||||
n.Online = &v
|
||||
|
||||
@@ -41,6 +41,7 @@ import (
|
||||
"tailscale.com/net/dnscache"
|
||||
"tailscale.com/net/dnsfallback"
|
||||
"tailscale.com/net/netutil"
|
||||
"tailscale.com/net/sockstats"
|
||||
"tailscale.com/net/tlsdial"
|
||||
"tailscale.com/net/tshttpproxy"
|
||||
"tailscale.com/tailcfg"
|
||||
@@ -272,6 +273,8 @@ 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)
|
||||
|
||||
// u80 and u443 are the URLs we'll try to hit over HTTP or HTTPS,
|
||||
// respectively, in order to do the HTTP upgrade to a net.Conn over which
|
||||
// we'll speak Noise.
|
||||
@@ -385,12 +388,14 @@ func (a *Dialer) tryURLUpgrade(ctx context.Context, u *url.URL, addr netip.Addr,
|
||||
dns = &dnscache.Resolver{
|
||||
SingleHostStaticResult: []netip.Addr{addr},
|
||||
SingleHost: u.Hostname(),
|
||||
Logf: a.Logf, // not a.logf method; we want to propagate nil-ness
|
||||
}
|
||||
} else {
|
||||
dns = &dnscache.Resolver{
|
||||
Forward: dnscache.Get().Forward,
|
||||
LookupIPFallback: dnsfallback.Lookup,
|
||||
UseLastGood: true,
|
||||
Logf: a.Logf, // not a.logf method; we want to propagate nil-ness
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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
|
||||
@@ -279,6 +280,7 @@ func (s *dupClientSet) removeClient(c *sclient) bool {
|
||||
// public key gets more than one PacketForwarder registered for it.
|
||||
type PacketForwarder interface {
|
||||
ForwardPacket(src, dst key.NodePublic, payload []byte) error
|
||||
String() string
|
||||
}
|
||||
|
||||
// Conn is the subset of the underlying net.Conn the DERP Server needs.
|
||||
@@ -323,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"),
|
||||
@@ -495,6 +498,7 @@ func (s *Server) registerClient(c *sclient) {
|
||||
switch set := set.(type) {
|
||||
case nil:
|
||||
s.clients[c.key] = singleClient{c}
|
||||
c.debug("register single client")
|
||||
case singleClient:
|
||||
s.dupClientKeys.Add(1)
|
||||
s.dupClientConns.Add(2) // both old and new count
|
||||
@@ -510,6 +514,7 @@ func (s *Server) registerClient(c *sclient) {
|
||||
},
|
||||
sendHistory: []*sclient{old},
|
||||
}
|
||||
c.debug("register duplicate client")
|
||||
case *dupClientSet:
|
||||
s.dupClientConns.Add(1) // the gauge
|
||||
s.dupClientConnTotal.Add(1) // the counter
|
||||
@@ -517,6 +522,7 @@ func (s *Server) registerClient(c *sclient) {
|
||||
set.set[c] = true
|
||||
set.last = c
|
||||
set.sendHistory = append(set.sendHistory, c)
|
||||
c.debug("register another duplicate client")
|
||||
}
|
||||
|
||||
if _, ok := s.clientsMesh[c.key]; !ok {
|
||||
@@ -549,7 +555,7 @@ func (s *Server) unregisterClient(c *sclient) {
|
||||
case nil:
|
||||
c.logf("[unexpected]; clients map is empty")
|
||||
case singleClient:
|
||||
c.logf("removing connection")
|
||||
c.logf("removed connection")
|
||||
delete(s.clients, c.key)
|
||||
if v, ok := s.clientsMesh[c.key]; ok && v == nil {
|
||||
delete(s.clientsMesh, c.key)
|
||||
@@ -557,6 +563,7 @@ func (s *Server) unregisterClient(c *sclient) {
|
||||
}
|
||||
s.broadcastPeerStateChangeLocked(c.key, false)
|
||||
case *dupClientSet:
|
||||
c.debug("removed duplicate client")
|
||||
if set.removeClient(c) {
|
||||
s.dupClientConns.Add(-1)
|
||||
} else {
|
||||
@@ -610,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")
|
||||
@@ -673,7 +693,7 @@ func (s *Server) accept(ctx context.Context, nc Conn, brw *bufio.ReadWriter, rem
|
||||
nc: nc,
|
||||
br: br,
|
||||
bw: bw,
|
||||
logf: logger.WithPrefix(s.logf, fmt.Sprintf("derp client %v/%x: ", remoteAddr, clientKey)),
|
||||
logf: logger.WithPrefix(s.logf, fmt.Sprintf("derp client %v%s: ", remoteAddr, clientKey.ShortString())),
|
||||
done: ctx.Done(),
|
||||
remoteAddr: remoteAddr,
|
||||
remoteIPPort: remoteIPPort,
|
||||
@@ -681,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 {
|
||||
@@ -690,6 +711,9 @@ func (s *Server) accept(ctx context.Context, nc Conn, brw *bufio.ReadWriter, rem
|
||||
}
|
||||
if clientInfo != nil {
|
||||
c.info = *clientInfo
|
||||
if envknob.Bool("DERP_PROBER_DEBUG_LOGS") && clientInfo.IsProber {
|
||||
c.debugLogging = true
|
||||
}
|
||||
}
|
||||
|
||||
s.registerClient(c)
|
||||
@@ -726,6 +750,7 @@ func (c *sclient) run(ctx context.Context) error {
|
||||
|
||||
for {
|
||||
ft, fl, err := readFrameHeader(c.br)
|
||||
c.debug("read frame type %d len %d err %v", ft, fl, err)
|
||||
if err != nil {
|
||||
if errors.Is(err, io.EOF) {
|
||||
c.logf("read EOF")
|
||||
@@ -735,7 +760,7 @@ func (c *sclient) run(ctx context.Context) error {
|
||||
c.logf("closing; server closed")
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("client %x: readFrameHeader: %w", c.key, err)
|
||||
return fmt.Errorf("client %s: readFrameHeader: %w", c.key.ShortString(), err)
|
||||
}
|
||||
c.s.noteClientActivity(c)
|
||||
switch ft {
|
||||
@@ -878,11 +903,15 @@ func (c *sclient) handleFrameForwardPacket(ft frameType, fl uint32) error {
|
||||
reason := dropReasonUnknownDestOnFwd
|
||||
if dstLen > 1 {
|
||||
reason = dropReasonDupClient
|
||||
} else {
|
||||
c.requestPeerGoneWriteLimited(dstKey, contents, PeerGoneReasonNotHere)
|
||||
}
|
||||
s.recordDrop(contents, srcKey, dstKey, reason)
|
||||
return nil
|
||||
}
|
||||
|
||||
dst.debug("received forwarded packet from %s via %s", srcKey.ShortString(), c.key.ShortString())
|
||||
|
||||
return c.sendPkt(dst, pkt{
|
||||
bs: contents,
|
||||
enqueuedAt: time.Now(),
|
||||
@@ -930,7 +959,9 @@ func (c *sclient) handleFrameSendPacket(ft frameType, fl uint32) error {
|
||||
if dst == nil {
|
||||
if fwd != nil {
|
||||
s.packetsForwardedOut.Add(1)
|
||||
if err := fwd.ForwardPacket(c.key, dstKey, contents); err != nil {
|
||||
err := fwd.ForwardPacket(c.key, dstKey, contents)
|
||||
c.debug("SendPacket for %s, forwarding via %s: %v", dstKey.ShortString(), fwd, err)
|
||||
if err != nil {
|
||||
// TODO:
|
||||
return nil
|
||||
}
|
||||
@@ -939,10 +970,14 @@ func (c *sclient) handleFrameSendPacket(ft frameType, fl uint32) error {
|
||||
reason := dropReasonUnknownDest
|
||||
if dstLen > 1 {
|
||||
reason = dropReasonDupClient
|
||||
} else {
|
||||
c.requestPeerGoneWriteLimited(dstKey, contents, PeerGoneReasonNotHere)
|
||||
}
|
||||
s.recordDrop(contents, c.key, dstKey, reason)
|
||||
c.debug("SendPacket for %s, dropping with reason=%s", dstKey.ShortString(), reason)
|
||||
return nil
|
||||
}
|
||||
c.debug("SendPacket for %s, sending directly", dstKey.ShortString())
|
||||
|
||||
p := pkt{
|
||||
bs: contents,
|
||||
@@ -952,6 +987,12 @@ func (c *sclient) handleFrameSendPacket(ft frameType, fl uint32) error {
|
||||
return c.sendPkt(dst, p)
|
||||
}
|
||||
|
||||
func (c *sclient) debug(format string, v ...any) {
|
||||
if c.debugLogging {
|
||||
c.logf(format, v...)
|
||||
}
|
||||
}
|
||||
|
||||
// dropReason is why we dropped a DERP frame.
|
||||
type dropReason int
|
||||
|
||||
@@ -960,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
|
||||
@@ -1002,12 +1043,14 @@ 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:
|
||||
}
|
||||
select {
|
||||
case sendQueue <- p:
|
||||
dst.debug("sendPkt attempt %d enqueued", attempt)
|
||||
return nil
|
||||
default:
|
||||
}
|
||||
@@ -1023,16 +1066,20 @@ func (c *sclient) sendPkt(dst *sclient, p pkt) error {
|
||||
// contended queue with racing writers. Give up and tail-drop in
|
||||
// this case to keep reader unblocked.
|
||||
s.recordDrop(p.bs, c.key, dstKey, dropReasonQueueTail)
|
||||
dst.debug("sendPkt attempt %d dropped, queue full")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// requestPeerGoneWrite sends a request to write a "peer gone" frame
|
||||
// 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:
|
||||
}
|
||||
}
|
||||
@@ -1246,22 +1293,19 @@ 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
|
||||
|
||||
// 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
|
||||
debugLogging bool
|
||||
|
||||
// Owned by run, not thread-safe.
|
||||
br *bufio.Reader
|
||||
@@ -1278,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
|
||||
@@ -1301,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
|
||||
@@ -1355,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
|
||||
}
|
||||
@@ -1378,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()
|
||||
@@ -1410,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
|
||||
@@ -1452,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
|
||||
}
|
||||
|
||||
@@ -1489,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
|
||||
@@ -1529,6 +1593,7 @@ func (c *sclient) sendPacket(srcKey key.NodePublic, contents []byte) (err error)
|
||||
c.s.packetsSent.Add(1)
|
||||
c.s.bytesSent.Add(int64(len(contents)))
|
||||
}
|
||||
c.debug("sendPacket from %s: %v", srcKey.ShortString(), err)
|
||||
}()
|
||||
|
||||
c.setWriteDeadline()
|
||||
@@ -1689,6 +1754,10 @@ func (f *multiForwarder) ForwardPacket(src, dst key.NodePublic, payload []byte)
|
||||
return f.fwd.Load().ForwardPacket(src, dst, payload)
|
||||
}
|
||||
|
||||
func (f *multiForwarder) String() string {
|
||||
return fmt.Sprintf("<MultiForwarder fwd=%s total=%d>", f.fwd.Load(), len(f.all))
|
||||
}
|
||||
|
||||
func (s *Server) expVarFunc(f func() any) expvar.Func {
|
||||
return expvar.Func(func() any {
|
||||
s.mu.Lock()
|
||||
@@ -1725,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)
|
||||
}
|
||||
@@ -660,6 +709,9 @@ type testFwd int
|
||||
func (testFwd) ForwardPacket(key.NodePublic, key.NodePublic, []byte) error {
|
||||
panic("not called in tests")
|
||||
}
|
||||
func (testFwd) String() string {
|
||||
panic("not called in tests")
|
||||
}
|
||||
|
||||
func pubAll(b byte) (ret key.NodePublic) {
|
||||
var bs [32]byte
|
||||
@@ -787,6 +839,7 @@ type channelFwd struct {
|
||||
c chan []byte
|
||||
}
|
||||
|
||||
func (f channelFwd) String() string { return "" }
|
||||
func (f channelFwd) ForwardPacket(_ key.NodePublic, _ key.NodePublic, packet []byte) error {
|
||||
f.c <- packet
|
||||
return nil
|
||||
|
||||
@@ -32,6 +32,7 @@ import (
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/net/dnscache"
|
||||
"tailscale.com/net/netns"
|
||||
"tailscale.com/net/sockstats"
|
||||
"tailscale.com/net/tlsdial"
|
||||
"tailscale.com/net/tshttpproxy"
|
||||
"tailscale.com/syncs"
|
||||
@@ -81,6 +82,10 @@ type Client struct {
|
||||
pingOut map[derp.PingMessage]chan<- bool // chan to send to on pong
|
||||
}
|
||||
|
||||
func (c *Client) String() string {
|
||||
return fmt.Sprintf("<derphttp_client.Client %s url=%s>", c.serverPubKey.ShortString(), c.url)
|
||||
}
|
||||
|
||||
// NewRegionClient returns a new DERP-over-HTTP client. It connects lazily.
|
||||
// To trigger a connection, use Connect.
|
||||
func NewRegionClient(privateKey key.NodePrivate, logf logger.Logf, getRegion func() *tailcfg.DERPRegion) *Client {
|
||||
@@ -320,7 +325,7 @@ func (c *Client) connect(ctx context.Context, caller string) (client *derp.Clien
|
||||
}
|
||||
c.serverPubKey = derpClient.ServerPublicKey()
|
||||
c.client = derpClient
|
||||
c.netConn = tcpConn
|
||||
c.netConn = conn
|
||||
c.connGen++
|
||||
return c.client, c.connGen, nil
|
||||
case c.url != nil:
|
||||
@@ -615,6 +620,8 @@ 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)
|
||||
|
||||
nwait := 0
|
||||
startDial := func(dstPrimary, proto string) {
|
||||
nwait++
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -42,6 +42,7 @@ var (
|
||||
regBool = map[string]*bool{}
|
||||
regOptBool = map[string]*opt.Bool{}
|
||||
regDuration = map[string]*time.Duration{}
|
||||
regInt = map[string]*int{}
|
||||
)
|
||||
|
||||
func noteEnv(k, v string) {
|
||||
@@ -182,6 +183,25 @@ func RegisterDuration(envVar string) func() time.Duration {
|
||||
return func() time.Duration { return *p }
|
||||
}
|
||||
|
||||
// RegisterInt returns a func that gets the named environment variable as an
|
||||
// integer, without a map lookup per call. It assumes that any mutations happen
|
||||
// via envknob.Setenv.
|
||||
func RegisterInt(envVar string) func() int {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
p, ok := regInt[envVar]
|
||||
if !ok {
|
||||
val := os.Getenv(envVar)
|
||||
if val != "" {
|
||||
noteEnvLocked(envVar, val)
|
||||
}
|
||||
p = new(int)
|
||||
setIntLocked(p, envVar, val)
|
||||
regInt[envVar] = p
|
||||
}
|
||||
return func() int { return *p }
|
||||
}
|
||||
|
||||
func setBoolLocked(p *bool, envVar, val string) {
|
||||
noteEnvLocked(envVar, val)
|
||||
if val == "" {
|
||||
@@ -221,6 +241,19 @@ func setDurationLocked(p *time.Duration, envVar, val string) {
|
||||
}
|
||||
}
|
||||
|
||||
func setIntLocked(p *int, envVar, val string) {
|
||||
noteEnvLocked(envVar, val)
|
||||
if val == "" {
|
||||
*p = 0
|
||||
return
|
||||
}
|
||||
var err error
|
||||
*p, err = strconv.Atoi(val)
|
||||
if err != nil {
|
||||
log.Fatalf("invalid int environment variable %s value %q", envVar, val)
|
||||
}
|
||||
}
|
||||
|
||||
// Bool returns the boolean value of the named environment variable.
|
||||
// If the variable is not set, it returns false.
|
||||
// An invalid value exits the binary with a failure.
|
||||
@@ -297,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)
|
||||
}
|
||||
}
|
||||
@@ -108,10 +108,11 @@
|
||||
graphviz
|
||||
perl
|
||||
go_1_20
|
||||
yarn
|
||||
];
|
||||
};
|
||||
};
|
||||
in
|
||||
flake-utils.lib.eachDefaultSystem (system: flakeForSystem nixpkgs system);
|
||||
}
|
||||
# nix-direnv cache busting line: sha256-zyyqBRFPNPzPYCMgnbnOy5rb3fkn4XEHZlTlJvwqunM=
|
||||
# nix-direnv cache busting line: sha256-lSK9rTz5NDXf5BBELL6YYYtxtjrHjfqEiYwN75hYA2c=
|
||||
|
||||
17
go.mod
17
go.mod
@@ -61,7 +61,7 @@ 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-20230328204031-f7bfdb68b4af
|
||||
github.com/tc-hib/winres v0.1.6
|
||||
github.com/tcnksm/go-httpstat v0.2.0
|
||||
github.com/toqueteos/webbrowser v1.2.0
|
||||
@@ -74,17 +74,18 @@ require (
|
||||
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/oauth2 v0.4.0
|
||||
golang.org/x/sync v0.1.0
|
||||
golang.org/x/sys v0.5.0
|
||||
golang.org/x/sys v0.5.1-0.20230222185716-a3b23cc77e89
|
||||
golang.org/x/term v0.5.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.5.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.2
|
||||
inet.af/peercred v0.0.0-20210906144145-0893ea02156a
|
||||
inet.af/tcpproxy v0.0.0-20221017015627-91f861402626
|
||||
inet.af/wf v0.0.0-20220728202103-50d96caab2f6
|
||||
k8s.io/api v0.25.0
|
||||
k8s.io/apimachinery v0.25.0
|
||||
@@ -304,11 +305,11 @@ require (
|
||||
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/image v0.0.0-20201208152932-35266b937fa6 // indirect
|
||||
golang.org/x/image v0.5.0 // indirect
|
||||
golang.org/x/text v0.7.0 // indirect
|
||||
gomodules.xyz/jsonpatch/v2 v2.2.0 // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
google.golang.org/protobuf v1.28.0 // indirect
|
||||
google.golang.org/protobuf v1.28.2-0.20230118093459-a9481185b34d // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gopkg.in/ini.v1 v1.66.2 // indirect
|
||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
|
||||
@@ -1 +1 @@
|
||||
sha256-zyyqBRFPNPzPYCMgnbnOy5rb3fkn4XEHZlTlJvwqunM=
|
||||
sha256-lSK9rTz5NDXf5BBELL6YYYtxtjrHjfqEiYwN75hYA2c=
|
||||
|
||||
43
go.sum
43
go.sum
@@ -126,6 +126,7 @@ github.com/aokoli/goutils v1.0.1/go.mod h1:SijmP0QR8LtwsmDs8Yii5Z/S4trXFGFC2oO5g
|
||||
github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
|
||||
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
||||
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
|
||||
github.com/armon/go-proxyproto v0.0.0-20210323213023-7e956b284f0a/go.mod h1:QmP9hvJ91BbJmGVGSbutW19IC0Q9phDCLGaomwTJbgU=
|
||||
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
||||
github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
|
||||
@@ -214,8 +215,8 @@ github.com/chavacava/garif v0.0.0-20210405164556-e8a0a408d6af/go.mod h1:Qjyv4H3/
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/cilium/ebpf v0.8.1 h1:bLSSEbBLqGPXxls55pGr5qWZaTqcmfDJHhou7t254ao=
|
||||
github.com/cilium/ebpf v0.8.1/go.mod h1:f5zLIM0FSNuAkSyLAN7X+Hy6yznlF1mNiWUMfxMtrgk=
|
||||
github.com/cilium/ebpf v0.9.3 h1:5KtxXZU+scyERvkJMEm16TbScVvuuMrlhPly78ZMbSc=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cloudflare/circl v1.1.0 h1:bZgT/A+cikZnKIwn7xL2OBj012Bmvho/o6RpRvv3GKY=
|
||||
github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I=
|
||||
@@ -1169,8 +1170,8 @@ github.com/tailscale/mkctr v0.0.0-20220601142259-c0b937af2e89 h1:7xU7AFQE83h0wz/
|
||||
github.com/tailscale/mkctr v0.0.0-20220601142259-c0b937af2e89/go.mod h1:OGMqrTzDqmJkGumUTtOv44Rp3/4xS+QFbE8Rn0AGlaU=
|
||||
github.com/tailscale/netlink v1.1.1-0.20211101221916-cabfb018fe85 h1:zrsUcqrG2uQSPhaUPjUQwozcRdDdSxxqhNgNZ3drZFk=
|
||||
github.com/tailscale/netlink v1.1.1-0.20211101221916-cabfb018fe85/go.mod h1:NzVQi3Mleb+qzq8VmcWpSkcSYxXIg0DkI6XDzpVkhJ0=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20221219190806-4fa124729667 h1:etWp6uUwKu8NEj37K2OuMBnZ7EnVMKA7gJg5AqPFy/o=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20221219190806-4fa124729667/go.mod h1:iiClgxBTruKI+nmzlQxbFw6c3nB/wb4Td/WCyX2berY=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20230328204031-f7bfdb68b4af h1:ZEHPJYZnOs8G5ldkk8iefYzmbOB/SIpfIEA+9znIv8s=
|
||||
github.com/tailscale/wireguard-go v0.0.0-20230328204031-f7bfdb68b4af/go.mod h1:QRIcq2+DbdIC5sKh/gcAZhuqu6WT6L6G8/ALPN5wqYw=
|
||||
github.com/tc-hib/winres v0.1.6 h1:qgsYHze+BxQPEYilxIz/KCQGaClvI2+yLBAZs+3+0B8=
|
||||
github.com/tc-hib/winres v0.1.6/go.mod h1:pe6dOR40VOrGz8PkzreVKNvEKnlE8t4yR8A8naL+t7A=
|
||||
github.com/tcnksm/go-httpstat v0.2.0 h1:rP7T5e5U2HfmOBmZzGgGZjBQ5/GluWUylujl0tJ04I0=
|
||||
@@ -1270,6 +1271,7 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec
|
||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
||||
go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
|
||||
go.etcd.io/bbolt v1.3.4/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
|
||||
@@ -1357,8 +1359,9 @@ golang.org/x/exp/typeparams v0.0.0-20221208152030-732eee02a75a h1:Jw5wfR+h9mnIYH
|
||||
golang.org/x/exp/typeparams v0.0.0-20221208152030-732eee02a75a/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk=
|
||||
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
|
||||
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.0.0-20201208152932-35266b937fa6 h1:nfeHNc1nAqecKCy2FCy4HY+soOOe5sDLJ/gZLbx6GYI=
|
||||
golang.org/x/image v0.0.0-20201208152932-35266b937fa6/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.5.0 h1:5JMiNunQeQw++mMOz48/ISeNu3Iweh/JaZU8ZLqHRrI=
|
||||
golang.org/x/image v0.5.0/go.mod h1:FVC7BI/5Ym8R25iw5OLsgshdUBbT1h5jZTpA+mvAdZ4=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
@@ -1384,6 +1387,7 @@ golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
|
||||
golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.7.0 h1:LapD9S96VoQRhi/GrNTqeBJFrUjs5UHCAtTlgwA5oZA=
|
||||
golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@@ -1446,6 +1450,7 @@ golang.org/x/net v0.0.0-20210928044308-7d9f5e0b762b/go.mod h1:9nx3DQGgdP8bBQD5qx
|
||||
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g=
|
||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
@@ -1464,8 +1469,8 @@ golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ
|
||||
golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5 h1:OSnWWcOd/CtWQC2cYSBgbTSJv3ciqd8r54ySIW2y3RE=
|
||||
golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
|
||||
golang.org/x/oauth2 v0.4.0 h1:NF0gk8LVPg1Ml7SSbGyySuoxdsXitj7TvgvuRxIMc/M=
|
||||
golang.org/x/oauth2 v0.4.0/go.mod h1:RznEsdpjGAINPTOF0UH/t+xJ75L18YO3Ho6Pyn+uRec=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@@ -1478,6 +1483,7 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@@ -1586,11 +1592,13 @@ golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.4.1-0.20230131160137-e7d7f63158de/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.1-0.20230222185716-a3b23cc77e89 h1:260HNjMTPDya+jq5AM1zZLgG9pv9GASPAGiEEJUbRg4=
|
||||
golang.org/x/sys v0.5.1-0.20230222185716-a3b23cc77e89/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
@@ -1725,8 +1733,9 @@ golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.6/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
|
||||
golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
|
||||
golang.org/x/tools v0.1.8-0.20211102182255-bb4add04ddef/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
|
||||
golang.org/x/tools v0.4.1-0.20221208213631-3f74d914ae6d h1:9ZNWAi4CYhNv60mXGgAncgq7SGc5qa7C8VZV8Tg7Ggs=
|
||||
golang.org/x/tools v0.4.1-0.20221208213631-3f74d914ae6d/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.5.0 h1:+bSpV5HIeWkuvgaMfI3UmKRThoTA5ODJTUd8T17NO+4=
|
||||
golang.org/x/tools v0.5.0/go.mod h1:N+Kgy78s5I24c24dU8OfWNEotWjutIs8SnJvn5IDq+k=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
@@ -1877,8 +1886,8 @@ google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlba
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw=
|
||||
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
google.golang.org/protobuf v1.28.2-0.20230118093459-a9481185b34d h1:qp0AnQCvRCMlu9jBjtdbTaaEmThIgZOrbVyDEOcmKhQ=
|
||||
google.golang.org/protobuf v1.28.2-0.20230118093459-a9481185b34d/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
@@ -1921,8 +1930,8 @@ gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gotest.tools/v3 v3.4.0 h1:ZazjZUfuVeZGLAmlKKuyv3IKP5orXcwtOwDQH6YVr6o=
|
||||
gvisor.dev/gvisor v0.0.0-20221203005347-703fd9b7fbc0 h1:Wobr37noukisGxpKo5jAsLREcpj61RxrWYzD8uwveOY=
|
||||
gvisor.dev/gvisor v0.0.0-20221203005347-703fd9b7fbc0/go.mod h1:Dn5idtptoW1dIos9U6A2rpebLs/MtTwFacjKb8jLdQA=
|
||||
gvisor.dev/gvisor v0.0.0-20230328175328-162ed5ef888d h1:pzPNKWBCLdzu7KGiFnFGuvKsLf+pNkOzHSK6zJ9tMKY=
|
||||
gvisor.dev/gvisor v0.0.0-20230328175328-162ed5ef888d/go.mod h1:pzr6sy8gDLfVmDAg8OYrlKvGEHw5C3PGTiBXBTCx76Q=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
@@ -1932,13 +1941,15 @@ honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9
|
||||
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
honnef.co/go/tools v0.0.1-2020.1.6/go.mod h1:pyyisuGw24ruLjrr1ddx39WE0y9OooInRzEYLhQB2YY=
|
||||
honnef.co/go/tools v0.2.1/go.mod h1:lPVVZ2BS5TfnjLyizF7o7hv7j9/L+8cZY2hLyjP9cGY=
|
||||
honnef.co/go/tools v0.4.0-0.dev.0.20230130122044-c30b15588105 h1:2OzOQ+1scFmv2dt7x+wNxgikA/Rn2qKrvc/CJCVuAJM=
|
||||
honnef.co/go/tools v0.4.0-0.dev.0.20230130122044-c30b15588105/go.mod h1:36ZgoUOrqOk1GxwHhyryEkq8FQWkUO2xGuSMhUCcdvA=
|
||||
honnef.co/go/tools v0.4.2 h1:6qXr+R5w+ktL5UkwEbPp+fEvfyoMPche6GkOpGHZcLc=
|
||||
honnef.co/go/tools v0.4.2/go.mod h1:36ZgoUOrqOk1GxwHhyryEkq8FQWkUO2xGuSMhUCcdvA=
|
||||
howett.net/plist v0.0.0-20181124034731-591f970eefbb/go.mod h1:vMygbs4qMhSZSc4lCUl2OEE+rDiIIJAIdR4m7MiMcm0=
|
||||
howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM=
|
||||
howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g=
|
||||
inet.af/peercred v0.0.0-20210906144145-0893ea02156a h1:qdkS8Q5/i10xU2ArJMKYhVa1DORzBfYS/qA2UK2jheg=
|
||||
inet.af/peercred v0.0.0-20210906144145-0893ea02156a/go.mod h1:FjawnflS/udxX+SvpsMgZfdqx2aykOlkISeAsADi5IU=
|
||||
inet.af/tcpproxy v0.0.0-20221017015627-91f861402626 h1:2dMP3Ox/Wh5BiItwOt4jxRsfzkgyBrHzx2nW28Yg6nc=
|
||||
inet.af/tcpproxy v0.0.0-20221017015627-91f861402626/go.mod h1:Tojt5kmHpDIR2jMojxzZK2w2ZR7OILODmUo2gaSwjrk=
|
||||
inet.af/wf v0.0.0-20220728202103-50d96caab2f6 h1:BfgDtKnWJTeu+xI1aOEweXdPwqOhB3IbQUDj1XuftcY=
|
||||
inet.af/wf v0.0.0-20220728202103-50d96caab2f6/go.mod h1:bSAQ38BYbY68uwpasXOTZo22dKGy9SNvI6PZFeKomZE=
|
||||
k8s.io/api v0.25.0 h1:H+Q4ma2U/ww0iGB78ijZx6DRByPz6/733jIuFpX70e0=
|
||||
|
||||
@@ -1 +1 @@
|
||||
ec180cbca39fcb5dc420399b37583e53fcf382c9
|
||||
568add9f5d780e86f8b3e7002fd7b4a7479005fa
|
||||
|
||||
@@ -36,6 +36,7 @@ func New() *tailcfg.Hostinfo {
|
||||
return &tailcfg.Hostinfo{
|
||||
IPNVersion: version.Long(),
|
||||
Hostname: hostname,
|
||||
App: appTypeCached(),
|
||||
OS: version.OS(),
|
||||
OSVersion: GetOSVersion(),
|
||||
Container: lazyInContainer.Get(),
|
||||
@@ -112,6 +113,13 @@ func GetOSVersion() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func appTypeCached() string {
|
||||
if v, ok := appType.Load().(string); ok {
|
||||
return v
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func packageTypeCached() string {
|
||||
if v, _ := packagingType.Load().(string); v != "" {
|
||||
return v
|
||||
@@ -159,6 +167,7 @@ var (
|
||||
osVersionAtomic atomic.Value // of string
|
||||
desktopAtomic atomic.Value // of opt.Bool
|
||||
packagingType atomic.Value // of string
|
||||
appType atomic.Value // of string
|
||||
)
|
||||
|
||||
// SetPushDeviceToken sets the device token for use in Hostinfo updates.
|
||||
@@ -176,6 +185,11 @@ func SetOSVersion(v string) { osVersionAtomic.Store(v) }
|
||||
// F-Droid build) and tsnet (set to "tsnet").
|
||||
func SetPackage(v string) { packagingType.Store(v) }
|
||||
|
||||
// SetApp sets the app type for the app.
|
||||
// It is used by tsnet to specify what app is using it such as "golinks"
|
||||
// and "k8s-operator".
|
||||
func SetApp(v string) { appType.Store(v) }
|
||||
|
||||
func deviceModel() string {
|
||||
s, _ := deviceModelAtomic.Load().(string)
|
||||
return s
|
||||
|
||||
@@ -83,6 +83,14 @@ func (b *LocalBackend) handleC2N(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
writeJSON(res)
|
||||
case "/sockstats":
|
||||
if r.Method != "POST" {
|
||||
http.Error(w, "bad method", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
b.sockstatLogger.Flush()
|
||||
fmt.Fprintln(w, b.sockstatLogger.LogID())
|
||||
default:
|
||||
http.Error(w, "unknown c2n path", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
@@ -31,10 +31,13 @@ import (
|
||||
"time"
|
||||
|
||||
"golang.org/x/crypto/acme"
|
||||
"tailscale.com/atomicfile"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/hostinfo"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/ipn/store"
|
||||
"tailscale.com/ipn/store/mem"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/version"
|
||||
"tailscale.com/version/distro"
|
||||
@@ -82,11 +85,6 @@ func (b *LocalBackend) GetCertPEM(ctx context.Context, domain string) (*TLSCertK
|
||||
return nil, errors.New("invalid domain")
|
||||
}
|
||||
logf := logger.WithPrefix(b.logf, fmt.Sprintf("cert(%q): ", domain))
|
||||
dir, err := b.certDir()
|
||||
if err != nil {
|
||||
logf("failed to get certDir: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
now := time.Now()
|
||||
traceACME := func(v any) {
|
||||
if !acmeDebug() {
|
||||
@@ -96,17 +94,22 @@ func (b *LocalBackend) GetCertPEM(ctx context.Context, domain string) (*TLSCertK
|
||||
log.Printf("acme %T: %s", v, j)
|
||||
}
|
||||
|
||||
if pair, err := b.getCertPEMCached(dir, domain, now); err == nil {
|
||||
cs, err := b.getCertStore()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if pair, err := getCertPEMCached(cs, domain, now); err == nil {
|
||||
future := now.AddDate(0, 0, 14)
|
||||
if b.shouldStartDomainRenewal(dir, domain, future) {
|
||||
if b.shouldStartDomainRenewal(cs, domain, future) {
|
||||
logf("starting async renewal")
|
||||
// Start renewal in the background.
|
||||
go b.getCertPEM(context.Background(), logf, traceACME, dir, domain, future)
|
||||
go b.getCertPEM(context.Background(), cs, logf, traceACME, domain, future)
|
||||
}
|
||||
return pair, nil
|
||||
}
|
||||
|
||||
pair, err := b.getCertPEM(ctx, logf, traceACME, dir, domain, now)
|
||||
pair, err := b.getCertPEM(ctx, cs, logf, traceACME, domain, now)
|
||||
if err != nil {
|
||||
logf("getCertPEM: %v", err)
|
||||
return nil, err
|
||||
@@ -114,7 +117,7 @@ func (b *LocalBackend) GetCertPEM(ctx context.Context, domain string) (*TLSCertK
|
||||
return pair, nil
|
||||
}
|
||||
|
||||
func (b *LocalBackend) shouldStartDomainRenewal(dir, domain string, future time.Time) bool {
|
||||
func (b *LocalBackend) shouldStartDomainRenewal(cs certStore, domain string, future time.Time) bool {
|
||||
renewMu.Lock()
|
||||
defer renewMu.Unlock()
|
||||
now := time.Now()
|
||||
@@ -124,7 +127,7 @@ func (b *LocalBackend) shouldStartDomainRenewal(dir, domain string, future time.
|
||||
return false
|
||||
}
|
||||
lastRenewCheck[domain] = now
|
||||
_, err := b.getCertPEMCached(dir, domain, future)
|
||||
_, err := getCertPEMCached(cs, domain, future)
|
||||
return errors.Is(err, errCertExpired)
|
||||
}
|
||||
|
||||
@@ -140,15 +143,32 @@ type certStore interface {
|
||||
WriteCert(domain string, cert []byte) error
|
||||
// WriteKey writes the key for domain.
|
||||
WriteKey(domain string, key []byte) error
|
||||
// ACMEKey returns the value previously stored via WriteACMEKey.
|
||||
// It is a PEM encoded ECDSA key.
|
||||
ACMEKey() ([]byte, error)
|
||||
// WriteACMEKey stores the provided PEM encoded ECDSA key.
|
||||
WriteACMEKey([]byte) error
|
||||
}
|
||||
|
||||
var errCertExpired = errors.New("cert expired")
|
||||
|
||||
func (b *LocalBackend) getCertStore(dir string) certStore {
|
||||
if hostinfo.GetEnvType() == hostinfo.Kubernetes && dir == "/tmp" {
|
||||
return certStateStore{StateStore: b.store}
|
||||
func (b *LocalBackend) getCertStore() (certStore, error) {
|
||||
switch b.store.(type) {
|
||||
case *store.FileStore:
|
||||
case *mem.Store:
|
||||
default:
|
||||
if hostinfo.GetEnvType() == hostinfo.Kubernetes {
|
||||
// We're running in Kubernetes with a custom StateStore,
|
||||
// use that instead of the cert directory.
|
||||
// TODO(maisem): expand this to other environments?
|
||||
return certStateStore{StateStore: b.store}, nil
|
||||
}
|
||||
}
|
||||
return certFileStore{dir: dir}
|
||||
dir, err := b.certDir()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return certFileStore{dir: dir}, nil
|
||||
}
|
||||
|
||||
// certFileStore implements certStore by storing the cert & key files in the named directory.
|
||||
@@ -160,6 +180,25 @@ type certFileStore struct {
|
||||
testRoots *x509.CertPool
|
||||
}
|
||||
|
||||
const acmePEMName = "acme-account.key.pem"
|
||||
|
||||
func (f certFileStore) ACMEKey() ([]byte, error) {
|
||||
pemName := filepath.Join(f.dir, acmePEMName)
|
||||
v, err := os.ReadFile(pemName)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil, ipn.ErrStateNotExist
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
func (f certFileStore) WriteACMEKey(b []byte) error {
|
||||
pemName := filepath.Join(f.dir, acmePEMName)
|
||||
return atomicfile.WriteFile(pemName, b, 0600)
|
||||
}
|
||||
|
||||
func (f certFileStore) Read(domain string, now time.Time) (*TLSCertKeyPair, error) {
|
||||
certPEM, err := os.ReadFile(certFile(f.dir, domain))
|
||||
if err != nil {
|
||||
@@ -182,11 +221,11 @@ func (f certFileStore) Read(domain string, now time.Time) (*TLSCertKeyPair, erro
|
||||
}
|
||||
|
||||
func (f certFileStore) WriteCert(domain string, cert []byte) error {
|
||||
return os.WriteFile(certFile(f.dir, domain), cert, 0644)
|
||||
return atomicfile.WriteFile(certFile(f.dir, domain), cert, 0644)
|
||||
}
|
||||
|
||||
func (f certFileStore) WriteKey(domain string, key []byte) error {
|
||||
return os.WriteFile(keyFile(f.dir, domain), key, 0600)
|
||||
return atomicfile.WriteFile(keyFile(f.dir, domain), key, 0600)
|
||||
}
|
||||
|
||||
// certStateStore implements certStore by storing the cert & key files in an ipn.StateStore.
|
||||
@@ -221,6 +260,14 @@ func (s certStateStore) WriteKey(domain string, key []byte) error {
|
||||
return s.WriteState(ipn.StateKey(domain+".key"), key)
|
||||
}
|
||||
|
||||
func (s certStateStore) ACMEKey() ([]byte, error) {
|
||||
return s.ReadState(ipn.StateKey(acmePEMName))
|
||||
}
|
||||
|
||||
func (s certStateStore) WriteACMEKey(key []byte) error {
|
||||
return s.WriteState(ipn.StateKey(acmePEMName), key)
|
||||
}
|
||||
|
||||
// TLSCertKeyPair is a TLS public and private key, and whether they were obtained
|
||||
// from cache or freshly obtained.
|
||||
type TLSCertKeyPair struct {
|
||||
@@ -236,26 +283,26 @@ func certFile(dir, domain string) string { return filepath.Join(dir, domain+".cr
|
||||
// domain exists on disk in dir that is valid at the provided now time.
|
||||
// If the keypair is expired, it returns errCertExpired.
|
||||
// If the keypair doesn't exist, it returns ipn.ErrStateNotExist.
|
||||
func (b *LocalBackend) getCertPEMCached(dir, domain string, now time.Time) (p *TLSCertKeyPair, err error) {
|
||||
func getCertPEMCached(cs certStore, domain string, now time.Time) (p *TLSCertKeyPair, err error) {
|
||||
if !validLookingCertDomain(domain) {
|
||||
// Before we read files from disk using it, validate it's halfway
|
||||
// reasonable looking.
|
||||
return nil, fmt.Errorf("invalid domain %q", domain)
|
||||
}
|
||||
return b.getCertStore(dir).Read(domain, now)
|
||||
return cs.Read(domain, now)
|
||||
}
|
||||
|
||||
func (b *LocalBackend) getCertPEM(ctx context.Context, logf logger.Logf, traceACME func(any), dir, domain string, now time.Time) (*TLSCertKeyPair, error) {
|
||||
func (b *LocalBackend) getCertPEM(ctx context.Context, cs certStore, logf logger.Logf, traceACME func(any), domain string, now time.Time) (*TLSCertKeyPair, error) {
|
||||
acmeMu.Lock()
|
||||
defer acmeMu.Unlock()
|
||||
|
||||
if p, err := b.getCertPEMCached(dir, domain, now); err == nil {
|
||||
if p, err := getCertPEMCached(cs, domain, now); err == nil {
|
||||
return p, nil
|
||||
} else if !errors.Is(err, ipn.ErrStateNotExist) && !errors.Is(err, errCertExpired) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
key, err := acmeKey(dir)
|
||||
key, err := acmeKey(cs)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("acmeKey: %w", err)
|
||||
}
|
||||
@@ -366,8 +413,7 @@ func (b *LocalBackend) getCertPEM(ctx context.Context, logf logger.Logf, traceAC
|
||||
if err := encodeECDSAKey(&privPEM, certPrivKey); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
certStore := b.getCertStore(dir)
|
||||
if err := certStore.WriteKey(domain, privPEM.Bytes()); err != nil {
|
||||
if err := cs.WriteKey(domain, privPEM.Bytes()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -390,7 +436,7 @@ func (b *LocalBackend) getCertPEM(ctx context.Context, logf logger.Logf, traceAC
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if err := certStore.WriteCert(domain, certPEM.Bytes()); err != nil {
|
||||
if err := cs.WriteCert(domain, certPEM.Bytes()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -444,14 +490,15 @@ func parsePrivateKey(der []byte) (crypto.Signer, error) {
|
||||
return nil, errors.New("acme/autocert: failed to parse private key")
|
||||
}
|
||||
|
||||
func acmeKey(dir string) (crypto.Signer, error) {
|
||||
pemName := filepath.Join(dir, "acme-account.key.pem")
|
||||
if v, err := os.ReadFile(pemName); err == nil {
|
||||
func acmeKey(cs certStore) (crypto.Signer, error) {
|
||||
if v, err := cs.ACMEKey(); err == nil {
|
||||
priv, _ := pem.Decode(v)
|
||||
if priv == nil || !strings.Contains(priv.Type, "PRIVATE") {
|
||||
return nil, errors.New("acme/autocert: invalid account key found in cache")
|
||||
}
|
||||
return parsePrivateKey(priv.Bytes)
|
||||
} else if err != nil && !errors.Is(err, ipn.ErrStateNotExist) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
privKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
@@ -462,7 +509,7 @@ func acmeKey(dir string) (crypto.Signer, error) {
|
||||
if err := encodeECDSAKey(&pemBuf, privKey); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := os.WriteFile(pemName, pemBuf.Bytes(), 0600); err != nil {
|
||||
if err := cs.WriteACMEKey(pemBuf.Bytes()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return privKey, nil
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
@@ -34,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"
|
||||
@@ -43,6 +45,8 @@ import (
|
||||
"tailscale.com/ipn/ipnauth"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/ipn/policy"
|
||||
"tailscale.com/log/sockstatlog"
|
||||
"tailscale.com/logpolicy"
|
||||
"tailscale.com/net/dns"
|
||||
"tailscale.com/net/dnscache"
|
||||
"tailscale.com/net/dnsfallback"
|
||||
@@ -60,6 +64,7 @@ import (
|
||||
"tailscale.com/types/empty"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/logid"
|
||||
"tailscale.com/types/netmap"
|
||||
"tailscale.com/types/persist"
|
||||
"tailscale.com/types/preftype"
|
||||
@@ -136,7 +141,7 @@ type LocalBackend struct {
|
||||
pm *profileManager
|
||||
store ipn.StateStore
|
||||
dialer *tsdial.Dialer // non-nil
|
||||
backendLogID string
|
||||
backendLogID logid.PublicID
|
||||
unregisterLinkMon func()
|
||||
unregisterHealthWatch func()
|
||||
portpoll *portlist.Poller // may be nil
|
||||
@@ -149,6 +154,23 @@ type LocalBackend struct {
|
||||
sshAtomicBool atomic.Bool
|
||||
shutdownCalled bool // if Shutdown has been called
|
||||
debugSink *capture.Sink
|
||||
sockstatLogger *sockstatlog.Logger
|
||||
|
||||
// getTCPHandlerForFunnelFlow returns a handler for an incoming TCP flow for
|
||||
// the provided srcAddr and dstPort if one exists.
|
||||
//
|
||||
// srcAddr is the source address of the flow, not the address of the Funnel
|
||||
// node relaying the flow.
|
||||
// dstPort is the destination port of the flow.
|
||||
//
|
||||
// It returns nil if there is no known handler for this flow.
|
||||
//
|
||||
// This is specifically used to handle TCP flows for Funnel connections to tsnet
|
||||
// servers.
|
||||
//
|
||||
// It is set once during initialization, and can be nil if SetTCPHandlerForFunnelFlow
|
||||
// is never called.
|
||||
getTCPHandlerForFunnelFlow func(srcAddr netip.AddrPort, dstPort uint16) (handler func(net.Conn))
|
||||
|
||||
// lastProfileID tracks the last profile we've seen from the ProfileManager.
|
||||
// It's used to detect when the user has changed their profile.
|
||||
@@ -245,7 +267,7 @@ type clientGen func(controlclient.Options) (controlclient.Client, error)
|
||||
// but is not actually running.
|
||||
//
|
||||
// If dialer is nil, a new one is made.
|
||||
func NewLocalBackend(logf logger.Logf, logid string, store ipn.StateStore, dialer *tsdial.Dialer, e wgengine.Engine, loginFlags controlclient.LoginFlags) (*LocalBackend, error) {
|
||||
func NewLocalBackend(logf logger.Logf, logID logid.PublicID, store ipn.StateStore, dialer *tsdial.Dialer, e wgengine.Engine, loginFlags controlclient.LoginFlags) (*LocalBackend, error) {
|
||||
if e == nil {
|
||||
panic("ipn.NewLocalBackend: engine must not be nil")
|
||||
}
|
||||
@@ -254,6 +276,9 @@ func NewLocalBackend(logf logger.Logf, logid string, store ipn.StateStore, diale
|
||||
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)
|
||||
@@ -278,9 +303,9 @@ func NewLocalBackend(logf logger.Logf, logid string, store ipn.StateStore, diale
|
||||
statsLogf: logger.LogOnChange(logf, 5*time.Minute, time.Now),
|
||||
e: e,
|
||||
pm: pm,
|
||||
store: pm.Store(),
|
||||
store: store,
|
||||
dialer: dialer,
|
||||
backendLogID: logid,
|
||||
backendLogID: logID,
|
||||
state: ipn.NoState,
|
||||
portpoll: portpoll,
|
||||
em: newExpiryManager(logf),
|
||||
@@ -288,6 +313,15 @@ func NewLocalBackend(logf logger.Logf, logid string, store ipn.StateStore, diale
|
||||
loginFlags: loginFlags,
|
||||
}
|
||||
|
||||
b.sockstatLogger, err = sockstatlog.NewLogger(logpolicy.LogsDir(logf), logf, logID)
|
||||
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.
|
||||
b.setFilter(filter.NewAllowNone(logf, &netipx.IPSet{}))
|
||||
|
||||
@@ -336,6 +370,7 @@ type componentLogState struct {
|
||||
|
||||
var debuggableComponents = []string{
|
||||
"magicsock",
|
||||
"sockstats",
|
||||
}
|
||||
|
||||
func componentStateKey(component string) ipn.StateKey {
|
||||
@@ -348,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()
|
||||
@@ -360,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)
|
||||
@@ -525,6 +572,10 @@ func (b *LocalBackend) Shutdown() {
|
||||
}
|
||||
b.mu.Unlock()
|
||||
|
||||
if b.sockstatLogger != nil {
|
||||
b.sockstatLogger.Shutdown()
|
||||
}
|
||||
|
||||
b.unregisterLinkMon()
|
||||
b.unregisterHealthWatch()
|
||||
if cc != nil {
|
||||
@@ -1262,7 +1313,7 @@ func (b *LocalBackend) Start(opts ipn.Options) error {
|
||||
}
|
||||
|
||||
hostinfo := hostinfo.New()
|
||||
hostinfo.BackendLogID = b.backendLogID
|
||||
hostinfo.BackendLogID = b.backendLogID.String()
|
||||
hostinfo.FrontendLogID = opts.FrontendLogID
|
||||
hostinfo.Userspace.Set(wgengine.IsNetstack(b.e))
|
||||
hostinfo.UserspaceRouter.Set(wgengine.IsNetstackRouter(b.e))
|
||||
@@ -1416,7 +1467,7 @@ func (b *LocalBackend) Start(opts ipn.Options) error {
|
||||
|
||||
b.e.SetNetInfoCallback(b.setNetInfo)
|
||||
|
||||
blid := b.backendLogID
|
||||
blid := b.backendLogID.String()
|
||||
b.logf("Backend: logs: be:%v fe:%v", blid, opts.FrontendLogID)
|
||||
b.send(ipn.Notify{BackendLogID: &blid})
|
||||
b.send(ipn.Notify{Prefs: &prefs})
|
||||
@@ -2520,9 +2571,6 @@ func (b *LocalBackend) checkSSHPrefsLocked(p *ipn.Prefs) error {
|
||||
if version.IsSandboxedMacOS() {
|
||||
return errors.New("The Tailscale SSH server does not run in sandboxed Tailscale GUI builds.")
|
||||
}
|
||||
if !envknob.UseWIPCode() {
|
||||
return errors.New("The Tailscale SSH server is disabled on macOS tailscaled by default. To try, set env TAILSCALE_USE_WIP_CODE=1")
|
||||
}
|
||||
case "freebsd", "openbsd":
|
||||
default:
|
||||
return errors.New("The Tailscale SSH server is not supported on " + runtime.GOOS)
|
||||
@@ -3117,6 +3165,12 @@ func dnsConfigForNetmap(nm *netmap.NetworkMap, prefs ipn.PrefsView, logf logger.
|
||||
return dcfg
|
||||
}
|
||||
|
||||
// SetTCPHandlerForFunnelFlow sets the TCP handler for Funnel flows.
|
||||
// It should only be called before the LocalBackend is used.
|
||||
func (b *LocalBackend) SetTCPHandlerForFunnelFlow(h func(src netip.AddrPort, dstPort uint16) (handler func(net.Conn))) {
|
||||
b.getTCPHandlerForFunnelFlow = h
|
||||
}
|
||||
|
||||
// SetVarRoot sets the root directory of Tailscale's writable
|
||||
// storage area . (e.g. "/var/lib/tailscale")
|
||||
//
|
||||
@@ -3326,7 +3380,7 @@ var (
|
||||
// peerRoutes returns the routerConfig.Routes to access peers.
|
||||
// If there are over cgnatThreshold CGNAT routes, one big CGNAT route
|
||||
// is used instead.
|
||||
func peerRoutes(peers []wgcfg.Peer, cgnatThreshold int) (routes []netip.Prefix) {
|
||||
func peerRoutes(logf logger.Logf, peers []wgcfg.Peer, cgnatThreshold int) (routes []netip.Prefix) {
|
||||
tsULA := tsaddr.TailscaleULARange()
|
||||
cgNAT := tsaddr.CGNATRange()
|
||||
var didULA bool
|
||||
@@ -3334,6 +3388,18 @@ func peerRoutes(peers []wgcfg.Peer, cgnatThreshold int) (routes []netip.Prefix)
|
||||
for _, peer := range peers {
|
||||
for _, aip := range peer.AllowedIPs {
|
||||
aip = unmapIPPrefix(aip)
|
||||
|
||||
// Ensure that we're only accepting properly-masked
|
||||
// prefixes; the control server should be masking
|
||||
// these, so if we get them, skip.
|
||||
if mm := aip.Masked(); aip != mm {
|
||||
// To avoid a DoS where a peer could cause all
|
||||
// reconfigs to fail by sending a bad prefix, we just
|
||||
// skip, but don't error, on an unmasked route.
|
||||
logf("advertised route %s from %s has non-address bits set; expected %s", aip, peer.PublicKey.ShortString(), mm)
|
||||
continue
|
||||
}
|
||||
|
||||
// Only add the Tailscale IPv6 ULA once, if we see anybody using part of it.
|
||||
if aip.Addr().Is6() && aip.IsSingleIP() && tsULA.Contains(aip.Addr()) {
|
||||
if !didULA {
|
||||
@@ -3366,12 +3432,13 @@ func (b *LocalBackend) routerConfig(cfg *wgcfg.Config, prefs ipn.PrefsView, oneC
|
||||
if oneCGNATRoute {
|
||||
singleRouteThreshold = 1
|
||||
}
|
||||
|
||||
rs := &router.Config{
|
||||
LocalAddrs: unmapIPPrefixes(cfg.Addresses),
|
||||
SubnetRoutes: unmapIPPrefixes(prefs.AdvertiseRoutes().AsSlice()),
|
||||
SNATSubnetRoutes: !prefs.NoSNAT(),
|
||||
NetfilterMode: prefs.NetfilterMode(),
|
||||
Routes: peerRoutes(cfg.Peers, singleRouteThreshold),
|
||||
Routes: peerRoutes(b.logf, cfg.Peers, singleRouteThreshold),
|
||||
}
|
||||
|
||||
if distro.Get() == distro.Synology {
|
||||
@@ -3669,6 +3736,21 @@ func (b *LocalBackend) resetControlClientLockedAsync() {
|
||||
if b.cc == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// When we clear the control client, stop any outstanding netmap expiry
|
||||
// timer; synthesizing a new netmap while we don't have a control
|
||||
// client will break things.
|
||||
//
|
||||
// See https://github.com/tailscale/tailscale/issues/7392
|
||||
if b.nmExpiryTimer != nil {
|
||||
b.nmExpiryTimer.Stop()
|
||||
b.nmExpiryTimer = nil
|
||||
|
||||
// Also bump the epoch to ensure that if the timer started, it
|
||||
// will abort.
|
||||
b.numClientStatusCalls.Add(1)
|
||||
}
|
||||
|
||||
go b.cc.Shutdown()
|
||||
b.cc = nil
|
||||
b.ccAuto = nil
|
||||
@@ -4626,7 +4708,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
|
||||
@@ -4740,6 +4825,9 @@ func (b *LocalBackend) initTKALocked() error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("initializing tka: %v", err)
|
||||
}
|
||||
if err := authority.Compact(storage, tkaCompactionDefaults); err != nil {
|
||||
b.logf("tka compaction failed: %v", err)
|
||||
}
|
||||
|
||||
b.tka = &tkaState{
|
||||
profile: cp.ID,
|
||||
@@ -4866,3 +4954,25 @@ func (b *LocalBackend) StreamDebugCapture(ctx context.Context, w io.Writer) erro
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *LocalBackend) GetPeerEndpointChanges(ctx context.Context, ip netip.Addr) ([]magicsock.EndpointChange, error) {
|
||||
pip, ok := b.e.PeerForIP(ip)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("no matching peer")
|
||||
}
|
||||
if pip.IsSelf {
|
||||
return nil, fmt.Errorf("%v is local Tailscale IP", ip)
|
||||
}
|
||||
peer := pip.Node
|
||||
|
||||
mc, err := b.magicConn()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("getting magicsock conn: %w", err)
|
||||
}
|
||||
|
||||
chs, err := mc.GetEndpointChanges(peer)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("getting endpoint changes: %w", err)
|
||||
}
|
||||
return chs, nil
|
||||
}
|
||||
|
||||
@@ -21,7 +21,9 @@ import (
|
||||
"tailscale.com/net/tsaddr"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tstest"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/logid"
|
||||
"tailscale.com/types/netmap"
|
||||
"tailscale.com/wgengine"
|
||||
"tailscale.com/wgengine/filter"
|
||||
@@ -333,10 +335,25 @@ func TestPeerRoutes(t *testing.T) {
|
||||
pp("100.64.0.2/32"),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "skip-unmasked-prefixes",
|
||||
peers: []wgcfg.Peer{
|
||||
{
|
||||
PublicKey: key.NewNode().Public(),
|
||||
AllowedIPs: []netip.Prefix{
|
||||
pp("100.64.0.2/32"),
|
||||
pp("10.0.0.100/16"),
|
||||
},
|
||||
},
|
||||
},
|
||||
want: []netip.Prefix{
|
||||
pp("100.64.0.2/32"),
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := peerRoutes(tt.peers, 2)
|
||||
got := peerRoutes(t.Logf, tt.peers, 2)
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("got = %v; want %v", got, tt.want)
|
||||
}
|
||||
@@ -481,8 +498,7 @@ func (panicOnUseTransport) RoundTrip(*http.Request) (*http.Response, error) {
|
||||
|
||||
// Issue 1573: don't generate a machine key if we don't want to be running.
|
||||
func TestLazyMachineKeyGeneration(t *testing.T) {
|
||||
defer func(old func() bool) { panicOnMachineKeyGeneration = old }(panicOnMachineKeyGeneration)
|
||||
panicOnMachineKeyGeneration = func() bool { return true }
|
||||
tstest.Replace(t, &panicOnMachineKeyGeneration, func() bool { return true })
|
||||
|
||||
var logf logger.Logf = logger.Discard
|
||||
store := new(mem.Store)
|
||||
@@ -491,7 +507,7 @@ func TestLazyMachineKeyGeneration(t *testing.T) {
|
||||
t.Fatalf("NewFakeUserspaceEngine: %v", err)
|
||||
}
|
||||
t.Cleanup(eng.Close)
|
||||
lb, err := NewLocalBackend(logf, "logid", store, nil, eng, 0)
|
||||
lb, err := NewLocalBackend(logf, logid.PublicID{}, store, nil, eng, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("NewLocalBackend: %v", err)
|
||||
}
|
||||
@@ -755,7 +771,7 @@ func TestStatusWithoutPeers(t *testing.T) {
|
||||
}
|
||||
t.Cleanup(e.Close)
|
||||
|
||||
b, err := NewLocalBackend(logf, "logid", store, nil, e, 0)
|
||||
b, err := NewLocalBackend(logf, logid.PublicID{}, store, nil, e, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("NewLocalBackend: %v", err)
|
||||
}
|
||||
|
||||
@@ -11,11 +11,11 @@ import (
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/ipn/store/mem"
|
||||
"tailscale.com/logtail"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tstest"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/logid"
|
||||
"tailscale.com/types/persist"
|
||||
"tailscale.com/wgengine"
|
||||
)
|
||||
@@ -37,8 +37,8 @@ func TestLocalLogLines(t *testing.T) {
|
||||
// This lets the logListen tracker verify that the rate-limiter allows these key lines.
|
||||
logf := logger.RateLimitedFnWithClock(logListen.Logf, 5*time.Second, 0, 10, time.Now)
|
||||
|
||||
logid := func(hex byte) logtail.PublicID {
|
||||
var ret logtail.PublicID
|
||||
logid := func(hex byte) logid.PublicID {
|
||||
var ret logid.PublicID
|
||||
for i := 0; i < len(ret); i++ {
|
||||
ret[i] = hex
|
||||
}
|
||||
@@ -54,7 +54,7 @@ func TestLocalLogLines(t *testing.T) {
|
||||
}
|
||||
t.Cleanup(e.Close)
|
||||
|
||||
lb, err := NewLocalBackend(logf, idA.String(), store, nil, e, 0)
|
||||
lb, err := NewLocalBackend(logf, idA, store, nil, e, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -6,7 +6,9 @@ package ipnlocal
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/ed25519"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
@@ -37,6 +39,11 @@ import (
|
||||
var (
|
||||
errMissingNetmap = errors.New("missing netmap: verify that you are logged in")
|
||||
errNetworkLockNotActive = errors.New("network-lock is not active")
|
||||
|
||||
tkaCompactionDefaults = tka.CompactionOptions{
|
||||
MinChain: 24, // Keep at minimum 24 AUMs since head.
|
||||
MinAge: 14 * 24 * time.Hour, // Keep 2 weeks of AUMs.
|
||||
}
|
||||
)
|
||||
|
||||
type tkaState struct {
|
||||
@@ -99,6 +106,7 @@ func (b *LocalBackend) tkaFilterNetmapLocked(nm *netmap.NetworkMap) {
|
||||
ID: p.ID,
|
||||
StableID: p.StableID,
|
||||
TailscaleIPs: make([]netip.Addr, len(p.Addresses)),
|
||||
NodeKey: p.Key,
|
||||
}
|
||||
for i, addr := range p.Addresses {
|
||||
if addr.IsSingleIP() && tsaddr.IsTailscaleIP(addr.Addr()) {
|
||||
@@ -784,6 +792,98 @@ func (b *LocalBackend) NetworkLockLog(maxEntries int) ([]ipnstate.NetworkLockUpd
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// NetworkLockAffectedSigs returns the signatures which would be invalidated
|
||||
// by removing trust in the specified KeyID.
|
||||
func (b *LocalBackend) NetworkLockAffectedSigs(keyID tkatype.KeyID) ([]tkatype.MarshaledSignature, error) {
|
||||
var (
|
||||
ourNodeKey key.NodePublic
|
||||
err error
|
||||
)
|
||||
b.mu.Lock()
|
||||
if p := b.pm.CurrentPrefs(); p.Valid() && p.Persist().Valid() && !p.Persist().PrivateNodeKey().IsZero() {
|
||||
ourNodeKey = p.Persist().PublicNodeKey()
|
||||
}
|
||||
if b.tka == nil {
|
||||
err = errNetworkLockNotActive
|
||||
}
|
||||
b.mu.Unlock()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := b.tkaReadAffectedSigs(ourNodeKey, keyID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
if b.tka == nil {
|
||||
return nil, errNetworkLockNotActive
|
||||
}
|
||||
|
||||
// Confirm for ourselves tha the signatures would actually be invalidated
|
||||
// by removal of trusted in the specified key.
|
||||
for i, sigBytes := range resp.Signatures {
|
||||
var sig tka.NodeKeySignature
|
||||
if err := sig.Unserialize(sigBytes); err != nil {
|
||||
return nil, fmt.Errorf("failed decoding signature %d: %w", i, err)
|
||||
}
|
||||
|
||||
sigKeyID, err := sig.UnverifiedAuthorizingKeyID()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("extracting SigID from signature %d: %w", i, err)
|
||||
}
|
||||
if !bytes.Equal(keyID, sigKeyID) {
|
||||
return nil, fmt.Errorf("got signature with keyID %X from request for %X", sigKeyID, keyID)
|
||||
}
|
||||
|
||||
var nodeKey key.NodePublic
|
||||
if err := nodeKey.UnmarshalBinary(sig.Pubkey); err != nil {
|
||||
return nil, fmt.Errorf("failed decoding pubkey for signature %d: %w", i, err)
|
||||
}
|
||||
if err := b.tka.authority.NodeKeyAuthorized(nodeKey, sigBytes); err != nil {
|
||||
return nil, fmt.Errorf("signature %d is not valid: %w", i, err)
|
||||
}
|
||||
}
|
||||
|
||||
return resp.Signatures, nil
|
||||
}
|
||||
|
||||
var tkaSuffixEncoder = base64.RawStdEncoding
|
||||
|
||||
// NetworkLockWrapPreauthKey wraps a pre-auth key with information to
|
||||
// enable unattended bringup in the locked tailnet.
|
||||
//
|
||||
// The provided trusted tailnet-lock key is used to sign
|
||||
// a SigCredential structure, which is encoded along with the
|
||||
// private key and appended to the pre-auth key.
|
||||
func (b *LocalBackend) NetworkLockWrapPreauthKey(preauthKey string, tkaKey key.NLPrivate) (string, error) {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
if b.tka == nil {
|
||||
return "", errNetworkLockNotActive
|
||||
}
|
||||
|
||||
pub, priv, err := ed25519.GenerateKey(nil) // nil == crypto/rand
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
sig := tka.NodeKeySignature{
|
||||
SigKind: tka.SigCredential,
|
||||
KeyID: tkaKey.KeyID(),
|
||||
WrappingPubkey: pub,
|
||||
}
|
||||
sig.Signature, err = tkaKey.SignNKS(sig.SigHash())
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("signing failed: %w", err)
|
||||
}
|
||||
|
||||
b.logf("Generated network-lock credential signature using %s", tkaKey.Public().CLIString())
|
||||
return fmt.Sprintf("%s--TL%s-%s", preauthKey, tkaSuffixEncoder.EncodeToString(sig.Serialize()), tkaSuffixEncoder.EncodeToString(priv)), nil
|
||||
}
|
||||
|
||||
func signNodeKey(nodeInfo tailcfg.TKASignInfo, signer key.NLPrivate) (*tka.NodeKeySignature, error) {
|
||||
p, err := nodeInfo.NodePublic.MarshalBinary()
|
||||
if err != nil {
|
||||
@@ -1110,3 +1210,39 @@ func (b *LocalBackend) tkaSubmitSignature(ourNodeKey key.NodePublic, sig tkatype
|
||||
|
||||
return a, nil
|
||||
}
|
||||
|
||||
func (b *LocalBackend) tkaReadAffectedSigs(ourNodeKey key.NodePublic, key tkatype.KeyID) (*tailcfg.TKASignaturesUsingKeyResponse, error) {
|
||||
var encodedReq bytes.Buffer
|
||||
if err := json.NewEncoder(&encodedReq).Encode(tailcfg.TKASignaturesUsingKeyRequest{
|
||||
Version: tailcfg.CurrentCapabilityVersion,
|
||||
NodeKey: ourNodeKey,
|
||||
KeyID: key,
|
||||
}); err != nil {
|
||||
return nil, fmt.Errorf("encoding request: %v", err)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
|
||||
defer cancel()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", "https://unused/machine/tka/affected-sigs", &encodedReq)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("req: %w", err)
|
||||
}
|
||||
resp, err := b.DoNoiseRequest(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("resp: %w", err)
|
||||
}
|
||||
if resp.StatusCode != 200 {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
resp.Body.Close()
|
||||
return nil, fmt.Errorf("request returned (%d): %s", resp.StatusCode, string(body))
|
||||
}
|
||||
a := new(tailcfg.TKASignaturesUsingKeyResponse)
|
||||
err = json.NewDecoder(&io.LimitedReader{R: resp.Body, N: 1024 * 1024}).Decode(a)
|
||||
resp.Body.Close()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("decoding JSON: %w", err)
|
||||
}
|
||||
|
||||
return a, nil
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
@@ -877,3 +878,135 @@ func TestTKAForceDisable(t *testing.T) {
|
||||
t.Fatal("tka was re-initalized")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTKAAffectedSigs(t *testing.T) {
|
||||
nodePriv := key.NewNode()
|
||||
// toSign := key.NewNode()
|
||||
nlPriv := key.NewNLPrivate()
|
||||
|
||||
pm := must.Get(newProfileManager(new(mem.Store), t.Logf))
|
||||
must.Do(pm.SetPrefs((&ipn.Prefs{
|
||||
Persist: &persist.Persist{
|
||||
PrivateNodeKey: nodePriv,
|
||||
NetworkLockKey: nlPriv,
|
||||
},
|
||||
}).View()))
|
||||
|
||||
// Make a fake TKA authority, to seed local state.
|
||||
disablementSecret := bytes.Repeat([]byte{0xa5}, 32)
|
||||
tkaKey := tka.Key{Kind: tka.Key25519, Public: nlPriv.Public().Verifier(), Votes: 2}
|
||||
|
||||
temp := t.TempDir()
|
||||
tkaPath := filepath.Join(temp, "tka-profile", string(pm.CurrentProfile().ID))
|
||||
os.Mkdir(tkaPath, 0755)
|
||||
chonk, err := tka.ChonkDir(tkaPath)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
authority, _, err := tka.Create(chonk, tka.State{
|
||||
Keys: []tka.Key{tkaKey},
|
||||
DisablementSecrets: [][]byte{tka.DisablementKDF(disablementSecret)},
|
||||
}, nlPriv)
|
||||
if err != nil {
|
||||
t.Fatalf("tka.Create() failed: %v", err)
|
||||
}
|
||||
|
||||
untrustedKey := key.NewNLPrivate()
|
||||
tcs := []struct {
|
||||
name string
|
||||
makeSig func() *tka.NodeKeySignature
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
"no error",
|
||||
func() *tka.NodeKeySignature {
|
||||
sig, _ := signNodeKey(tailcfg.TKASignInfo{NodePublic: nodePriv.Public()}, nlPriv)
|
||||
return sig
|
||||
},
|
||||
"",
|
||||
},
|
||||
{
|
||||
"signature for different keyID",
|
||||
func() *tka.NodeKeySignature {
|
||||
sig, _ := signNodeKey(tailcfg.TKASignInfo{NodePublic: nodePriv.Public()}, untrustedKey)
|
||||
return sig
|
||||
},
|
||||
fmt.Sprintf("got signature with keyID %X from request for %X", untrustedKey.KeyID(), nlPriv.KeyID()),
|
||||
},
|
||||
{
|
||||
"invalid signature",
|
||||
func() *tka.NodeKeySignature {
|
||||
sig, _ := signNodeKey(tailcfg.TKASignInfo{NodePublic: nodePriv.Public()}, nlPriv)
|
||||
copy(sig.Signature, []byte{1, 2, 3, 4, 5, 6}) // overwrite with trash to invalid signature
|
||||
return sig
|
||||
},
|
||||
"signature 0 is not valid: invalid signature",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tcs {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
s := tc.makeSig()
|
||||
ts, client := fakeNoiseServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
defer r.Body.Close()
|
||||
switch r.URL.Path {
|
||||
case "/machine/tka/affected-sigs":
|
||||
body := new(tailcfg.TKASignaturesUsingKeyRequest)
|
||||
if err := json.NewDecoder(r.Body).Decode(body); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if body.Version != tailcfg.CurrentCapabilityVersion {
|
||||
t.Errorf("sign CapVer = %v, want %v", body.Version, tailcfg.CurrentCapabilityVersion)
|
||||
}
|
||||
if body.NodeKey != nodePriv.Public() {
|
||||
t.Errorf("nodeKey = %v, want %v", body.NodeKey, nodePriv.Public())
|
||||
}
|
||||
|
||||
w.WriteHeader(200)
|
||||
if err := json.NewEncoder(w).Encode(tailcfg.TKASignaturesUsingKeyResponse{
|
||||
Signatures: []tkatype.MarshaledSignature{s.Serialize()},
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
default:
|
||||
t.Errorf("unhandled endpoint path: %v", r.URL.Path)
|
||||
w.WriteHeader(404)
|
||||
}
|
||||
}))
|
||||
defer ts.Close()
|
||||
cc := fakeControlClient(t, client)
|
||||
b := LocalBackend{
|
||||
varRoot: temp,
|
||||
cc: cc,
|
||||
ccAuto: cc,
|
||||
logf: t.Logf,
|
||||
tka: &tkaState{
|
||||
authority: authority,
|
||||
storage: chonk,
|
||||
},
|
||||
pm: pm,
|
||||
store: pm.Store(),
|
||||
}
|
||||
|
||||
sigs, err := b.NetworkLockAffectedSigs(nlPriv.KeyID())
|
||||
switch {
|
||||
case tc.wantErr == "" && err != nil:
|
||||
t.Errorf("NetworkLockAffectedSigs() failed: %v", err)
|
||||
case tc.wantErr != "" && err == nil:
|
||||
t.Errorf("NetworkLockAffectedSigs().err = nil, want %q", tc.wantErr)
|
||||
case tc.wantErr != "" && err.Error() != tc.wantErr:
|
||||
t.Errorf("NetworkLockAffectedSigs().err = %q, want %q", err.Error(), tc.wantErr)
|
||||
}
|
||||
|
||||
if tc.wantErr == "" {
|
||||
if len(sigs) != 1 {
|
||||
t.Fatalf("len(sigs) = %d, want 1", len(sigs))
|
||||
}
|
||||
if !bytes.Equal(s.Serialize(), sigs[0]) {
|
||||
t.Errorf("unexpected signature: got %v, want %v", sigs[0], s.Serialize())
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,6 +45,7 @@ import (
|
||||
"tailscale.com/net/interfaces"
|
||||
"tailscale.com/net/netaddr"
|
||||
"tailscale.com/net/netutil"
|
||||
"tailscale.com/net/sockstats"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/util/clientmetric"
|
||||
"tailscale.com/util/multierr"
|
||||
@@ -709,6 +710,8 @@ func (h *peerAPIHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
case "/v0/doctor":
|
||||
h.handleServeDoctor(w, r)
|
||||
case "/v0/sockstats":
|
||||
h.handleServeSockStats(w, r)
|
||||
return
|
||||
case "/v0/ingress":
|
||||
metricIngressCalls.Add(1)
|
||||
@@ -758,12 +761,12 @@ func (h *peerAPIHandler) handleServeIngress(w http.ResponseWriter, r *http.Reque
|
||||
bad("Tailscale-Ingress-Src header invalid; want ip:port")
|
||||
return
|
||||
}
|
||||
target := r.Header.Get("Tailscale-Ingress-Target")
|
||||
target := ipn.HostPort(r.Header.Get("Tailscale-Ingress-Target"))
|
||||
if target == "" {
|
||||
bad("Tailscale-Ingress-Target header not set")
|
||||
return
|
||||
}
|
||||
if _, _, err := net.SplitHostPort(target); err != nil {
|
||||
if _, _, err := net.SplitHostPort(string(target)); err != nil {
|
||||
bad("Tailscale-Ingress-Target header invalid; want host:port")
|
||||
return
|
||||
}
|
||||
@@ -776,13 +779,17 @@ func (h *peerAPIHandler) handleServeIngress(w http.ResponseWriter, r *http.Reque
|
||||
return nil, false
|
||||
}
|
||||
io.WriteString(conn, "HTTP/1.1 101 Switching Protocols\r\n\r\n")
|
||||
return conn, true
|
||||
return &ipn.FunnelConn{
|
||||
Conn: conn,
|
||||
Src: srcAddr,
|
||||
Target: target,
|
||||
}, true
|
||||
}
|
||||
sendRST := func() {
|
||||
http.Error(w, "denied", http.StatusForbidden)
|
||||
}
|
||||
|
||||
h.ps.b.HandleIngressTCPConn(h.peerNode, ipn.HostPort(target), srcAddr, getConn, sendRST)
|
||||
h.ps.b.HandleIngressTCPConn(h.peerNode, target, srcAddr, getConn, sendRST)
|
||||
}
|
||||
|
||||
func (h *peerAPIHandler) handleServeInterfaces(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -850,6 +857,93 @@ func (h *peerAPIHandler) handleServeDoctor(w http.ResponseWriter, r *http.Reques
|
||||
fmt.Fprintln(w, "</pre>")
|
||||
}
|
||||
|
||||
func (h *peerAPIHandler) handleServeSockStats(w http.ResponseWriter, r *http.Request) {
|
||||
if !h.canDebug() {
|
||||
http.Error(w, "denied; no debug access", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
fmt.Fprintln(w, "<!DOCTYPE html><h1>Socket Stats</h1>")
|
||||
|
||||
stats, interfaceStats, validation := sockstats.Get(), sockstats.GetInterfaces(), sockstats.GetValidation()
|
||||
if stats == nil {
|
||||
fmt.Fprintln(w, "No socket stats available")
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Fprintln(w, "<table border='1' cellspacing='0' style='border-collapse: collapse;'>")
|
||||
fmt.Fprintln(w, "<thead>")
|
||||
fmt.Fprintln(w, "<th>Label</th>")
|
||||
fmt.Fprintln(w, "<th>Tx</th>")
|
||||
fmt.Fprintln(w, "<th>Rx</th>")
|
||||
for _, iface := range interfaceStats.Interfaces {
|
||||
fmt.Fprintf(w, "<th>Tx (%s)</th>", html.EscapeString(iface))
|
||||
fmt.Fprintf(w, "<th>Rx (%s)</th>", html.EscapeString(iface))
|
||||
}
|
||||
fmt.Fprintln(w, "<th>Validation</th>")
|
||||
fmt.Fprintln(w, "</thead>")
|
||||
|
||||
fmt.Fprintln(w, "<tbody>")
|
||||
labels := make([]sockstats.Label, 0, len(stats.Stats))
|
||||
for label := range stats.Stats {
|
||||
labels = append(labels, label)
|
||||
}
|
||||
slices.SortFunc(labels, func(a, b sockstats.Label) bool {
|
||||
return a.String() < b.String()
|
||||
})
|
||||
|
||||
txTotal := uint64(0)
|
||||
rxTotal := uint64(0)
|
||||
txTotalByInterface := map[string]uint64{}
|
||||
rxTotalByInterface := map[string]uint64{}
|
||||
|
||||
for _, label := range labels {
|
||||
stat := stats.Stats[label]
|
||||
fmt.Fprintln(w, "<tr>")
|
||||
fmt.Fprintf(w, "<td>%s</td>", html.EscapeString(label.String()))
|
||||
fmt.Fprintf(w, "<td align=right>%d</td>", stat.TxBytes)
|
||||
fmt.Fprintf(w, "<td align=right>%d</td>", stat.RxBytes)
|
||||
|
||||
txTotal += stat.TxBytes
|
||||
rxTotal += stat.RxBytes
|
||||
|
||||
if interfaceStat, ok := interfaceStats.Stats[label]; ok {
|
||||
for _, iface := range interfaceStats.Interfaces {
|
||||
fmt.Fprintf(w, "<td align=right>%d</td>", interfaceStat.TxBytesByInterface[iface])
|
||||
fmt.Fprintf(w, "<td align=right>%d</td>", interfaceStat.RxBytesByInterface[iface])
|
||||
txTotalByInterface[iface] += interfaceStat.TxBytesByInterface[iface]
|
||||
rxTotalByInterface[iface] += interfaceStat.RxBytesByInterface[iface]
|
||||
}
|
||||
}
|
||||
|
||||
if validationStat, ok := validation.Stats[label]; ok && (validationStat.RxBytes > 0 || validationStat.TxBytes > 0) {
|
||||
fmt.Fprintf(w, "<td>Tx=%d (%+d) Rx=%d (%+d)</td>",
|
||||
validationStat.TxBytes,
|
||||
int64(validationStat.TxBytes)-int64(stat.TxBytes),
|
||||
validationStat.RxBytes,
|
||||
int64(validationStat.RxBytes)-int64(stat.RxBytes))
|
||||
} else {
|
||||
fmt.Fprintln(w, "<td></td>")
|
||||
}
|
||||
|
||||
fmt.Fprintln(w, "</tr>")
|
||||
}
|
||||
fmt.Fprintln(w, "</tbody>")
|
||||
|
||||
fmt.Fprintln(w, "<tfoot>")
|
||||
fmt.Fprintln(w, "<th>Total</th>")
|
||||
fmt.Fprintf(w, "<th>%d</th>", txTotal)
|
||||
fmt.Fprintf(w, "<th>%d</th>", rxTotal)
|
||||
for _, iface := range interfaceStats.Interfaces {
|
||||
fmt.Fprintf(w, "<th>%d</th>", txTotalByInterface[iface])
|
||||
fmt.Fprintf(w, "<th>%d</th>", rxTotalByInterface[iface])
|
||||
}
|
||||
fmt.Fprintln(w, "<th></th>")
|
||||
fmt.Fprintln(w, "</tfoot>")
|
||||
|
||||
fmt.Fprintln(w, "</table>")
|
||||
}
|
||||
|
||||
type incomingFile struct {
|
||||
name string // "foo.jpg"
|
||||
started time.Time
|
||||
|
||||
@@ -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.
|
||||
}
|
||||
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)
|
||||
}
|
||||
@@ -281,9 +281,22 @@ func (b *LocalBackend) HandleIngressTCPConn(ingressPeer *tailcfg.Node, target ip
|
||||
sendRST()
|
||||
return
|
||||
}
|
||||
dport := uint16(port16)
|
||||
if b.getTCPHandlerForFunnelFlow != nil {
|
||||
handler := b.getTCPHandlerForFunnelFlow(srcAddr, dport)
|
||||
if handler != nil {
|
||||
c, ok := getConn()
|
||||
if !ok {
|
||||
b.logf("localbackend: getConn didn't complete from %v to port %v", srcAddr, dport)
|
||||
return
|
||||
}
|
||||
handler(c)
|
||||
return
|
||||
}
|
||||
}
|
||||
// TODO(bradfitz): pass ingressPeer etc in context to HandleInterceptedTCPConn,
|
||||
// extend serveHTTPContext or similar.
|
||||
b.HandleInterceptedTCPConn(uint16(port16), srcAddr, getConn, sendRST)
|
||||
b.HandleInterceptedTCPConn(dport, srcAddr, getConn, sendRST)
|
||||
}
|
||||
|
||||
func (b *LocalBackend) HandleInterceptedTCPConn(dport uint16, srcAddr netip.AddrPort, getConn func() (net.Conn, bool), sendRST func()) {
|
||||
@@ -426,18 +439,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
|
||||
}
|
||||
@@ -463,7 +484,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
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
"tailscale.com/types/empty"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/logid"
|
||||
"tailscale.com/types/netmap"
|
||||
"tailscale.com/types/persist"
|
||||
"tailscale.com/wgengine"
|
||||
@@ -303,7 +304,7 @@ func TestStateMachine(t *testing.T) {
|
||||
}
|
||||
t.Cleanup(e.Close)
|
||||
|
||||
b, err := NewLocalBackend(logf, "logid", store, nil, e, 0)
|
||||
b, err := NewLocalBackend(logf, logid.PublicID{}, store, nil, e, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("NewLocalBackend: %v", err)
|
||||
}
|
||||
@@ -946,7 +947,7 @@ func TestEditPrefsHasNoKeys(t *testing.T) {
|
||||
}
|
||||
t.Cleanup(e.Close)
|
||||
|
||||
b, err := NewLocalBackend(logf, "logid", new(mem.Store), nil, e, 0)
|
||||
b, err := NewLocalBackend(logf, logid.PublicID{}, new(mem.Store), nil, e, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("NewLocalBackend: %v", err)
|
||||
}
|
||||
@@ -1025,7 +1026,7 @@ func TestWGEngineStatusRace(t *testing.T) {
|
||||
eng, err := wgengine.NewFakeUserspaceEngine(logf, 0)
|
||||
c.Assert(err, qt.IsNil)
|
||||
t.Cleanup(eng.Close)
|
||||
b, err := NewLocalBackend(logf, "logid", new(mem.Store), nil, eng, 0)
|
||||
b, err := NewLocalBackend(logf, logid.PublicID{}, new(mem.Store), nil, eng, 0)
|
||||
c.Assert(err, qt.IsNil)
|
||||
|
||||
var cc *mockControl
|
||||
|
||||
@@ -37,8 +37,7 @@ func (s *Server) handleProxyConnectConn(w http.ResponseWriter, r *http.Request)
|
||||
return
|
||||
}
|
||||
|
||||
tr := logpolicy.NewLogtailTransport(logHost)
|
||||
back, err := tr.DialContext(ctx, "tcp", hostPort)
|
||||
back, err := logpolicy.DialContext(ctx, "tcp", hostPort)
|
||||
if err != nil {
|
||||
s.logf("error CONNECT dialing %v: %v", hostPort, err)
|
||||
http.Error(w, "Connect failure", http.StatusBadGateway)
|
||||
|
||||
@@ -25,6 +25,7 @@ import (
|
||||
"tailscale.com/ipn/ipnlocal"
|
||||
"tailscale.com/ipn/localapi"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/logid"
|
||||
"tailscale.com/util/mak"
|
||||
"tailscale.com/util/set"
|
||||
"tailscale.com/util/systemd"
|
||||
@@ -35,7 +36,7 @@ import (
|
||||
type Server struct {
|
||||
lb atomic.Pointer[ipnlocal.LocalBackend]
|
||||
logf logger.Logf
|
||||
backendLogID string
|
||||
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
|
||||
// being run in "client mode" that requires an active GUI
|
||||
@@ -412,9 +413,9 @@ func (s *Server) addActiveHTTPRequest(req *http.Request, ci *ipnauth.ConnIdentit
|
||||
//
|
||||
// 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 string) *Server {
|
||||
func New(logf logger.Logf, logID logid.PublicID) *Server {
|
||||
return &Server{
|
||||
backendLogID: logid,
|
||||
backendLogID: logID,
|
||||
logf: logf,
|
||||
resetOnZero: envknob.GOOS() == "windows",
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user