Compare commits
103 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0438c67e25 | ||
|
|
6842c3c194 | ||
|
|
576b08e5ee | ||
|
|
a8231b18cc | ||
|
|
6d98b5c9a8 | ||
|
|
fe33b17db3 | ||
|
|
5a98bbcbbb | ||
|
|
1e1c16bc24 | ||
|
|
ad504be066 | ||
|
|
6bdb9daec0 | ||
|
|
a3ce35d0c6 | ||
|
|
d1fc9bba7e | ||
|
|
9271e8a062 | ||
|
|
50cf21a779 | ||
|
|
ab998de989 | ||
|
|
44d73ce932 | ||
|
|
d8feeeee4c | ||
|
|
30380403d0 | ||
|
|
b49aa6ac31 | ||
|
|
586a88c710 | ||
|
|
e8b695626e | ||
|
|
c1daa42c24 | ||
|
|
6e5faff51e | ||
|
|
c8db70fd73 | ||
|
|
140b9aad5c | ||
|
|
e002260b62 | ||
|
|
06fff461dc | ||
|
|
b6aa1c1f22 | ||
|
|
b74db24149 | ||
|
|
65c9ce5a1b | ||
|
|
64547b2b86 | ||
|
|
fd92fbd69e | ||
|
|
d5100e0910 | ||
|
|
ba5aa2c486 | ||
|
|
5ca22a0068 | ||
|
|
4471e403aa | ||
|
|
6793685bba | ||
|
|
73399f784b | ||
|
|
fec888581a | ||
|
|
6edf357b96 | ||
|
|
c129bf1da1 | ||
|
|
71a7b8581d | ||
|
|
4fb663fbd2 | ||
|
|
58ad21b252 | ||
|
|
dd7057682c | ||
|
|
aea251d42a | ||
|
|
2df38b1feb | ||
|
|
3addcacfe9 | ||
|
|
eec734a578 | ||
|
|
3eb986fe05 | ||
|
|
ee6d18e35f | ||
|
|
287fe83f91 | ||
|
|
ef1c902c21 | ||
|
|
b657187a69 | ||
|
|
5f96d6211a | ||
|
|
72cc70ebfc | ||
|
|
3582628691 | ||
|
|
3a018e51bb | ||
|
|
6d85a94767 | ||
|
|
c1a2e2c380 | ||
|
|
3386a59cf1 | ||
|
|
006ec659e6 | ||
|
|
d9144c73a8 | ||
|
|
67f82e62a1 | ||
|
|
f011a0923a | ||
|
|
11ce5b7e57 | ||
|
|
5eded58924 | ||
|
|
355c3b2be7 | ||
|
|
61dfbc0a6e | ||
|
|
8a1201ac42 | ||
|
|
faf2d30439 | ||
|
|
25a0091f69 | ||
|
|
b76dffa594 | ||
|
|
6f18fbce8d | ||
|
|
2ac5474be1 | ||
|
|
3becf82dd3 | ||
|
|
1e67947cfa | ||
|
|
22ebb25e83 | ||
|
|
2afa1672ac | ||
|
|
237b1108b3 | ||
|
|
fff617c988 | ||
|
|
c684ca7a0c | ||
|
|
1116602d4c | ||
|
|
be67b8e75b | ||
|
|
8047dfa2dc | ||
|
|
ebbf5c57b3 | ||
|
|
39efba528f | ||
|
|
69c0b7e712 | ||
|
|
673b3d8dbd | ||
|
|
10eec37cd9 | ||
|
|
8f2bc0708b | ||
|
|
0088c5ddc0 | ||
|
|
907f85cd67 | ||
|
|
8724aa254f | ||
|
|
c4e262a0fc | ||
|
|
eafbf8886d | ||
|
|
b2b8e62476 | ||
|
|
91e64ca74f | ||
|
|
d72575eaaa | ||
|
|
b2c55e62c8 | ||
|
|
467ace7d0c | ||
|
|
aad6830df0 | ||
|
|
ea70aa3d98 |
5
.github/workflows/linux-race.yml
vendored
5
.github/workflows/linux-race.yml
vendored
@@ -29,11 +29,14 @@ jobs:
|
||||
go-version-file: go.mod
|
||||
id: go
|
||||
|
||||
- name: Build test wrapper
|
||||
run: ./tool/go build -o /tmp/testwrapper ./cmd/testwrapper
|
||||
|
||||
- name: Basic build
|
||||
run: go build ./cmd/...
|
||||
|
||||
- name: Run tests and benchmarks with -race flag on linux
|
||||
run: go test -race -bench=. -benchtime=1x ./...
|
||||
run: go test -exec=/tmp/testwrapper -race -bench=. -benchtime=1x ./...
|
||||
|
||||
- name: Check that no tracked files in the repo have been modified
|
||||
run: git diff --no-ext-diff --name-only --exit-code || (echo "Build/test modified the files above."; exit 1)
|
||||
|
||||
5
.github/workflows/linux.yml
vendored
5
.github/workflows/linux.yml
vendored
@@ -32,6 +32,9 @@ jobs:
|
||||
- name: Basic build
|
||||
run: go build ./cmd/...
|
||||
|
||||
- name: Build test wrapper
|
||||
run: ./tool/go build -o /tmp/testwrapper ./cmd/testwrapper
|
||||
|
||||
- name: Build variants
|
||||
run: |
|
||||
go install --tags=ts_include_cli ./cmd/tailscaled
|
||||
@@ -43,7 +46,7 @@ jobs:
|
||||
sudo apt-get -y install qemu-user
|
||||
|
||||
- name: Run tests on linux
|
||||
run: go test -bench=. -benchtime=1x ./...
|
||||
run: go test -exec=/tmp/testwrapper -bench=. -benchtime=1x ./...
|
||||
|
||||
- name: Check that no tracked files in the repo have been modified
|
||||
run: git diff --no-ext-diff --name-only --exit-code || (echo "Build/test modified the files above."; exit 1)
|
||||
|
||||
32
README.md
32
README.md
@@ -6,27 +6,41 @@ Private WireGuard® networks made easy
|
||||
|
||||
## Overview
|
||||
|
||||
This repository contains all the open source Tailscale client code and
|
||||
the `tailscaled` daemon and `tailscale` CLI tool. The `tailscaled`
|
||||
daemon runs on Linux, Windows and [macOS](https://tailscale.com/kb/1065/macos-variants/), and to varying degrees on FreeBSD, OpenBSD, and Darwin. (The Tailscale iOS and Android apps use this repo's code, but this repo doesn't contain the mobile GUI code.)
|
||||
This repository contains the majority of Tailscale's open source code.
|
||||
Notably, it includes the `tailscaled` daemon and
|
||||
the `tailscale` CLI tool. The `tailscaled` daemon runs on Linux, Windows,
|
||||
[macOS](https://tailscale.com/kb/1065/macos-variants/), and to varying degrees
|
||||
on FreeBSD and OpenBSD. The Tailscale iOS and Android apps use this repo's
|
||||
code, but this repo doesn't contain the mobile GUI code.
|
||||
|
||||
The Android app is at https://github.com/tailscale/tailscale-android
|
||||
Other [Tailscale repos](https://github.com/orgs/tailscale/repositories) of note:
|
||||
|
||||
The Synology package is at https://github.com/tailscale/tailscale-synology
|
||||
* the Android app is at https://github.com/tailscale/tailscale-android
|
||||
* the Synology package is at https://github.com/tailscale/tailscale-synology
|
||||
* the QNAP package is at https://github.com/tailscale/tailscale-qpkg
|
||||
* the Chocolatey packaging is at https://github.com/tailscale/tailscale-chocolatey
|
||||
|
||||
For background on which parts of Tailscale are open source and why,
|
||||
see [https://tailscale.com/opensource/](https://tailscale.com/opensource/).
|
||||
|
||||
## Using
|
||||
|
||||
We serve packages for a variety of distros at
|
||||
https://pkgs.tailscale.com .
|
||||
We serve packages for a variety of distros and platforms at
|
||||
[https://pkgs.tailscale.com](https://pkgs.tailscale.com/).
|
||||
|
||||
## Other clients
|
||||
|
||||
The [macOS, iOS, and Windows clients](https://tailscale.com/download)
|
||||
use the code in this repository but additionally include small GUI
|
||||
wrappers that are not open source.
|
||||
wrappers. The GUI wrappers on non-open source platforms are themselves
|
||||
not open source.
|
||||
|
||||
## Building
|
||||
|
||||
We always require the latest Go release, currently Go 1.19. (While we build
|
||||
releases with our [Go fork](https://github.com/tailscale/go/), its use is not
|
||||
required.)
|
||||
|
||||
```
|
||||
go install tailscale.com/cmd/tailscale{,d}
|
||||
```
|
||||
@@ -43,8 +57,6 @@ If your distro has conventions that preclude the use of
|
||||
`build_dist.sh`, please do the equivalent of what it does in your
|
||||
distro's way, so that bug reports contain useful version information.
|
||||
|
||||
We require the latest Go release, currently Go 1.19.
|
||||
|
||||
## Bugs
|
||||
|
||||
Please file any issues about this code or the hosted service on
|
||||
|
||||
@@ -1 +1 @@
|
||||
1.35.0
|
||||
1.36.2
|
||||
|
||||
@@ -55,14 +55,14 @@ func (c *Client) Keys(ctx context.Context) ([]string, error) {
|
||||
return nil, handleErrorResponse(b, resp)
|
||||
}
|
||||
|
||||
var keys []struct {
|
||||
ID string `json:"id"`
|
||||
var keys struct {
|
||||
Keys []*Key `json:"keys"`
|
||||
}
|
||||
if err := json.Unmarshal(b, &keys); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ret := make([]string, 0, len(keys))
|
||||
for _, k := range keys {
|
||||
ret := make([]string, 0, len(keys.Keys))
|
||||
for _, k := range keys.Keys {
|
||||
ret = append(ret, k.ID)
|
||||
}
|
||||
return ret, nil
|
||||
|
||||
@@ -114,6 +114,7 @@ func (lc *LocalClient) defaultDialer(ctx context.Context, network, addr string)
|
||||
//
|
||||
// DoLocalRequest may mutate the request to add Authorization headers.
|
||||
func (lc *LocalClient) DoLocalRequest(req *http.Request) (*http.Response, error) {
|
||||
req.Header.Set("Tailscale-Cap", strconv.Itoa(int(tailcfg.CurrentCapabilityVersion)))
|
||||
lc.tsClientOnce.Do(func() {
|
||||
lc.tsClient = &http.Client{
|
||||
Transport: &http.Transport{
|
||||
@@ -257,6 +258,23 @@ func (lc *LocalClient) DaemonMetrics(ctx context.Context) ([]byte, error) {
|
||||
return lc.get200(ctx, "/localapi/v0/metrics")
|
||||
}
|
||||
|
||||
// TailDaemonLogs returns a stream the Tailscale daemon's logs as they arrive.
|
||||
// Close the context to stop the stream.
|
||||
func (lc *LocalClient) TailDaemonLogs(ctx context.Context) (io.Reader, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", "http://"+apitype.LocalAPIHost+"/localapi/v0/logtap", nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res, err := lc.doLocalRequestNiceError(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if res.StatusCode != 200 {
|
||||
return nil, errors.New(res.Status)
|
||||
}
|
||||
return res.Body, nil
|
||||
}
|
||||
|
||||
// Pprof returns a pprof profile of the Tailscale daemon.
|
||||
func (lc *LocalClient) Pprof(ctx context.Context, pprofType string, sec int) ([]byte, error) {
|
||||
var secArg string
|
||||
@@ -1002,6 +1020,15 @@ func (lc *LocalClient) DebugDERPRegion(ctx context.Context, regionIDOrCode strin
|
||||
return decodeJSON[*ipnstate.DebugDERPRegionReport](body)
|
||||
}
|
||||
|
||||
// DebugSetExpireIn marks the current node key to expire in d.
|
||||
//
|
||||
// This is meant primarily for debug and testing.
|
||||
func (lc *LocalClient) DebugSetExpireIn(ctx context.Context, d time.Duration) error {
|
||||
v := url.Values{"expiry": {fmt.Sprint(time.Now().Add(d).Unix())}}
|
||||
_, err := lc.send(ctx, "POST", "/localapi/v0/set-expiry-sooner?"+v.Encode(), 200, nil)
|
||||
return err
|
||||
}
|
||||
|
||||
// WatchIPNBus subscribes to the IPN notification bus. It returns a watcher
|
||||
// once the bus is connected successfully.
|
||||
//
|
||||
|
||||
@@ -80,7 +80,7 @@ func main() {
|
||||
w("}")
|
||||
}
|
||||
cloneOutput := pkg.Name + "_clone.go"
|
||||
if err := codegen.WritePackageFile("tailscale.com/cmd/cloner", pkg, cloneOutput, it, buf); err != nil {
|
||||
if err := codegen.WritePackageFile("tailscale.com/cmd/cloner", pkg, cloneOutput, codegen.CopyrightYear("."), it, buf); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
// As with most container things, configuration is passed through environment
|
||||
// variables. All configuration is optional.
|
||||
//
|
||||
// - TS_AUTH_KEY: the authkey to use for login.
|
||||
// - TS_AUTHKEY: the authkey to use for login.
|
||||
// - TS_ROUTES: subnet routes to advertise.
|
||||
// - TS_DEST_IP: proxy all incoming Tailscale traffic to the given
|
||||
// destination.
|
||||
@@ -42,7 +42,7 @@
|
||||
// TS_KUBE_SECRET="" and TS_STATE_DIR=/path/to/storage/dir. The state dir should
|
||||
// be persistent storage.
|
||||
//
|
||||
// Additionally, if TS_AUTH_KEY is not set and the TS_KUBE_SECRET contains an
|
||||
// Additionally, if TS_AUTHKEY is not set and the TS_KUBE_SECRET contains an
|
||||
// "authkey" field, that key is used as the tailscale authkey.
|
||||
package main
|
||||
|
||||
@@ -73,7 +73,7 @@ func main() {
|
||||
tailscale.I_Acknowledge_This_API_Is_Unstable = true
|
||||
|
||||
cfg := &settings{
|
||||
AuthKey: defaultEnv("TS_AUTH_KEY", ""),
|
||||
AuthKey: defaultEnvs([]string{"TS_AUTHKEY", "TS_AUTH_KEY"}, ""),
|
||||
Routes: defaultEnv("TS_ROUTES", ""),
|
||||
ProxyTo: defaultEnv("TS_DEST_IP", ""),
|
||||
DaemonExtraArgs: defaultEnv("TS_TAILSCALED_EXTRA_ARGS", ""),
|
||||
@@ -548,6 +548,15 @@ func defaultEnv(name, defVal string) string {
|
||||
return defVal
|
||||
}
|
||||
|
||||
func defaultEnvs(names []string, defVal string) string {
|
||||
for _, name := range names {
|
||||
if v, ok := os.LookupEnv(name); ok {
|
||||
return v
|
||||
}
|
||||
}
|
||||
return defVal
|
||||
}
|
||||
|
||||
// defaultBool returns the boolean value of the given envvar name, or
|
||||
// defVal if unset or not a bool.
|
||||
func defaultBool(name string, defVal bool) bool {
|
||||
|
||||
@@ -146,6 +146,24 @@ func TestContainerBoot(t *testing.T) {
|
||||
{
|
||||
// Userspace mode, ephemeral storage, authkey provided on every run.
|
||||
Name: "authkey",
|
||||
Env: map[string]string{
|
||||
"TS_AUTHKEY": "tskey-key",
|
||||
},
|
||||
Phases: []phase{
|
||||
{
|
||||
WantCmds: []string{
|
||||
"/usr/bin/tailscaled --socket=/tmp/tailscaled.sock --state=mem: --statedir=/tmp --tun=userspace-networking",
|
||||
"/usr/bin/tailscale --socket=/tmp/tailscaled.sock up --accept-dns=false --authkey=tskey-key",
|
||||
},
|
||||
},
|
||||
{
|
||||
Notify: runningNotify,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
// Userspace mode, ephemeral storage, authkey provided on every run.
|
||||
Name: "authkey-old-flag",
|
||||
Env: map[string]string{
|
||||
"TS_AUTH_KEY": "tskey-key",
|
||||
},
|
||||
@@ -164,7 +182,7 @@ func TestContainerBoot(t *testing.T) {
|
||||
{
|
||||
Name: "authkey_disk_state",
|
||||
Env: map[string]string{
|
||||
"TS_AUTH_KEY": "tskey-key",
|
||||
"TS_AUTHKEY": "tskey-key",
|
||||
"TS_STATE_DIR": filepath.Join(d, "tmp"),
|
||||
},
|
||||
Phases: []phase{
|
||||
@@ -182,8 +200,8 @@ func TestContainerBoot(t *testing.T) {
|
||||
{
|
||||
Name: "routes",
|
||||
Env: map[string]string{
|
||||
"TS_AUTH_KEY": "tskey-key",
|
||||
"TS_ROUTES": "1.2.3.0/24,10.20.30.0/24",
|
||||
"TS_AUTHKEY": "tskey-key",
|
||||
"TS_ROUTES": "1.2.3.0/24,10.20.30.0/24",
|
||||
},
|
||||
Phases: []phase{
|
||||
{
|
||||
@@ -204,7 +222,7 @@ func TestContainerBoot(t *testing.T) {
|
||||
{
|
||||
Name: "routes_kernel_ipv4",
|
||||
Env: map[string]string{
|
||||
"TS_AUTH_KEY": "tskey-key",
|
||||
"TS_AUTHKEY": "tskey-key",
|
||||
"TS_ROUTES": "1.2.3.0/24,10.20.30.0/24",
|
||||
"TS_USERSPACE": "false",
|
||||
},
|
||||
@@ -227,7 +245,7 @@ func TestContainerBoot(t *testing.T) {
|
||||
{
|
||||
Name: "routes_kernel_ipv6",
|
||||
Env: map[string]string{
|
||||
"TS_AUTH_KEY": "tskey-key",
|
||||
"TS_AUTHKEY": "tskey-key",
|
||||
"TS_ROUTES": "::/64,1::/64",
|
||||
"TS_USERSPACE": "false",
|
||||
},
|
||||
@@ -250,7 +268,7 @@ func TestContainerBoot(t *testing.T) {
|
||||
{
|
||||
Name: "routes_kernel_all_families",
|
||||
Env: map[string]string{
|
||||
"TS_AUTH_KEY": "tskey-key",
|
||||
"TS_AUTHKEY": "tskey-key",
|
||||
"TS_ROUTES": "::/64,1.2.3.0/24",
|
||||
"TS_USERSPACE": "false",
|
||||
},
|
||||
@@ -273,7 +291,7 @@ func TestContainerBoot(t *testing.T) {
|
||||
{
|
||||
Name: "proxy",
|
||||
Env: map[string]string{
|
||||
"TS_AUTH_KEY": "tskey-key",
|
||||
"TS_AUTHKEY": "tskey-key",
|
||||
"TS_DEST_IP": "1.2.3.4",
|
||||
"TS_USERSPACE": "false",
|
||||
},
|
||||
@@ -295,7 +313,7 @@ func TestContainerBoot(t *testing.T) {
|
||||
{
|
||||
Name: "authkey_once",
|
||||
Env: map[string]string{
|
||||
"TS_AUTH_KEY": "tskey-key",
|
||||
"TS_AUTHKEY": "tskey-key",
|
||||
"TS_AUTH_ONCE": "true",
|
||||
},
|
||||
Phases: []phase{
|
||||
@@ -354,7 +372,7 @@ func TestContainerBoot(t *testing.T) {
|
||||
// Explicitly set to an empty value, to override the default of "tailscale".
|
||||
"TS_KUBE_SECRET": "",
|
||||
"TS_STATE_DIR": filepath.Join(d, "tmp"),
|
||||
"TS_AUTH_KEY": "tskey-key",
|
||||
"TS_AUTHKEY": "tskey-key",
|
||||
},
|
||||
KubeSecret: map[string]string{},
|
||||
Phases: []phase{
|
||||
@@ -376,7 +394,7 @@ func TestContainerBoot(t *testing.T) {
|
||||
Env: map[string]string{
|
||||
"KUBERNETES_SERVICE_HOST": kube.Host,
|
||||
"KUBERNETES_SERVICE_PORT_HTTPS": kube.Port,
|
||||
"TS_AUTH_KEY": "tskey-key",
|
||||
"TS_AUTHKEY": "tskey-key",
|
||||
},
|
||||
KubeSecret: map[string]string{},
|
||||
KubeDenyPatch: true,
|
||||
|
||||
@@ -69,7 +69,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
|
||||
tailscale.com/types/opt from tailscale.com/client/tailscale+
|
||||
tailscale.com/types/persist from tailscale.com/ipn
|
||||
tailscale.com/types/preftype from tailscale.com/ipn
|
||||
tailscale.com/types/ptr from tailscale.com/hostinfo
|
||||
tailscale.com/types/ptr from tailscale.com/hostinfo+
|
||||
tailscale.com/types/structs from tailscale.com/ipn+
|
||||
tailscale.com/types/tkatype from tailscale.com/types/key+
|
||||
tailscale.com/types/views from tailscale.com/ipn/ipnstate+
|
||||
@@ -79,7 +79,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
|
||||
L 💣 tailscale.com/util/dirwalk from tailscale.com/metrics
|
||||
tailscale.com/util/dnsname from tailscale.com/hostinfo+
|
||||
tailscale.com/util/lineread from tailscale.com/hostinfo+
|
||||
tailscale.com/util/mak from tailscale.com/syncs
|
||||
tailscale.com/util/mak from tailscale.com/syncs+
|
||||
tailscale.com/util/singleflight from tailscale.com/net/dnscache
|
||||
tailscale.com/util/strs from tailscale.com/hostinfo+
|
||||
W 💣 tailscale.com/util/winutil from tailscale.com/hostinfo+
|
||||
|
||||
1
cmd/get-authkey/.gitignore
vendored
Normal file
1
cmd/get-authkey/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
get-authkey
|
||||
72
cmd/get-authkey/main.go
Normal file
72
cmd/get-authkey/main.go
Normal file
@@ -0,0 +1,72 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// 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
|
||||
// to stdout for scripts to capture and use.
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/oauth2/clientcredentials"
|
||||
"tailscale.com/client/tailscale"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Required to use our client API. We're fine with the instability since the
|
||||
// client lives in the same repo as this code.
|
||||
tailscale.I_Acknowledge_This_API_Is_Unstable = true
|
||||
|
||||
reusable := flag.Bool("reusable", false, "allocate a reusable authkey")
|
||||
ephemeral := flag.Bool("ephemeral", false, "allocate an ephemeral authkey")
|
||||
preauth := flag.Bool("preauth", true, "set the authkey as pre-authorized")
|
||||
tags := flag.String("tags", "", "comma-separated list of tags to apply to the authkey")
|
||||
flag.Parse()
|
||||
|
||||
clientId := os.Getenv("TS_API_CLIENT_ID")
|
||||
clientSecret := os.Getenv("TS_API_CLIENT_SECRET")
|
||||
if clientId == "" || clientSecret == "" {
|
||||
log.Fatal("TS_API_CLIENT_ID and TS_API_CLIENT_SECRET must be set")
|
||||
}
|
||||
|
||||
baseUrl := os.Getenv("TS_BASE_URL")
|
||||
if baseUrl == "" {
|
||||
baseUrl = "https://api.tailscale.com"
|
||||
}
|
||||
|
||||
credentials := clientcredentials.Config{
|
||||
ClientID: clientId,
|
||||
ClientSecret: clientSecret,
|
||||
TokenURL: baseUrl + "/api/v2/oauth/token",
|
||||
Scopes: []string{"device"},
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
tsClient := tailscale.NewClient("-", nil)
|
||||
tsClient.HTTPClient = credentials.Client(ctx)
|
||||
tsClient.BaseURL = baseUrl
|
||||
|
||||
caps := tailscale.KeyCapabilities{
|
||||
Devices: tailscale.KeyDeviceCapabilities{
|
||||
Create: tailscale.KeyDeviceCreateCapabilities{
|
||||
Reusable: *reusable,
|
||||
Ephemeral: *ephemeral,
|
||||
Preauthorized: *preauth,
|
||||
Tags: strings.Split(*tags, ","),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
authkey, _, err := tsClient.CreateKey(ctx, caps)
|
||||
if err != nil {
|
||||
log.Fatal(err.Error())
|
||||
}
|
||||
|
||||
fmt.Println(authkey)
|
||||
}
|
||||
@@ -164,14 +164,6 @@ waitOnline:
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
|
||||
sr := &ServiceReconciler{
|
||||
tsClient: tsClient,
|
||||
defaultTags: strings.Split(tags, ","),
|
||||
operatorNamespace: tsNamespace,
|
||||
proxyImage: image,
|
||||
logger: zlog.Named("service-reconciler"),
|
||||
}
|
||||
|
||||
// For secrets and statefulsets, we only get permission to touch the objects
|
||||
// in the controller's own namespace. This cannot be expressed by
|
||||
// .Watches(...) below, instead you have to add a per-type field selector to
|
||||
@@ -193,6 +185,15 @@ waitOnline:
|
||||
startlog.Fatalf("could not create manager: %v", err)
|
||||
}
|
||||
|
||||
sr := &ServiceReconciler{
|
||||
Client: mgr.GetClient(),
|
||||
tsClient: tsClient,
|
||||
defaultTags: strings.Split(tags, ","),
|
||||
operatorNamespace: tsNamespace,
|
||||
proxyImage: image,
|
||||
logger: zlog.Named("service-reconciler"),
|
||||
}
|
||||
|
||||
reconcileFilter := handler.EnqueueRequestsFromMapFunc(func(o client.Object) []reconcile.Request {
|
||||
ls := o.GetLabels()
|
||||
if ls[LabelManaged] != "true" {
|
||||
@@ -591,11 +592,6 @@ func (a *ServiceReconciler) reconcileSTS(ctx context.Context, logger *zap.Sugare
|
||||
return createOrUpdate(ctx, a.Client, a.operatorNamespace, &ss, func(s *appsv1.StatefulSet) { s.Spec = ss.Spec })
|
||||
}
|
||||
|
||||
func (a *ServiceReconciler) InjectClient(c client.Client) error {
|
||||
a.Client = c
|
||||
return nil
|
||||
}
|
||||
|
||||
// ptrObject is a type constraint for pointer types that implement
|
||||
// client.Object.
|
||||
type ptrObject[T any] interface {
|
||||
|
||||
@@ -58,6 +58,7 @@ func main() {
|
||||
postrm := flag.String("postrm", "", "debian postrm script path")
|
||||
replaces := flag.String("replaces", "", "package which this package replaces, if any")
|
||||
depends := flag.String("depends", "", "comma-separated list of packages this package depends on")
|
||||
recommends := flag.String("recommends", "", "comma-separated list of packages this package recommends")
|
||||
flag.Parse()
|
||||
|
||||
filesMap, err := parseFiles(*files)
|
||||
@@ -93,6 +94,9 @@ func main() {
|
||||
if len(*depends) != 0 {
|
||||
info.Overridables.Depends = strings.Split(*depends, ",")
|
||||
}
|
||||
if len(*recommends) != 0 {
|
||||
info.Overridables.Recommends = strings.Split(*recommends, ",")
|
||||
}
|
||||
if *replaces != "" {
|
||||
info.Overridables.Replaces = []string{*replaces}
|
||||
info.Overridables.Conflicts = []string{*replaces}
|
||||
|
||||
38
cmd/tailscale/cli/authenticode_windows.go
Normal file
38
cmd/tailscale/cli/authenticode_windows.go
Normal file
@@ -0,0 +1,38 @@
|
||||
/* SPDX-License-Identifier: MIT
|
||||
*
|
||||
* Copyright (C) 2019-2022 WireGuard LLC. All Rights Reserved.
|
||||
*/
|
||||
|
||||
package cli
|
||||
|
||||
import (
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
func init() {
|
||||
verifyAuthenticode = verifyAuthenticodeWindows
|
||||
}
|
||||
|
||||
func verifyAuthenticodeWindows(path string) error {
|
||||
path16, err := windows.UTF16PtrFromString(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
data := &windows.WinTrustData{
|
||||
Size: uint32(unsafe.Sizeof(windows.WinTrustData{})),
|
||||
UIChoice: windows.WTD_UI_NONE,
|
||||
RevocationChecks: windows.WTD_REVOKE_WHOLECHAIN, // Full revocation checking, as this is called with network connectivity.
|
||||
UnionChoice: windows.WTD_CHOICE_FILE,
|
||||
StateAction: windows.WTD_STATEACTION_VERIFY,
|
||||
FileOrCatalogOrBlobOrSgnrOrCert: unsafe.Pointer(&windows.WinTrustFileInfo{
|
||||
Size: uint32(unsafe.Sizeof(windows.WinTrustFileInfo{})),
|
||||
FilePath: path16,
|
||||
}),
|
||||
}
|
||||
err = windows.WinVerifyTrustEx(windows.InvalidHWND, &windows.WINTRUST_ACTION_GENERIC_VERIFY_V2, data)
|
||||
data.StateAction = windows.WTD_STATEACTION_CLOSE
|
||||
windows.WinVerifyTrustEx(windows.InvalidHWND, &windows.WINTRUST_ACTION_GENERIC_VERIFY_V2, data)
|
||||
return err
|
||||
}
|
||||
@@ -196,6 +196,8 @@ change in the future.
|
||||
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)
|
||||
}
|
||||
if runtime.GOOS == "linux" && distro.Get() == distro.Synology {
|
||||
rootCmd.Subcommands = append(rootCmd.Subcommands, configureHostCmd)
|
||||
|
||||
@@ -76,6 +76,17 @@ var debugCmd = &ffcli.Command{
|
||||
Exec: runDaemonGoroutines,
|
||||
ShortHelp: "print tailscaled's goroutines",
|
||||
},
|
||||
{
|
||||
Name: "daemon-logs",
|
||||
Exec: runDaemonLogs,
|
||||
ShortHelp: "watch tailscaled's server logs",
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
fs := newFlagSet("daemon-logs")
|
||||
fs.IntVar(&daemonLogsArgs.verbose, "verbose", 0, "verbosity level")
|
||||
fs.BoolVar(&daemonLogsArgs.time, "time", false, "include client time")
|
||||
return fs
|
||||
})(),
|
||||
},
|
||||
{
|
||||
Name: "metrics",
|
||||
Exec: runDaemonMetrics,
|
||||
@@ -134,6 +145,7 @@ var debugCmd = &ffcli.Command{
|
||||
fs := newFlagSet("watch-ipn")
|
||||
fs.BoolVar(&watchIPNArgs.netmap, "netmap", true, "include netmap in messages")
|
||||
fs.BoolVar(&watchIPNArgs.initial, "initial", false, "include initial status")
|
||||
fs.BoolVar(&watchIPNArgs.showPrivateKey, "show-private-key", false, "include node private key in printed netmap")
|
||||
return fs
|
||||
})(),
|
||||
},
|
||||
@@ -154,6 +166,16 @@ var debugCmd = &ffcli.Command{
|
||||
return fs
|
||||
})(),
|
||||
},
|
||||
{
|
||||
Name: "set-expire",
|
||||
Exec: runSetExpire,
|
||||
ShortHelp: "manipulate node key expiry for testing",
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
fs := newFlagSet("set-expire")
|
||||
fs.DurationVar(&setExpireArgs.in, "in", 0, "if non-zero, set node key to expire this duration from now")
|
||||
return fs
|
||||
})(),
|
||||
},
|
||||
{
|
||||
Name: "dev-store-set",
|
||||
Exec: runDevStoreSet,
|
||||
@@ -319,8 +341,9 @@ func runPrefs(ctx context.Context, args []string) error {
|
||||
}
|
||||
|
||||
var watchIPNArgs struct {
|
||||
netmap bool
|
||||
initial bool
|
||||
netmap bool
|
||||
initial bool
|
||||
showPrivateKey bool
|
||||
}
|
||||
|
||||
func runWatchIPN(ctx context.Context, args []string) error {
|
||||
@@ -328,6 +351,9 @@ func runWatchIPN(ctx context.Context, args []string) error {
|
||||
if watchIPNArgs.initial {
|
||||
mask = ipn.NotifyInitialState | ipn.NotifyInitialPrefs | ipn.NotifyInitialNetMap
|
||||
}
|
||||
if !watchIPNArgs.showPrivateKey {
|
||||
mask |= ipn.NotifyNoPrivateKeys
|
||||
}
|
||||
watcher, err := localClient.WatchIPNBus(ctx, mask)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -414,6 +440,39 @@ func runDaemonGoroutines(ctx context.Context, args []string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
var daemonLogsArgs struct {
|
||||
verbose int
|
||||
time bool
|
||||
}
|
||||
|
||||
func runDaemonLogs(ctx context.Context, args []string) error {
|
||||
logs, err := localClient.TailDaemonLogs(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d := json.NewDecoder(logs)
|
||||
for {
|
||||
var line struct {
|
||||
Text string `json:"text"`
|
||||
Verbose int `json:"v"`
|
||||
Time string `json:"client_time"`
|
||||
}
|
||||
err := d.Decode(&line)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
line.Text = strings.TrimSpace(line.Text)
|
||||
if line.Text == "" || line.Verbose > daemonLogsArgs.verbose {
|
||||
continue
|
||||
}
|
||||
if daemonLogsArgs.time {
|
||||
fmt.Printf("%s %s\n", line.Time, line.Text)
|
||||
} else {
|
||||
fmt.Println(line.Text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var metricsArgs struct {
|
||||
watch bool
|
||||
}
|
||||
@@ -665,3 +724,14 @@ func runDebugDERP(ctx context.Context, args []string) error {
|
||||
fmt.Printf("%s\n", must.Get(json.MarshalIndent(st, "", " ")))
|
||||
return nil
|
||||
}
|
||||
|
||||
var setExpireArgs struct {
|
||||
in time.Duration
|
||||
}
|
||||
|
||||
func runSetExpire(ctx context.Context, args []string) error {
|
||||
if len(args) != 0 || setExpireArgs.in == 0 {
|
||||
return errors.New("usage --in=<duration>")
|
||||
}
|
||||
return localClient.DebugSetExpireIn(ctx, setExpireArgs.in)
|
||||
}
|
||||
|
||||
@@ -8,11 +8,13 @@ package cli
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
ps "github.com/mitchellh/go-ps"
|
||||
"tailscale.com/version/distro"
|
||||
)
|
||||
|
||||
// fixTailscaledConnectError is called when the local tailscaled has
|
||||
@@ -47,9 +49,27 @@ func fixTailscaledConnectError(origErr error) error {
|
||||
case "darwin":
|
||||
return fmt.Errorf("failed to connect to local Tailscale service; is Tailscale running?")
|
||||
case "linux":
|
||||
return fmt.Errorf("failed to connect to local tailscaled; it doesn't appear to be running (sudo systemctl start tailscaled ?)")
|
||||
var hint string
|
||||
if isSystemdSystem() {
|
||||
hint = " (sudo systemctl start tailscaled ?)"
|
||||
}
|
||||
return fmt.Errorf("failed to connect to local tailscaled; it doesn't appear to be running%s", hint)
|
||||
}
|
||||
return fmt.Errorf("failed to connect to local tailscaled process; it doesn't appear to be running")
|
||||
}
|
||||
return fmt.Errorf("failed to connect to local tailscaled (which appears to be running as %v, pid %v). Got error: %w", foundProc.Executable(), foundProc.Pid(), origErr)
|
||||
}
|
||||
|
||||
// isSystemdSystem reports whether the current machine uses systemd
|
||||
// and in particular whether the systemctl command is available.
|
||||
func isSystemdSystem() bool {
|
||||
if runtime.GOOS != "linux" {
|
||||
return false
|
||||
}
|
||||
switch distro.Get() {
|
||||
case distro.QNAP, distro.Gokrazy, distro.Synology:
|
||||
return false
|
||||
}
|
||||
_, err := exec.LookPath("systemctl")
|
||||
return err == nil
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ var loginArgs upArgsT
|
||||
|
||||
var loginCmd = &ffcli.Command{
|
||||
Name: "login",
|
||||
ShortUsage: "[ALPHA] login [flags]",
|
||||
ShortUsage: "login [flags]",
|
||||
ShortHelp: "Log in to a Tailscale account",
|
||||
LongHelp: `"tailscale login" logs this machine in to your Tailscale network.
|
||||
This command is currently in alpha and may change in the future.`,
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
@@ -99,6 +100,22 @@ func runNetworkLockInit(ctx context.Context, args []string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Common mistake: Not specifying the current node's key as one of the trusted keys.
|
||||
foundSelfKey := false
|
||||
for _, k := range keys {
|
||||
keyID, err := k.ID()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if bytes.Equal(keyID, st.PublicKey.KeyID()) {
|
||||
foundSelfKey = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !foundSelfKey {
|
||||
return errors.New("the tailnet lock key of the current node must be one of the trusted keys during initialization")
|
||||
}
|
||||
|
||||
fmt.Println("You are initializing tailnet lock with the following trusted signing keys:")
|
||||
for _, k := range keys {
|
||||
fmt.Printf(" - tlpub:%x (%s key)\n", k.Public, k.Kind.String())
|
||||
@@ -151,12 +168,21 @@ func runNetworkLockInit(ctx context.Context, args []string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
var nlStatusArgs struct {
|
||||
json bool
|
||||
}
|
||||
|
||||
var nlStatusCmd = &ffcli.Command{
|
||||
Name: "status",
|
||||
ShortUsage: "status",
|
||||
ShortHelp: "Outputs the state of tailnet lock",
|
||||
LongHelp: "Outputs the state of tailnet lock",
|
||||
Exec: runNetworkLockStatus,
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
fs := newFlagSet("lock status")
|
||||
fs.BoolVar(&nlStatusArgs.json, "json", false, "output in JSON format (WARNING: format subject to change)")
|
||||
return fs
|
||||
})(),
|
||||
}
|
||||
|
||||
func runNetworkLockStatus(ctx context.Context, args []string) error {
|
||||
@@ -164,6 +190,13 @@ func runNetworkLockStatus(ctx context.Context, args []string) error {
|
||||
if err != nil {
|
||||
return fixTailscaledConnectError(err)
|
||||
}
|
||||
|
||||
if nlStatusArgs.json {
|
||||
enc := json.NewEncoder(os.Stdout)
|
||||
enc.SetIndent("", " ")
|
||||
return enc.Encode(st)
|
||||
}
|
||||
|
||||
if st.Enabled {
|
||||
fmt.Println("Tailnet lock is ENABLED.")
|
||||
} else {
|
||||
@@ -196,7 +229,7 @@ func runNetworkLockStatus(ctx context.Context, args []string) error {
|
||||
line.WriteString(fmt.Sprint(k.Votes))
|
||||
line.WriteString("\t")
|
||||
if k.Key == st.PublicKey {
|
||||
line.WriteString("(us)")
|
||||
line.WriteString("(self)")
|
||||
}
|
||||
fmt.Println(line.String())
|
||||
}
|
||||
@@ -418,6 +451,7 @@ func runNetworkLockDisablementKDF(ctx context.Context, args []string) error {
|
||||
|
||||
var nlLogArgs struct {
|
||||
limit int
|
||||
json bool
|
||||
}
|
||||
|
||||
var nlLogCmd = &ffcli.Command{
|
||||
@@ -429,6 +463,7 @@ var nlLogCmd = &ffcli.Command{
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
fs := newFlagSet("lock log")
|
||||
fs.IntVar(&nlLogArgs.limit, "limit", 50, "max number of updates to list")
|
||||
fs.BoolVar(&nlLogArgs.json, "json", false, "output in JSON format (WARNING: format subject to change)")
|
||||
return fs
|
||||
})(),
|
||||
}
|
||||
@@ -444,8 +479,13 @@ func nlDescribeUpdate(update ipnstate.NetworkLockUpdate, color bool) (string, er
|
||||
var stanza strings.Builder
|
||||
printKey := func(key *tka.Key, prefix string) {
|
||||
fmt.Fprintf(&stanza, "%sType: %s\n", prefix, key.Kind.String())
|
||||
fmt.Fprintf(&stanza, "%sKeyID: %x\n", prefix, key.ID())
|
||||
fmt.Fprintf(&stanza, "%sVotes: %d\n", prefix, key.Votes)
|
||||
if keyID, err := key.ID(); err == nil {
|
||||
fmt.Fprintf(&stanza, "%sKeyID: %x\n", prefix, keyID)
|
||||
} else {
|
||||
// Older versions of the client shouldn't explode when they encounter an
|
||||
// unknown key type.
|
||||
fmt.Fprintf(&stanza, "%sKeyID: <Error: %v>\n", prefix, err)
|
||||
}
|
||||
if key.Meta != nil {
|
||||
fmt.Fprintf(&stanza, "%sMetadata: %+v\n", prefix, key.Meta)
|
||||
}
|
||||
@@ -501,6 +541,12 @@ func runNetworkLockLog(ctx context.Context, args []string) error {
|
||||
if err != nil {
|
||||
return fixTailscaledConnectError(err)
|
||||
}
|
||||
if nlLogArgs.json {
|
||||
enc := json.NewEncoder(os.Stdout)
|
||||
enc.SetIndent("", " ")
|
||||
return enc.Encode(updates)
|
||||
}
|
||||
|
||||
useColor := isatty.IsTerminal(os.Stdout.Fd())
|
||||
|
||||
stdOut := colorable.NewColorableStdout()
|
||||
|
||||
@@ -30,9 +30,9 @@ import (
|
||||
"tailscale.com/version"
|
||||
)
|
||||
|
||||
var serveCmd = newServeCommand(&serveEnv{})
|
||||
var serveCmd = newServeCommand(&serveEnv{lc: &localClient})
|
||||
|
||||
// newServeCommand returns a new "serve" subcommand using e as its environmment.
|
||||
// newServeCommand returns a new "serve" subcommand using e as its environment.
|
||||
func newServeCommand(e *serveEnv) *ffcli.Command {
|
||||
return &ffcli.Command{
|
||||
Name: "serve",
|
||||
@@ -89,7 +89,7 @@ EXAMPLES
|
||||
" $ tailscale serve tcp 5432",
|
||||
"",
|
||||
" - Forward raw, TLS-terminated TCP packets to a local TCP server on port 5432:",
|
||||
" $ tailscale serve --terminate-tls tcp 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")
|
||||
@@ -127,8 +127,21 @@ func (e *serveEnv) newFlags(name string, setup func(fs *flag.FlagSet)) *flag.Fla
|
||||
return fs
|
||||
}
|
||||
|
||||
// localServeClient is an interface conforming to the subset of
|
||||
// tailscale.LocalClient. It includes only the methods used by the
|
||||
// serve command.
|
||||
//
|
||||
// The purpose of this interface is to allow tests to provide a mock.
|
||||
type localServeClient interface {
|
||||
Status(context.Context) (*ipnstate.Status, error)
|
||||
GetServeConfig(context.Context) (*ipn.ServeConfig, error)
|
||||
SetServeConfig(context.Context, *ipn.ServeConfig) error
|
||||
}
|
||||
|
||||
// serveEnv is the environment the serve command runs within. All I/O should be
|
||||
// done via serveEnv methods so that it can be faked out for tests.
|
||||
// Calls to localClient should be done via the lc field, which is an interface
|
||||
// that can be faked out for tests.
|
||||
//
|
||||
// It also contains the flags, as registered with newServeCommand.
|
||||
type serveEnv struct {
|
||||
@@ -138,12 +151,11 @@ type serveEnv struct {
|
||||
remove bool // remove a serve config
|
||||
json bool // output JSON (status only for now)
|
||||
|
||||
lc localServeClient // localClient interface, specific to serve
|
||||
|
||||
// optional stuff for tests:
|
||||
testFlagOut io.Writer
|
||||
testGetServeConfig func(context.Context) (*ipn.ServeConfig, error)
|
||||
testSetServeConfig func(context.Context, *ipn.ServeConfig) error
|
||||
testGetLocalClientStatus func(context.Context) (*ipnstate.Status, error)
|
||||
testStdout io.Writer
|
||||
testFlagOut io.Writer
|
||||
testStdout io.Writer
|
||||
}
|
||||
|
||||
// getSelfDNSName returns the DNS name of the current node.
|
||||
@@ -157,15 +169,12 @@ func (e *serveEnv) getSelfDNSName(ctx context.Context) (string, error) {
|
||||
return strings.TrimSuffix(st.Self.DNSName, "."), nil
|
||||
}
|
||||
|
||||
// getLocalClientStatus calls LocalClient.Status, checks if
|
||||
// Status is ready.
|
||||
// getLocalClientStatus returns the Status of the local client.
|
||||
// Returns error if unable to reach tailscaled or if self node is nil.
|
||||
//
|
||||
// Exits if status is not running or starting.
|
||||
func (e *serveEnv) getLocalClientStatus(ctx context.Context) (*ipnstate.Status, error) {
|
||||
if e.testGetLocalClientStatus != nil {
|
||||
return e.testGetLocalClientStatus(ctx)
|
||||
}
|
||||
st, err := localClient.Status(ctx)
|
||||
st, err := e.lc.Status(ctx)
|
||||
if err != nil {
|
||||
return nil, fixTailscaledConnectError(err)
|
||||
}
|
||||
@@ -180,20 +189,6 @@ func (e *serveEnv) getLocalClientStatus(ctx context.Context) (*ipnstate.Status,
|
||||
return st, nil
|
||||
}
|
||||
|
||||
func (e *serveEnv) getServeConfig(ctx context.Context) (*ipn.ServeConfig, error) {
|
||||
if e.testGetServeConfig != nil {
|
||||
return e.testGetServeConfig(ctx)
|
||||
}
|
||||
return localClient.GetServeConfig(ctx)
|
||||
}
|
||||
|
||||
func (e *serveEnv) setServeConfig(ctx context.Context, c *ipn.ServeConfig) error {
|
||||
if e.testSetServeConfig != nil {
|
||||
return e.testSetServeConfig(ctx, c)
|
||||
}
|
||||
return localClient.SetServeConfig(ctx, c)
|
||||
}
|
||||
|
||||
// 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) {
|
||||
@@ -232,7 +227,7 @@ func (e *serveEnv) runServe(ctx context.Context, args []string) error {
|
||||
if err := json.Unmarshal(valb, sc); err != nil {
|
||||
return fmt.Errorf("invalid JSON: %w", err)
|
||||
}
|
||||
return localClient.SetServeConfig(ctx, sc)
|
||||
return e.lc.SetServeConfig(ctx, sc)
|
||||
}
|
||||
|
||||
if !(len(args) == 3 || (e.remove && len(args) >= 1)) {
|
||||
@@ -294,7 +289,7 @@ func (e *serveEnv) runServe(ctx context.Context, args []string) error {
|
||||
return flag.ErrHelp
|
||||
}
|
||||
|
||||
cursc, err := e.getServeConfig(ctx)
|
||||
cursc, err := e.lc.GetServeConfig(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -337,7 +332,7 @@ func (e *serveEnv) runServe(ctx context.Context, args []string) error {
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(cursc, sc) {
|
||||
if err := e.setServeConfig(ctx, sc); err != nil {
|
||||
if err := e.lc.SetServeConfig(ctx, sc); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -351,7 +346,7 @@ func (e *serveEnv) handleWebServeRemove(ctx context.Context, mount string) error
|
||||
return err
|
||||
}
|
||||
srvPortStr := strconv.Itoa(int(srvPort))
|
||||
sc, err := e.getServeConfig(ctx)
|
||||
sc, err := e.lc.GetServeConfig(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -382,7 +377,7 @@ func (e *serveEnv) handleWebServeRemove(ctx context.Context, mount string) error
|
||||
if len(sc.TCP) == 0 {
|
||||
sc.TCP = nil
|
||||
}
|
||||
if err := e.setServeConfig(ctx, sc); err != nil {
|
||||
if err := e.lc.SetServeConfig(ctx, sc); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
@@ -453,7 +448,7 @@ func allNumeric(s string) bool {
|
||||
// - tailscale status
|
||||
// - tailscale status --json
|
||||
func (e *serveEnv) runServeStatus(ctx context.Context, args []string) error {
|
||||
sc, err := e.getServeConfig(ctx)
|
||||
sc, err := e.lc.GetServeConfig(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -584,8 +579,8 @@ func elipticallyTruncate(s string, max int) string {
|
||||
//
|
||||
// Examples:
|
||||
// - tailscale serve tcp 5432
|
||||
// - tailscale --serve-port=8443 tcp 4430
|
||||
// - tailscale --serve-port=10000 --terminate-tls tcp 8080
|
||||
// - 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")
|
||||
@@ -603,7 +598,7 @@ func (e *serveEnv) runServeTCP(ctx context.Context, args []string) error {
|
||||
fmt.Fprintf(os.Stderr, "error: invalid port %q\n\n", portStr)
|
||||
}
|
||||
|
||||
cursc, err := e.getServeConfig(ctx)
|
||||
cursc, err := e.lc.GetServeConfig(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -628,7 +623,7 @@ func (e *serveEnv) runServeTCP(ctx context.Context, args []string) error {
|
||||
if len(sc.TCP) == 0 {
|
||||
sc.TCP = nil
|
||||
}
|
||||
return e.setServeConfig(ctx, sc)
|
||||
return e.lc.SetServeConfig(ctx, sc)
|
||||
}
|
||||
return errors.New("error: serve config does not exist")
|
||||
}
|
||||
@@ -644,7 +639,7 @@ func (e *serveEnv) runServeTCP(ctx context.Context, args []string) error {
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(cursc, sc) {
|
||||
if err := e.setServeConfig(ctx, sc); err != nil {
|
||||
if err := e.lc.SetServeConfig(ctx, sc); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -674,7 +669,7 @@ func (e *serveEnv) runServeFunnel(ctx context.Context, args []string) error {
|
||||
default:
|
||||
return flag.ErrHelp
|
||||
}
|
||||
sc, err := e.getServeConfig(ctx)
|
||||
sc, err := e.lc.GetServeConfig(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -703,7 +698,7 @@ func (e *serveEnv) runServeFunnel(ctx context.Context, args []string) error {
|
||||
sc.AllowFunnel = nil
|
||||
}
|
||||
}
|
||||
if err := e.setServeConfig(ctx, sc); err != nil {
|
||||
if err := e.lc.SetServeConfig(ctx, sc); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -144,10 +144,10 @@ func TestServeConfigMutations(t *testing.T) {
|
||||
},
|
||||
},
|
||||
})
|
||||
add(step{
|
||||
add(step{ // invalid port
|
||||
command: cmd("--serve-port=9999 /abc proxy 3001"),
|
||||
wantErr: anyErr(),
|
||||
}) // invalid port
|
||||
})
|
||||
add(step{
|
||||
command: cmd("--serve-port=8443 /abc proxy 3001"),
|
||||
want: &ipn.ServeConfig{
|
||||
@@ -606,12 +606,12 @@ func TestServeConfigMutations(t *testing.T) {
|
||||
wantErr: anyErr(),
|
||||
})
|
||||
|
||||
lc := &fakeLocalServeClient{}
|
||||
// And now run the steps above.
|
||||
var current *ipn.ServeConfig
|
||||
for i, st := range steps {
|
||||
if st.reset {
|
||||
t.Logf("Executing step #%d, line %v: [reset]", i, st.line)
|
||||
current = nil
|
||||
lc.config = nil
|
||||
}
|
||||
if st.command == nil {
|
||||
continue
|
||||
@@ -620,26 +620,12 @@ func TestServeConfigMutations(t *testing.T) {
|
||||
|
||||
var stdout bytes.Buffer
|
||||
var flagOut bytes.Buffer
|
||||
var newState *ipn.ServeConfig
|
||||
e := &serveEnv{
|
||||
lc: lc,
|
||||
testFlagOut: &flagOut,
|
||||
testStdout: &stdout,
|
||||
testGetLocalClientStatus: func(context.Context) (*ipnstate.Status, error) {
|
||||
return &ipnstate.Status{
|
||||
Self: &ipnstate.PeerStatus{
|
||||
DNSName: "foo.test.ts.net",
|
||||
Capabilities: []string{tailcfg.NodeAttrFunnel},
|
||||
},
|
||||
}, nil
|
||||
},
|
||||
testGetServeConfig: func(context.Context) (*ipn.ServeConfig, error) {
|
||||
return current, nil
|
||||
},
|
||||
testSetServeConfig: func(_ context.Context, c *ipn.ServeConfig) error {
|
||||
newState = c
|
||||
return nil
|
||||
},
|
||||
}
|
||||
lastCount := lc.setCount
|
||||
cmd := newServeCommand(e)
|
||||
err := cmd.ParseAndRun(context.Background(), st.command)
|
||||
if flagOut.Len() > 0 {
|
||||
@@ -655,23 +641,61 @@ func TestServeConfigMutations(t *testing.T) {
|
||||
continue
|
||||
}
|
||||
if st.wantErr != nil {
|
||||
t.Fatalf("step #%d, line %v: got success (saved=%v), but wanted an error", i, st.line, newState != nil)
|
||||
t.Fatalf("step #%d, line %v: got success (saved=%v), but wanted an error", i, st.line, lc.config != nil)
|
||||
}
|
||||
if !reflect.DeepEqual(newState, st.want) {
|
||||
var got *ipn.ServeConfig = nil
|
||||
if lc.setCount > lastCount {
|
||||
got = lc.config
|
||||
}
|
||||
if !reflect.DeepEqual(got, st.want) {
|
||||
t.Fatalf("[%d] %v: bad state. got:\n%s\n\nwant:\n%s\n",
|
||||
i, st.command, asJSON(newState), asJSON(st.want))
|
||||
i, st.command, asJSON(got), asJSON(st.want))
|
||||
// NOTE: asJSON will omit empty fields, which might make
|
||||
// result in bad state got/want diffs being the same, even
|
||||
// though the actual state is different. Use below to debug:
|
||||
// t.Fatalf("[%d] %v: bad state. got:\n%+v\n\nwant:\n%+v\n",
|
||||
// i, st.command, newState, st.want)
|
||||
}
|
||||
if newState != nil {
|
||||
current = newState
|
||||
// i, st.command, got, st.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// fakeLocalServeClient is a fake tailscale.LocalClient for tests.
|
||||
// It's not a full implementation, just enough to test the serve command.
|
||||
//
|
||||
// The fake client is stateful, and is used to test manipulating
|
||||
// ServeConfig state. This implementation cannot be used concurrently.
|
||||
type fakeLocalServeClient struct {
|
||||
config *ipn.ServeConfig
|
||||
setCount int // counts calls to SetServeConfig
|
||||
}
|
||||
|
||||
// fakeStatus is a fake ipnstate.Status value for tests.
|
||||
// It's not a full implementation, just enough to test the serve command.
|
||||
//
|
||||
// It returns a state that's running, with a fake DNSName and the Funnel
|
||||
// node attribute capability.
|
||||
var fakeStatus = &ipnstate.Status{
|
||||
BackendState: ipn.Running.String(),
|
||||
Self: &ipnstate.PeerStatus{
|
||||
DNSName: "foo.test.ts.net",
|
||||
Capabilities: []string{tailcfg.NodeAttrFunnel},
|
||||
},
|
||||
}
|
||||
|
||||
func (lc *fakeLocalServeClient) Status(ctx context.Context) (*ipnstate.Status, error) {
|
||||
return fakeStatus, nil
|
||||
}
|
||||
|
||||
func (lc *fakeLocalServeClient) GetServeConfig(ctx context.Context) (*ipn.ServeConfig, error) {
|
||||
return lc.config.Clone(), nil
|
||||
}
|
||||
|
||||
func (lc *fakeLocalServeClient) SetServeConfig(ctx context.Context, config *ipn.ServeConfig) error {
|
||||
lc.setCount += 1
|
||||
lc.config = config.Clone()
|
||||
return nil
|
||||
}
|
||||
|
||||
// exactError returns an error checker that wants exactly the provided want error.
|
||||
// If optName is non-empty, it's used in the error message.
|
||||
func exactErr(want error, optName ...string) func(error) string {
|
||||
|
||||
@@ -26,8 +26,8 @@ var switchCmd = &ffcli.Command{
|
||||
Exec: switchProfile,
|
||||
UsageFunc: func(*ffcli.Command) string {
|
||||
return `USAGE
|
||||
[ALPHA] switch <name>
|
||||
[ALPHA] switch --list
|
||||
switch <name>
|
||||
switch --list
|
||||
|
||||
"tailscale switch" switches between logged in accounts.
|
||||
This command is currently in alpha and may change in the future.`
|
||||
|
||||
588
cmd/tailscale/cli/update.go
Normal file
588
cmd/tailscale/cli/update.go
Normal file
@@ -0,0 +1,588 @@
|
||||
// Copyright (c) 2023 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package cli
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
"tailscale.com/net/tshttpproxy"
|
||||
"tailscale.com/util/must"
|
||||
"tailscale.com/util/winutil"
|
||||
"tailscale.com/version"
|
||||
"tailscale.com/version/distro"
|
||||
)
|
||||
|
||||
var updateCmd = &ffcli.Command{
|
||||
Name: "update",
|
||||
ShortUsage: "update",
|
||||
ShortHelp: "[ALPHA] Update Tailscale to the latest/different version",
|
||||
Exec: runUpdate,
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
fs := newFlagSet("update")
|
||||
fs.BoolVar(&updateArgs.yes, "yes", false, "update without interactive prompts")
|
||||
fs.BoolVar(&updateArgs.dryRun, "dry-run", false, "print what update would do without doing it, or prompts")
|
||||
fs.StringVar(&updateArgs.track, "track", "", `which track to check for updates: "stable" or "unstable" (dev); empty means same as current`)
|
||||
fs.StringVar(&updateArgs.version, "version", "", `explicit version to update/downgrade to`)
|
||||
return fs
|
||||
})(),
|
||||
}
|
||||
|
||||
var updateArgs struct {
|
||||
yes bool
|
||||
dryRun bool
|
||||
track string // explicit track; empty means same as current
|
||||
version string // explicit version; empty means auto
|
||||
}
|
||||
|
||||
// winMSIEnv is the environment variable that, if set, is the MSI file for the
|
||||
// update command to install. It's passed like this so we can stop the
|
||||
// tailscale.exe process from running before the msiexec process runs and tries
|
||||
// to overwrite ourselves.
|
||||
const winMSIEnv = "TS_UPDATE_WIN_MSI"
|
||||
|
||||
func runUpdate(ctx context.Context, args []string) error {
|
||||
if msi := os.Getenv(winMSIEnv); msi != "" {
|
||||
log.Printf("installing %v ...", msi)
|
||||
if err := installMSI(msi); err != nil {
|
||||
log.Printf("MSI install failed: %v", err)
|
||||
return err
|
||||
}
|
||||
log.Printf("success.")
|
||||
return nil
|
||||
}
|
||||
if len(args) > 0 {
|
||||
return flag.ErrHelp
|
||||
}
|
||||
if updateArgs.version != "" && updateArgs.track != "" {
|
||||
return errors.New("cannot specify both --version and --track")
|
||||
}
|
||||
up, err := newUpdater()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return up.update()
|
||||
}
|
||||
|
||||
func versionIsStable(v string) (stable, wellFormed bool) {
|
||||
_, rest, ok := strings.Cut(v, ".")
|
||||
if !ok {
|
||||
return false, false
|
||||
}
|
||||
minorStr, _, ok := strings.Cut(rest, ".")
|
||||
if !ok {
|
||||
return false, false
|
||||
}
|
||||
minor, err := strconv.Atoi(minorStr)
|
||||
if err != nil {
|
||||
return false, false
|
||||
}
|
||||
return minor%2 == 0, true
|
||||
}
|
||||
|
||||
func newUpdater() (*updater, error) {
|
||||
up := &updater{
|
||||
track: updateArgs.track,
|
||||
}
|
||||
switch up.track {
|
||||
case "stable", "unstable":
|
||||
case "":
|
||||
if version.IsUnstableBuild() {
|
||||
up.track = "unstable"
|
||||
} else {
|
||||
up.track = "stable"
|
||||
}
|
||||
if updateArgs.version != "" {
|
||||
stable, ok := versionIsStable(updateArgs.version)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("malformed version %q", updateArgs.version)
|
||||
}
|
||||
if stable {
|
||||
up.track = "stable"
|
||||
} else {
|
||||
up.track = "unstable"
|
||||
}
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown track %q; must be 'stable' or 'unstable'", up.track)
|
||||
}
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
up.update = up.updateWindows
|
||||
case "linux":
|
||||
switch distro.Get() {
|
||||
case distro.Synology:
|
||||
up.update = up.updateSynology
|
||||
case distro.Debian: // includes Ubuntu
|
||||
up.update = up.updateDebLike
|
||||
}
|
||||
case "darwin":
|
||||
switch {
|
||||
case !version.IsSandboxedMacOS():
|
||||
return nil, errors.New("The 'update' command is not yet supported on this platform; see https://github.com/tailscale/tailscale/wiki/Tailscaled-on-macOS/ for now")
|
||||
case 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")
|
||||
}
|
||||
}
|
||||
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 up, nil
|
||||
}
|
||||
|
||||
type updater struct {
|
||||
track string
|
||||
update func() error
|
||||
}
|
||||
|
||||
func (up *updater) currentOrDryRun(ver string) bool {
|
||||
if version.Short == ver {
|
||||
fmt.Printf("already running %v; no update needed\n", ver)
|
||||
return true
|
||||
}
|
||||
if updateArgs.dryRun {
|
||||
fmt.Printf("Current: %v, Latest: %v\n", version.Short, ver)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (up *updater) confirm(ver string) error {
|
||||
if updateArgs.yes {
|
||||
log.Printf("Updating Tailscale from %v to %v; --yes given, continuing without prompts.\n", version.Short, ver)
|
||||
return nil
|
||||
}
|
||||
|
||||
fmt.Printf("This will update Tailscale from %v to %v. Continue? [y/n] ", version.Short, ver)
|
||||
var resp string
|
||||
fmt.Scanln(&resp)
|
||||
resp = strings.ToLower(resp)
|
||||
switch resp {
|
||||
case "y", "yes", "sure":
|
||||
return nil
|
||||
}
|
||||
return errors.New("aborting update")
|
||||
}
|
||||
|
||||
func (up *updater) updateSynology() error {
|
||||
// TODO(bradfitz): detect, map GOARCH+CPU to the right Synology arch.
|
||||
// TODO(bradfitz): add pkgs.tailscale.com endpoint to get release info
|
||||
// TODO(bradfitz): require root/sudo
|
||||
// TODO(bradfitz): run /usr/syno/bin/synopkg install tailscale.spk
|
||||
return errors.New("The 'update' command is not yet implemented on Synology.")
|
||||
}
|
||||
|
||||
func (up *updater) updateDebLike() error {
|
||||
ver := updateArgs.version
|
||||
if ver == "" {
|
||||
res, err := http.Get("https://pkgs.tailscale.com/" + up.track + "/?mode=json")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var latest struct {
|
||||
Tarballs map[string]string // ~goarch (ignoring "geode") => "tailscale_1.34.2_mips.tgz"
|
||||
}
|
||||
err = json.NewDecoder(res.Body).Decode(&latest)
|
||||
res.Body.Close()
|
||||
if err != nil {
|
||||
return fmt.Errorf("decoding JSON: %v: %w", res.Status, err)
|
||||
}
|
||||
f, ok := latest.Tarballs[runtime.GOARCH]
|
||||
if !ok {
|
||||
return fmt.Errorf("can't update architecture %q", runtime.GOARCH)
|
||||
}
|
||||
ver, _, ok = strings.Cut(strings.TrimPrefix(f, "tailscale_"), "_")
|
||||
if !ok {
|
||||
return fmt.Errorf("can't parse version from %q", f)
|
||||
}
|
||||
}
|
||||
if up.currentOrDryRun(ver) {
|
||||
return nil
|
||||
}
|
||||
|
||||
track := "unstable"
|
||||
if stable, ok := versionIsStable(ver); !ok {
|
||||
return fmt.Errorf("malformed version %q", ver)
|
||||
} else if stable {
|
||||
track = "stable"
|
||||
}
|
||||
|
||||
if os.Geteuid() != 0 {
|
||||
return errors.New("must be root; use sudo")
|
||||
}
|
||||
|
||||
if updated, err := updateDebianAptSourcesList(track); err != nil {
|
||||
return err
|
||||
} else if updated {
|
||||
fmt.Printf("Updated %s to use the %s track\n", aptSourcesFile, track)
|
||||
}
|
||||
|
||||
cmd := exec.Command("apt-get", "update",
|
||||
// Only update the tailscale repo, not the other ones, treating
|
||||
// the tailscale.list file as the main "sources.list" file.
|
||||
"-o", "Dir::Etc::SourceList=sources.list.d/tailscale.list",
|
||||
// Disable the "sources.list.d" directory:
|
||||
"-o", "Dir::Etc::SourceParts=-",
|
||||
// Don't forget about packages in the other repos just because
|
||||
// we're not updating them:
|
||||
"-o", "APT::Get::List-Cleanup=0",
|
||||
)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd = exec.Command("apt-get", "install", "--yes", "--allow-downgrades", "tailscale="+ver)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
const aptSourcesFile = "/etc/apt/sources.list.d/tailscale.list"
|
||||
|
||||
// updateDebianAptSourcesList updates the /etc/apt/sources.list.d/tailscale.list
|
||||
// file to make sure it has the provided track (stable or unstable) in it.
|
||||
//
|
||||
// If it already has the right track (including containing both stable and
|
||||
// unstable), it does nothing.
|
||||
func updateDebianAptSourcesList(dstTrack string) (rewrote bool, err error) {
|
||||
was, err := os.ReadFile(aptSourcesFile)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
newContent, err := updateDebianAptSourcesListBytes(was, dstTrack)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if bytes.Equal(was, newContent) {
|
||||
return false, nil
|
||||
}
|
||||
return true, os.WriteFile(aptSourcesFile, newContent, 0644)
|
||||
}
|
||||
|
||||
func updateDebianAptSourcesListBytes(was []byte, dstTrack string) (newContent []byte, err error) {
|
||||
trackURLPrefix := []byte("https://pkgs.tailscale.com/" + dstTrack + "/")
|
||||
var buf bytes.Buffer
|
||||
var changes int
|
||||
bs := bufio.NewScanner(bytes.NewReader(was))
|
||||
hadCorrect := false
|
||||
commentLine := regexp.MustCompile(`^\s*\#`)
|
||||
pkgsURL := regexp.MustCompile(`\bhttps://pkgs\.tailscale\.com/((un)?stable)/`)
|
||||
for bs.Scan() {
|
||||
line := bs.Bytes()
|
||||
if !commentLine.Match(line) {
|
||||
line = pkgsURL.ReplaceAllFunc(line, func(m []byte) []byte {
|
||||
if bytes.Equal(m, trackURLPrefix) {
|
||||
hadCorrect = true
|
||||
} else {
|
||||
changes++
|
||||
}
|
||||
return trackURLPrefix
|
||||
})
|
||||
}
|
||||
buf.Write(line)
|
||||
buf.WriteByte('\n')
|
||||
}
|
||||
if hadCorrect || (changes == 1 && bytes.Equal(bytes.TrimSpace(was), bytes.TrimSpace(buf.Bytes()))) {
|
||||
// Unchanged or close enough.
|
||||
return was, nil
|
||||
}
|
||||
if changes != 1 {
|
||||
// No changes, or an unexpected number of changes (what?). Bail.
|
||||
// They probably editted it by hand and we don't know what to do.
|
||||
return nil, fmt.Errorf("unexpected/unsupported %s contents", aptSourcesFile)
|
||||
}
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
func (up *updater) updateMacSys() error {
|
||||
// use sparkle? do we have permissions from this context? does sudo help?
|
||||
// We can at least fail with a command they can run to update from the shell.
|
||||
// Like "tailscale update --macsys | sudo sh" or something.
|
||||
//
|
||||
// TODO(bradfitz,mihai): implement. But for now:
|
||||
return errors.New("The 'update' command is not yet implemented on macOS.")
|
||||
}
|
||||
|
||||
var (
|
||||
verifyAuthenticode func(string) error // or nil on non-Windows
|
||||
markTempFileFunc func(string) error // or nil on non-Windows
|
||||
)
|
||||
|
||||
func (up *updater) updateWindows() error {
|
||||
ver := updateArgs.version
|
||||
if ver == "" {
|
||||
res, err := http.Get("https://pkgs.tailscale.com/" + up.track + "/?mode=json&os=windows")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var latest struct {
|
||||
Version string
|
||||
}
|
||||
err = json.NewDecoder(res.Body).Decode(&latest)
|
||||
res.Body.Close()
|
||||
if err != nil {
|
||||
return fmt.Errorf("decoding JSON: %v: %w", res.Status, err)
|
||||
}
|
||||
ver = latest.Version
|
||||
if ver == "" {
|
||||
return errors.New("no version found")
|
||||
}
|
||||
}
|
||||
arch := runtime.GOARCH
|
||||
if arch == "386" {
|
||||
arch = "x86"
|
||||
}
|
||||
url := fmt.Sprintf("https://pkgs.tailscale.com/%s/tailscale-setup-%s-%s.msi", up.track, ver, arch)
|
||||
|
||||
if up.currentOrDryRun(ver) {
|
||||
return nil
|
||||
}
|
||||
if !winutil.IsCurrentProcessElevated() {
|
||||
return errors.New("must be run as Administrator")
|
||||
}
|
||||
|
||||
tsDir := filepath.Join(os.Getenv("ProgramData"), "Tailscale")
|
||||
msiDir := filepath.Join(tsDir, "MSICache")
|
||||
if fi, err := os.Stat(tsDir); err != nil {
|
||||
return fmt.Errorf("expected %s to exist, got stat error: %w", tsDir, err)
|
||||
} else if !fi.IsDir() {
|
||||
return fmt.Errorf("expected %s to be a directory; got %v", tsDir, fi.Mode())
|
||||
}
|
||||
if err := os.MkdirAll(msiDir, 0700); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := up.confirm(ver); err != nil {
|
||||
return err
|
||||
}
|
||||
msiTarget := filepath.Join(msiDir, path.Base(url))
|
||||
if err := downloadURLToFile(url, msiTarget); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Printf("verifying MSI authenticode...")
|
||||
if err := verifyAuthenticode(msiTarget); err != nil {
|
||||
return fmt.Errorf("authenticode verification of %s failed: %w", msiTarget, err)
|
||||
}
|
||||
log.Printf("authenticode verification succeeded")
|
||||
|
||||
log.Printf("making tailscale.exe copy to switch to...")
|
||||
selfCopy, err := makeSelfCopy()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.Remove(selfCopy)
|
||||
log.Printf("running tailscale.exe copy for final install...")
|
||||
|
||||
cmd := exec.Command(selfCopy, "update")
|
||||
cmd.Env = append(os.Environ(), winMSIEnv+"="+msiTarget)
|
||||
cmd.Stdout = os.Stderr
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Stdin = os.Stdin
|
||||
if err := cmd.Start(); err != nil {
|
||||
return err
|
||||
}
|
||||
// Once it's started, exit ourselves, so the binary is free
|
||||
// to be replaced.
|
||||
os.Exit(0)
|
||||
panic("unreachable")
|
||||
}
|
||||
|
||||
func installMSI(msi string) error {
|
||||
var err error
|
||||
for tries := 0; tries < 2; tries++ {
|
||||
cmd := exec.Command("msiexec.exe", "/i", filepath.Base(msi), "/quiet", "/promptrestart", "/qn")
|
||||
cmd.Dir = filepath.Dir(msi)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Stdin = os.Stdin
|
||||
err = cmd.Run()
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
uninstallVersion := version.Short
|
||||
if v := os.Getenv("TS_DEBUG_UNINSTALL_VERSION"); v != "" {
|
||||
uninstallVersion = v
|
||||
}
|
||||
// Assume it's a downgrade, which msiexec won't permit. Uninstall our current version first.
|
||||
log.Printf("Uninstalling current version %q for downgrade...", uninstallVersion)
|
||||
cmd = exec.Command("msiexec.exe", "/x", msiUUIDForVersion(uninstallVersion), "/norestart", "/qn")
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Stdin = os.Stdin
|
||||
err = cmd.Run()
|
||||
log.Printf("msiexec uninstall: %v", err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func msiUUIDForVersion(ver string) string {
|
||||
arch := runtime.GOARCH
|
||||
if arch == "386" {
|
||||
arch = "x86"
|
||||
}
|
||||
track := "unstable"
|
||||
if stable, ok := versionIsStable(ver); ok && stable {
|
||||
track = "stable"
|
||||
}
|
||||
msiURL := fmt.Sprintf("https://pkgs.tailscale.com/%s/tailscale-setup-%s-%s.msi", track, ver, arch)
|
||||
return "{" + strings.ToUpper(uuid.NewSHA1(uuid.NameSpaceURL, []byte(msiURL)).String()) + "}"
|
||||
}
|
||||
|
||||
func makeSelfCopy() (tmpPathExe string, err error) {
|
||||
selfExe, err := os.Executable()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
f, err := os.Open(selfExe)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer f.Close()
|
||||
f2, err := os.CreateTemp("", "tailscale-updater-*.exe")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if f := markTempFileFunc; f != nil {
|
||||
if err := f(f2.Name()); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
if _, err := io.Copy(f2, f); err != nil {
|
||||
f2.Close()
|
||||
return "", err
|
||||
}
|
||||
return f2.Name(), f2.Close()
|
||||
}
|
||||
|
||||
func downloadURLToFile(urlSrc, fileDst string) (ret error) {
|
||||
tr := http.DefaultTransport.(*http.Transport).Clone()
|
||||
tr.Proxy = tshttpproxy.ProxyFromEnvironment
|
||||
defer tr.CloseIdleConnections()
|
||||
c := &http.Client{Transport: tr}
|
||||
|
||||
quickCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
headReq := must.Get(http.NewRequestWithContext(quickCtx, "HEAD", urlSrc, nil))
|
||||
|
||||
res, err := c.Do(headReq)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("HEAD %s: %v", urlSrc, res.Status)
|
||||
}
|
||||
if res.ContentLength <= 0 {
|
||||
return fmt.Errorf("HEAD %s: unexpected Content-Length %v", urlSrc, res.ContentLength)
|
||||
}
|
||||
log.Printf("Download size: %v", res.ContentLength)
|
||||
|
||||
hashReq := must.Get(http.NewRequestWithContext(quickCtx, "GET", urlSrc+".sha256", nil))
|
||||
hashRes, err := c.Do(hashReq)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
hashHex, err := io.ReadAll(io.LimitReader(hashRes.Body, 100))
|
||||
hashRes.Body.Close()
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("GET %s.sha256: %v", urlSrc, res.Status)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
wantHash, err := hex.DecodeString(string(strings.TrimSpace(string(hashHex))))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
hash := sha256.New()
|
||||
|
||||
dlReq := must.Get(http.NewRequestWithContext(context.Background(), "GET", urlSrc, nil))
|
||||
dlRes, err := c.Do(dlReq)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// TODO(bradfitz): resume from existing partial file on disk
|
||||
if dlRes.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("GET %s: %v", urlSrc, dlRes.Status)
|
||||
}
|
||||
|
||||
of, err := os.Create(fileDst)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
if ret != nil {
|
||||
of.Close()
|
||||
// TODO(bradfitz): os.Remove(fileDst) too? or keep it to resume from/debug later.
|
||||
}
|
||||
}()
|
||||
pw := &progressWriter{total: res.ContentLength}
|
||||
n, err := io.Copy(io.MultiWriter(hash, of, pw), io.LimitReader(dlRes.Body, res.ContentLength))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if n != res.ContentLength {
|
||||
return fmt.Errorf("downloaded %v; want %v", n, res.ContentLength)
|
||||
}
|
||||
if err := of.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
pw.print()
|
||||
|
||||
if !bytes.Equal(hash.Sum(nil), wantHash) {
|
||||
return fmt.Errorf("SHA-256 of downloaded MSI didn't match expected value")
|
||||
}
|
||||
log.Printf("hash matched")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type progressWriter struct {
|
||||
done int64
|
||||
total int64
|
||||
lastPrint time.Time
|
||||
}
|
||||
|
||||
func (pw *progressWriter) Write(p []byte) (n int, err error) {
|
||||
pw.done += int64(len(p))
|
||||
if time.Since(pw.lastPrint) > 2*time.Second {
|
||||
pw.print()
|
||||
}
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
func (pw *progressWriter) print() {
|
||||
pw.lastPrint = time.Now()
|
||||
log.Printf("Downloaded %v/%v (%.1f%%)", pw.done, pw.total, float64(pw.done)/float64(pw.total)*100)
|
||||
}
|
||||
76
cmd/tailscale/cli/update_test.go
Normal file
76
cmd/tailscale/cli/update_test.go
Normal file
@@ -0,0 +1,76 @@
|
||||
// Copyright (c) 2023 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package cli
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestUpdateDebianAptSourcesListBytes(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
toTrack string
|
||||
in string
|
||||
want string // empty means want no change
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "stable-to-unstable",
|
||||
toTrack: "unstable",
|
||||
in: "# Tailscale packages for debian buster\ndeb https://pkgs.tailscale.com/stable/debian bullseye main\n",
|
||||
want: "# Tailscale packages for debian buster\ndeb https://pkgs.tailscale.com/unstable/debian bullseye main\n",
|
||||
},
|
||||
{
|
||||
name: "stable-unchanged",
|
||||
toTrack: "stable",
|
||||
in: "# Tailscale packages for debian buster\ndeb https://pkgs.tailscale.com/stable/debian bullseye main\n",
|
||||
},
|
||||
{
|
||||
name: "if-both-stable-and-unstable-dont-change",
|
||||
toTrack: "stable",
|
||||
in: "# Tailscale packages for debian buster\n" +
|
||||
"deb https://pkgs.tailscale.com/stable/debian bullseye main\n" +
|
||||
"deb https://pkgs.tailscale.com/unstable/debian bullseye main\n",
|
||||
},
|
||||
{
|
||||
name: "if-both-stable-and-unstable-dont-change-unstable",
|
||||
toTrack: "unstable",
|
||||
in: "# Tailscale packages for debian buster\n" +
|
||||
"deb https://pkgs.tailscale.com/stable/debian bullseye main\n" +
|
||||
"deb https://pkgs.tailscale.com/unstable/debian bullseye main\n",
|
||||
},
|
||||
{
|
||||
name: "signed-by-form",
|
||||
toTrack: "unstable",
|
||||
in: "# Tailscale packages for ubuntu jammy\ndeb [signed-by=/usr/share/keyrings/tailscale-archive-keyring.gpg] https://pkgs.tailscale.com/stable/ubuntu jammy main\n",
|
||||
want: "# Tailscale packages for ubuntu jammy\ndeb [signed-by=/usr/share/keyrings/tailscale-archive-keyring.gpg] https://pkgs.tailscale.com/unstable/ubuntu jammy main\n",
|
||||
},
|
||||
{
|
||||
name: "unsupported-lines",
|
||||
toTrack: "unstable",
|
||||
in: "# Tailscale packages for ubuntu jammy\ndeb [signed-by=/usr/share/keyrings/tailscale-archive-keyring.gpg] https://pkgs.tailscale.com/foobar/ubuntu jammy main\n",
|
||||
wantErr: "unexpected/unsupported /etc/apt/sources.list.d/tailscale.list contents",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
newContent, err := updateDebianAptSourcesListBytes([]byte(tt.in), tt.toTrack)
|
||||
if err != nil {
|
||||
if err.Error() != tt.wantErr {
|
||||
t.Fatalf("error = %v; want %q", err, tt.wantErr)
|
||||
}
|
||||
return
|
||||
}
|
||||
if tt.wantErr != "" {
|
||||
t.Fatalf("got no error; want %q", tt.wantErr)
|
||||
}
|
||||
var gotChange string
|
||||
if string(newContent) != tt.in {
|
||||
gotChange = string(newContent)
|
||||
}
|
||||
if gotChange != tt.want {
|
||||
t.Errorf("wrong result\n got: %q\nwant: %q", gotChange, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
21
cmd/tailscale/cli/update_windows.go
Normal file
21
cmd/tailscale/cli/update_windows.go
Normal file
@@ -0,0 +1,21 @@
|
||||
// Copyright (c) 2023 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Windows-specific stuff that can't go in update.go because it needs
|
||||
// x/sys/windows.
|
||||
|
||||
package cli
|
||||
|
||||
import (
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
func init() {
|
||||
markTempFileFunc = markTempFileWindows
|
||||
}
|
||||
|
||||
func markTempFileWindows(name string) error {
|
||||
name16 := windows.StringToUTF16Ptr(name)
|
||||
return windows.MoveFileEx(name16, nil, windows.MOVEFILE_DELAY_UNTIL_REBOOT)
|
||||
}
|
||||
@@ -6,10 +6,13 @@ package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/version"
|
||||
)
|
||||
|
||||
@@ -20,6 +23,7 @@ var versionCmd = &ffcli.Command{
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
fs := newFlagSet("version")
|
||||
fs.BoolVar(&versionArgs.daemon, "daemon", false, "also print local node's daemon version")
|
||||
fs.BoolVar(&versionArgs.json, "json", false, "output in JSON format")
|
||||
return fs
|
||||
})(),
|
||||
Exec: runVersion,
|
||||
@@ -27,23 +31,38 @@ var versionCmd = &ffcli.Command{
|
||||
|
||||
var versionArgs struct {
|
||||
daemon bool // also check local node's daemon version
|
||||
json bool
|
||||
}
|
||||
|
||||
func runVersion(ctx context.Context, args []string) error {
|
||||
if len(args) > 0 {
|
||||
return fmt.Errorf("too many non-flag arguments: %q", args)
|
||||
}
|
||||
if !versionArgs.daemon {
|
||||
var err error
|
||||
var st *ipnstate.Status
|
||||
|
||||
if versionArgs.daemon {
|
||||
st, err = localClient.StatusWithoutPeers(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if versionArgs.json {
|
||||
m := version.GetMeta()
|
||||
if st != nil {
|
||||
m.DaemonLong = st.Version
|
||||
}
|
||||
e := json.NewEncoder(os.Stdout)
|
||||
e.SetIndent("", "\t")
|
||||
return e.Encode(m)
|
||||
}
|
||||
|
||||
if st == nil {
|
||||
outln(version.String())
|
||||
return nil
|
||||
}
|
||||
|
||||
printf("Client: %s\n", version.String())
|
||||
|
||||
st, err := localClient.StatusWithoutPeers(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
printf("Daemon: %s\n", st.Version)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -225,6 +225,11 @@ a {
|
||||
background-color: rgba(249, 247, 246, var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.bg-orange-0 {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgba(255, 250, 238, var(--tw-bg-opacity));
|
||||
}
|
||||
|
||||
.border-gray-200 {
|
||||
--tw-border-opacity: 1;
|
||||
border-color: rgba(238, 235, 234, var(--tw-border-opacity));
|
||||
@@ -1119,6 +1124,11 @@ a {
|
||||
color: rgba(35, 34, 34, var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.text-orange-800 {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgba(66, 14, 17, var(--tw-text-opacity));
|
||||
}
|
||||
|
||||
.leading-3 {
|
||||
line-height: 0.75rem;
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/tailcfg"
|
||||
@@ -58,6 +59,8 @@ type tmplData struct {
|
||||
AdvertiseExitNode bool
|
||||
AdvertiseRoutes string
|
||||
LicensesURL string
|
||||
TUNMode bool
|
||||
IsSynology bool
|
||||
}
|
||||
|
||||
var webCmd = &ffcli.Command{
|
||||
@@ -223,8 +226,8 @@ func qnapAuthnQtoken(r *http.Request, user, token string) (string, *qnapAuthResp
|
||||
"user": []string{user},
|
||||
}
|
||||
u := url.URL{
|
||||
Scheme: r.URL.Scheme,
|
||||
Host: r.URL.Host,
|
||||
Scheme: "http",
|
||||
Host: "127.0.0.1:8080",
|
||||
Path: "/cgi-bin/authLogin.cgi",
|
||||
RawQuery: query.Encode(),
|
||||
}
|
||||
@@ -237,8 +240,8 @@ func qnapAuthnSid(r *http.Request, user, sid string) (string, *qnapAuthResponse,
|
||||
"sid": []string{sid},
|
||||
}
|
||||
u := url.URL{
|
||||
Scheme: r.URL.Scheme,
|
||||
Host: r.URL.Host,
|
||||
Scheme: "http",
|
||||
Host: "127.0.0.1:8080",
|
||||
Path: "/cgi-bin/authLogin.cgi",
|
||||
RawQuery: query.Encode(),
|
||||
}
|
||||
@@ -247,7 +250,14 @@ func qnapAuthnSid(r *http.Request, user, sid string) (string, *qnapAuthResponse,
|
||||
}
|
||||
|
||||
func qnapAuthnFinish(user, url string) (string, *qnapAuthResponse, error) {
|
||||
resp, err := http.Get(url)
|
||||
// QNAP Force HTTPS mode uses a self-signed certificate. Even importing
|
||||
// the QNAP root CA isn't enough, the cert doesn't have a usable CN nor
|
||||
// SAN. See https://github.com/tailscale/tailscale/issues/6903
|
||||
tr := &http.Transport{
|
||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||
}
|
||||
client := &http.Client{Transport: tr}
|
||||
resp, err := client.Get(url)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
@@ -331,7 +341,7 @@ func webHandler(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
st, err := localClient.Status(ctx)
|
||||
st, err := localClient.StatusWithoutPeers(ctx)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
@@ -401,6 +411,8 @@ func webHandler(w http.ResponseWriter, r *http.Request) {
|
||||
Status: st.BackendState,
|
||||
DeviceName: deviceName,
|
||||
LicensesURL: licensesURL(),
|
||||
TUNMode: st.TUN,
|
||||
IsSynology: distro.Get() == distro.Synology || envknob.Bool("TS_FAKE_SYNOLOGY"),
|
||||
}
|
||||
exitNodeRouteV4 := netip.MustParsePrefix("0.0.0.0/0")
|
||||
exitNodeRouteV6 := netip.MustParsePrefix("::/0")
|
||||
|
||||
@@ -98,6 +98,15 @@
|
||||
<div class="mb-4">
|
||||
<a href="#" class="mb-4 link font-medium js-loginButton" target="_blank">Reauthenticate</a>
|
||||
</div>
|
||||
{{ if .IsSynology }}
|
||||
<div class="border border-gray-200 bg-orange-0 rounded-lg p-2 pl-3 pr-3 mb-8 width-full text-orange-800">
|
||||
Outgoing access {{ if .TUNMode }}enabled{{ else }}not configured{{ end }}.
|
||||
<nobr><a href="https://tailscale.com/kb/1152/synology-outbound/"
|
||||
class="font-medium link"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">Learn more →</a></nobr>
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
</main>
|
||||
<footer class="container max-w-lg mx-auto text-center">
|
||||
|
||||
@@ -10,7 +10,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
W 💣 github.com/alexbrainman/sspi/negotiate from tailscale.com/net/tshttpproxy
|
||||
github.com/fxamacker/cbor/v2 from tailscale.com/tka
|
||||
github.com/golang/groupcache/lru from tailscale.com/net/dnscache
|
||||
D github.com/google/uuid from tailscale.com/util/quarantine
|
||||
github.com/google/uuid from tailscale.com/util/quarantine+
|
||||
github.com/hdevalence/ed25519consensus from tailscale.com/tka
|
||||
LW github.com/josharian/native from github.com/mdlayher/netlink+
|
||||
L 💣 github.com/jsimonetti/rtnetlink from tailscale.com/net/interfaces
|
||||
@@ -49,7 +49,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
tailscale.com/atomicfile from tailscale.com/ipn+
|
||||
tailscale.com/client/tailscale from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/client/tailscale/apitype from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/cmd/tailscale/cli from tailscale.com/cmd/tailscale
|
||||
💣 tailscale.com/cmd/tailscale/cli from tailscale.com/cmd/tailscale
|
||||
tailscale.com/control/controlbase from tailscale.com/control/controlhttp
|
||||
tailscale.com/control/controlhttp from tailscale.com/cmd/tailscale/cli
|
||||
tailscale.com/control/controlknobs from tailscale.com/net/portmapper
|
||||
@@ -98,7 +98,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
tailscale.com/types/opt from tailscale.com/net/netcheck+
|
||||
tailscale.com/types/persist from tailscale.com/ipn
|
||||
tailscale.com/types/preftype from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/types/ptr from tailscale.com/hostinfo
|
||||
tailscale.com/types/ptr from tailscale.com/hostinfo+
|
||||
tailscale.com/types/structs from tailscale.com/ipn+
|
||||
tailscale.com/types/tkatype from tailscale.com/types/key+
|
||||
tailscale.com/types/views from tailscale.com/tailcfg+
|
||||
@@ -115,7 +115,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/singleflight from tailscale.com/net/dnscache
|
||||
tailscale.com/util/strs from tailscale.com/hostinfo+
|
||||
W 💣 tailscale.com/util/winutil from tailscale.com/hostinfo+
|
||||
💣 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+
|
||||
tailscale.com/wgengine/filter from tailscale.com/types/netmap
|
||||
@@ -184,7 +184,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
crypto/tls from github.com/tcnksm/go-httpstat+
|
||||
crypto/x509 from crypto/tls+
|
||||
crypto/x509/pkix from crypto/x509+
|
||||
D database/sql/driver from github.com/google/uuid
|
||||
database/sql/driver from github.com/google/uuid
|
||||
embed from tailscale.com/cmd/tailscale/cli+
|
||||
encoding from encoding/json+
|
||||
encoding/asn1 from crypto/x509+
|
||||
|
||||
@@ -12,6 +12,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
L github.com/aws/aws-sdk-go-v2 from github.com/aws/aws-sdk-go-v2/internal/ini
|
||||
L github.com/aws/aws-sdk-go-v2/aws from github.com/aws/aws-sdk-go-v2/aws/middleware+
|
||||
L github.com/aws/aws-sdk-go-v2/aws/arn from tailscale.com/ipn/store/awsstore
|
||||
L github.com/aws/aws-sdk-go-v2/aws/defaults from github.com/aws/aws-sdk-go-v2/service/ssm
|
||||
L github.com/aws/aws-sdk-go-v2/aws/middleware from github.com/aws/aws-sdk-go-v2/aws/retry+
|
||||
L github.com/aws/aws-sdk-go-v2/aws/protocol/query from github.com/aws/aws-sdk-go-v2/service/sts
|
||||
L github.com/aws/aws-sdk-go-v2/aws/protocol/restjson from github.com/aws/aws-sdk-go-v2/service/ssm+
|
||||
@@ -51,11 +52,14 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
L github.com/aws/aws-sdk-go-v2/service/sts/internal/endpoints from github.com/aws/aws-sdk-go-v2/service/sts
|
||||
L github.com/aws/aws-sdk-go-v2/service/sts/types from github.com/aws/aws-sdk-go-v2/credentials/stscreds+
|
||||
L github.com/aws/smithy-go from github.com/aws/aws-sdk-go-v2/aws/protocol/restjson+
|
||||
L github.com/aws/smithy-go/auth/bearer from github.com/aws/aws-sdk-go-v2/aws
|
||||
L github.com/aws/smithy-go/context from github.com/aws/smithy-go/auth/bearer
|
||||
L github.com/aws/smithy-go/document from github.com/aws/aws-sdk-go-v2/service/ssm+
|
||||
L github.com/aws/smithy-go/encoding from github.com/aws/smithy-go/encoding/json+
|
||||
L github.com/aws/smithy-go/encoding/httpbinding from github.com/aws/aws-sdk-go-v2/aws/protocol/query+
|
||||
L github.com/aws/smithy-go/encoding/json from github.com/aws/aws-sdk-go-v2/service/ssm
|
||||
L github.com/aws/smithy-go/encoding/xml from github.com/aws/aws-sdk-go-v2/service/sts
|
||||
L github.com/aws/smithy-go/internal/sync/singleflight from github.com/aws/smithy-go/auth/bearer
|
||||
L github.com/aws/smithy-go/io from github.com/aws/aws-sdk-go-v2/feature/ec2/imds+
|
||||
L github.com/aws/smithy-go/logging from github.com/aws/aws-sdk-go-v2/aws+
|
||||
L github.com/aws/smithy-go/middleware from github.com/aws/aws-sdk-go-v2/aws+
|
||||
@@ -247,7 +251,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
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/net/dns+
|
||||
tailscale.com/net/tstun from tailscale.com/cmd/tailscaled+
|
||||
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
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build linux || darwin || freebsd
|
||||
//go:build linux || darwin || freebsd || openbsd
|
||||
|
||||
package main
|
||||
|
||||
|
||||
@@ -526,6 +526,9 @@ func getLocalBackend(ctx context.Context, logf logger.Logf, logid string) (_ *ip
|
||||
return nil, fmt.Errorf("ipnlocal.NewLocalBackend: %w", err)
|
||||
}
|
||||
lb.SetVarRoot(opts.VarRoot)
|
||||
if logPol != nil {
|
||||
lb.SetLogFlusher(logPol.Logtail.StartFlush)
|
||||
}
|
||||
if root := lb.TailscaleVarRoot(); root != "" {
|
||||
dnsfallback.SetCachePath(filepath.Join(root, "derpmap.cached.json"))
|
||||
}
|
||||
|
||||
46
cmd/testwrapper/flakytest/flakytest.go
Normal file
46
cmd/testwrapper/flakytest/flakytest.go
Normal file
@@ -0,0 +1,46 @@
|
||||
// Copyright (c) 2023 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package flakytest contains test helpers for marking a test as flaky. For
|
||||
// tests run using cmd/testwrapper, a failed flaky test will cause tests to be
|
||||
// re-run a few time until they succeed or exceed our iteration limit.
|
||||
package flakytest
|
||||
|
||||
import (
|
||||
"os"
|
||||
"regexp"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// InTestWrapper returns whether or not this binary is running under our test
|
||||
// wrapper.
|
||||
func InTestWrapper() bool {
|
||||
return os.Getenv("TS_IN_TESTWRAPPER") != ""
|
||||
}
|
||||
|
||||
var issueRegexp = regexp.MustCompile(`\Ahttps://github\.com/tailscale/[a-zA-Z0-9_.-]+/issues/\d+\z`)
|
||||
|
||||
// Mark sets the current test as a flaky test, such that if it fails, it will
|
||||
// be retried a few times on failure. issue must be a GitHub issue that tracks
|
||||
// the status of the flaky test being marked, of the format:
|
||||
//
|
||||
// https://github.com/tailscale/myRepo-H3re/issues/12345
|
||||
func Mark(t testing.TB, issue string) {
|
||||
if !issueRegexp.MatchString(issue) {
|
||||
t.Fatalf("bad issue format: %q", issue)
|
||||
}
|
||||
|
||||
if !InTestWrapper() {
|
||||
return
|
||||
}
|
||||
|
||||
t.Cleanup(func() {
|
||||
if t.Failed() {
|
||||
t.Logf("flakytest: signaling test wrapper to retry test")
|
||||
|
||||
// Signal to test wrapper that we should restart.
|
||||
os.Exit(123)
|
||||
}
|
||||
})
|
||||
}
|
||||
27
cmd/testwrapper/flakytest/flakytest_test.go
Normal file
27
cmd/testwrapper/flakytest/flakytest_test.go
Normal file
@@ -0,0 +1,27 @@
|
||||
// Copyright (c) 2023 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package flakytest
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestIssueFormat(t *testing.T) {
|
||||
testCases := []struct {
|
||||
issue string
|
||||
want bool
|
||||
}{
|
||||
{"https://github.com/tailscale/cOrp/issues/1234", true},
|
||||
{"https://github.com/otherproject/corp/issues/1234", false},
|
||||
{"https://github.com/tailscale/corp/issues/", false},
|
||||
}
|
||||
for _, testCase := range testCases {
|
||||
if issueRegexp.MatchString(testCase.issue) != testCase.want {
|
||||
ss := ""
|
||||
if !testCase.want {
|
||||
ss = " not"
|
||||
}
|
||||
t.Errorf("expected issueRegexp to%s match %q", ss, testCase.issue)
|
||||
}
|
||||
}
|
||||
}
|
||||
63
cmd/testwrapper/testwrapper.go
Normal file
63
cmd/testwrapper/testwrapper.go
Normal file
@@ -0,0 +1,63 @@
|
||||
// Copyright (c) 2023 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// testwrapper is a wrapper for retrying flaky tests, using the -exec flag of
|
||||
// 'go test'. Tests that are flaky can use the 'flakytest' subpackage to mark
|
||||
// themselves as flaky and be retried on failure.
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
const (
|
||||
retryStatus = 123
|
||||
maxIterations = 3
|
||||
)
|
||||
|
||||
func main() {
|
||||
ctx := context.Background()
|
||||
debug := os.Getenv("TS_TESTWRAPPER_DEBUG") != ""
|
||||
|
||||
log.SetPrefix("testwrapper: ")
|
||||
if !debug {
|
||||
log.SetFlags(0)
|
||||
}
|
||||
|
||||
for i := 1; i <= maxIterations; i++ {
|
||||
if i > 1 {
|
||||
log.Printf("retrying flaky tests (%d of %d)", i, maxIterations)
|
||||
}
|
||||
cmd := exec.CommandContext(ctx, os.Args[1], os.Args[2:]...)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Env = append(os.Environ(), "TS_IN_TESTWRAPPER=1")
|
||||
err := cmd.Run()
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
|
||||
var exitErr *exec.ExitError
|
||||
if !errors.As(err, &exitErr) {
|
||||
if debug {
|
||||
log.Printf("error isn't an ExitError")
|
||||
}
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if code := exitErr.ExitCode(); code != retryStatus {
|
||||
if debug {
|
||||
log.Printf("code (%d) != retryStatus (%d)", code, retryStatus)
|
||||
}
|
||||
os.Exit(code)
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("test did not pass in %d iterations", maxIterations)
|
||||
os.Exit(1)
|
||||
}
|
||||
@@ -384,7 +384,7 @@ func main() {
|
||||
genView(buf, it, typ, pkg.Types)
|
||||
}
|
||||
out := pkg.Name + "_view.go"
|
||||
if err := codegen.WritePackageFile("tailscale/cmd/viewer", pkg, out, it, buf); err != nil {
|
||||
if err := codegen.WritePackageFile("tailscale/cmd/viewer", pkg, out, codegen.CopyrightYear("."), it, buf); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if runCloner {
|
||||
|
||||
@@ -77,6 +77,7 @@ type Direct struct {
|
||||
popBrowser func(url string) // or nil
|
||||
c2nHandler http.Handler // or nil
|
||||
onClientVersion func(*tailcfg.ClientVersion) // or nil
|
||||
onControlTime func(time.Time) // or nil
|
||||
|
||||
dialPlan ControlDialPlanner // can be nil
|
||||
|
||||
@@ -116,6 +117,7 @@ type Options struct {
|
||||
LinkMonitor *monitor.Mon // optional link monitor
|
||||
PopBrowserURL func(url string) // optional func to open browser
|
||||
OnClientVersion func(*tailcfg.ClientVersion) // optional func to inform GUI of client version status
|
||||
OnControlTime func(time.Time) // optional func to notify callers of new time from control
|
||||
Dialer *tsdial.Dialer // non-nil
|
||||
C2NHandler http.Handler // or nil
|
||||
|
||||
@@ -244,6 +246,7 @@ func NewDirect(opts Options) (*Direct, error) {
|
||||
pinger: opts.Pinger,
|
||||
popBrowser: opts.PopBrowserURL,
|
||||
onClientVersion: opts.OnClientVersion,
|
||||
onControlTime: opts.OnControlTime,
|
||||
c2nHandler: opts.C2NHandler,
|
||||
dialer: opts.Dialer,
|
||||
dialPlan: opts.DialPlan,
|
||||
@@ -1016,6 +1019,9 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, readOnly bool
|
||||
}
|
||||
if resp.ControlTime != nil && !resp.ControlTime.IsZero() {
|
||||
c.logf.JSON(1, "controltime", resp.ControlTime.UTC())
|
||||
if c.onControlTime != nil {
|
||||
c.onControlTime(*resp.ControlTime)
|
||||
}
|
||||
}
|
||||
if resp.KeepAlive {
|
||||
vlogf("netmap: got keep-alive")
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/netmap"
|
||||
"tailscale.com/types/opt"
|
||||
"tailscale.com/types/views"
|
||||
"tailscale.com/wgengine/filter"
|
||||
)
|
||||
|
||||
@@ -40,6 +41,7 @@ type mapSession struct {
|
||||
lastDNSConfig *tailcfg.DNSConfig
|
||||
lastDERPMap *tailcfg.DERPMap
|
||||
lastUserProfile map[tailcfg.UserID]tailcfg.UserProfile
|
||||
lastPacketFilterRules views.Slice[tailcfg.FilterRule]
|
||||
lastParsedPacketFilter []filter.Match
|
||||
lastSSHPolicy *tailcfg.SSHPolicy
|
||||
collectServices bool
|
||||
@@ -96,6 +98,7 @@ func (ms *mapSession) netmapForResponse(resp *tailcfg.MapResponse) *netmap.Netwo
|
||||
|
||||
if pf := resp.PacketFilter; pf != nil {
|
||||
var err error
|
||||
ms.lastPacketFilterRules = views.SliceOf(pf)
|
||||
ms.lastParsedPacketFilter, err = filter.MatchesFromFilterRules(pf)
|
||||
if err != nil {
|
||||
ms.logf("parsePacketFilter: %v", err)
|
||||
@@ -147,21 +150,22 @@ func (ms *mapSession) netmapForResponse(resp *tailcfg.MapResponse) *netmap.Netwo
|
||||
}
|
||||
|
||||
nm := &netmap.NetworkMap{
|
||||
NodeKey: ms.privateNodeKey.Public(),
|
||||
PrivateKey: ms.privateNodeKey,
|
||||
MachineKey: ms.machinePubKey,
|
||||
Peers: resp.Peers,
|
||||
UserProfiles: make(map[tailcfg.UserID]tailcfg.UserProfile),
|
||||
Domain: ms.lastDomain,
|
||||
DomainAuditLogID: ms.lastDomainAuditLogID,
|
||||
DNS: *ms.lastDNSConfig,
|
||||
PacketFilter: ms.lastParsedPacketFilter,
|
||||
SSHPolicy: ms.lastSSHPolicy,
|
||||
CollectServices: ms.collectServices,
|
||||
DERPMap: ms.lastDERPMap,
|
||||
Debug: debug,
|
||||
ControlHealth: ms.lastHealth,
|
||||
TKAEnabled: ms.lastTKAInfo != nil && !ms.lastTKAInfo.Disabled,
|
||||
NodeKey: ms.privateNodeKey.Public(),
|
||||
PrivateKey: ms.privateNodeKey,
|
||||
MachineKey: ms.machinePubKey,
|
||||
Peers: resp.Peers,
|
||||
UserProfiles: make(map[tailcfg.UserID]tailcfg.UserProfile),
|
||||
Domain: ms.lastDomain,
|
||||
DomainAuditLogID: ms.lastDomainAuditLogID,
|
||||
DNS: *ms.lastDNSConfig,
|
||||
PacketFilter: ms.lastParsedPacketFilter,
|
||||
PacketFilterRules: ms.lastPacketFilterRules,
|
||||
SSHPolicy: ms.lastSSHPolicy,
|
||||
CollectServices: ms.collectServices,
|
||||
DERPMap: ms.lastDERPMap,
|
||||
Debug: debug,
|
||||
ControlHealth: ms.lastHealth,
|
||||
TKAEnabled: ms.lastTKAInfo != nil && !ms.lastTKAInfo.Disabled,
|
||||
}
|
||||
ms.netMapBuilding = nm
|
||||
|
||||
@@ -306,6 +310,9 @@ func undeltaPeers(mapRes *tailcfg.MapResponse, prev []*tailcfg.Node) {
|
||||
if ec.DERPRegion != 0 {
|
||||
n.DERP = fmt.Sprintf("%s:%v", tailcfg.DerpMagicIP, ec.DERPRegion)
|
||||
}
|
||||
if ec.Cap != 0 {
|
||||
n.Cap = ec.Cap
|
||||
}
|
||||
if ec.Endpoints != nil {
|
||||
n.Endpoints = ec.Endpoints
|
||||
}
|
||||
|
||||
@@ -314,20 +314,29 @@ func formatNodes(nodes []*tailcfg.Node) string {
|
||||
if i > 0 {
|
||||
sb.WriteString(", ")
|
||||
}
|
||||
var extra string
|
||||
fmt.Fprintf(&sb, "(%d, %q", n.ID, n.Name)
|
||||
|
||||
if n.Online != nil {
|
||||
extra += fmt.Sprintf(", online=%v", *n.Online)
|
||||
fmt.Fprintf(&sb, ", online=%v", *n.Online)
|
||||
}
|
||||
if n.LastSeen != nil {
|
||||
extra += fmt.Sprintf(", lastSeen=%v", n.LastSeen.Unix())
|
||||
fmt.Fprintf(&sb, ", lastSeen=%v", n.LastSeen.Unix())
|
||||
}
|
||||
fmt.Fprintf(&sb, "(%d, %q%s)", n.ID, n.Name, extra)
|
||||
if n.Key != (key.NodePublic{}) {
|
||||
fmt.Fprintf(&sb, ", key=%v", n.Key.String())
|
||||
}
|
||||
if n.Expired {
|
||||
fmt.Fprintf(&sb, ", expired=true")
|
||||
}
|
||||
sb.WriteString(")")
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func newTestMapSession(t *testing.T) *mapSession {
|
||||
return newMapSession(key.NewNode())
|
||||
ms := newMapSession(key.NewNode())
|
||||
ms.logf = t.Logf
|
||||
return ms
|
||||
}
|
||||
|
||||
func TestNetmapForResponse(t *testing.T) {
|
||||
|
||||
@@ -82,6 +82,9 @@ func (a *Dialer) getProxyFunc() func(*http.Request) (*url.URL, error) {
|
||||
// httpsFallbackDelay is how long we'll wait for a.HTTPPort to work before
|
||||
// starting to try a.HTTPSPort.
|
||||
func (a *Dialer) httpsFallbackDelay() time.Duration {
|
||||
if forceNoise443() {
|
||||
return time.Nanosecond
|
||||
}
|
||||
if v := a.testFallbackDelay; v != 0 {
|
||||
return v
|
||||
}
|
||||
@@ -248,6 +251,18 @@ func (a *Dialer) dial(ctx context.Context) (*ClientConn, error) {
|
||||
return a.dialHost(ctx, netip.Addr{})
|
||||
}
|
||||
|
||||
// The TS_FORCE_NOISE_443 envknob forces the controlclient noise dialer to
|
||||
// always use port 443 HTTPS connections to the controlplane and not try the
|
||||
// port 80 HTTP fast path.
|
||||
//
|
||||
// This is currently (2023-01-17) needed for Docker Desktop's "VPNKit" proxy
|
||||
// that breaks port 80 for us post-Noise-handshake, causing us to never try port
|
||||
// 443. Until one of Docker's proxy and/or this package's port 443 fallback is
|
||||
// fixed, this is a workaround. It might also be useful for future debugging.
|
||||
var forceNoise443 = envknob.RegisterBool("TS_FORCE_NOISE_443")
|
||||
|
||||
var debugNoiseDial = envknob.RegisterBool("TS_DEBUG_NOISE_DIAL")
|
||||
|
||||
// dialHost connects to the configured Dialer.Hostname and upgrades the
|
||||
// connection into a controlbase.Conn. If addr is valid, then no DNS is used
|
||||
// and the connection will be made to the provided address.
|
||||
@@ -279,7 +294,13 @@ func (a *Dialer) dialHost(ctx context.Context, addr netip.Addr) (*ClientConn, er
|
||||
}
|
||||
ch := make(chan tryURLRes) // must be unbuffered
|
||||
try := func(u *url.URL) {
|
||||
if debugNoiseDial() {
|
||||
a.logf("trying noise dial (%v, %v) ...", u, addr)
|
||||
}
|
||||
cbConn, err := a.dialURL(ctx, u, addr)
|
||||
if debugNoiseDial() {
|
||||
a.logf("noise dial (%v, %v) = (%v, %v)", u, addr, cbConn, err)
|
||||
}
|
||||
select {
|
||||
case ch <- tryURLRes{u, cbConn, err}:
|
||||
case <-ctx.Done():
|
||||
@@ -289,8 +310,10 @@ func (a *Dialer) dialHost(ctx context.Context, addr netip.Addr) (*ClientConn, er
|
||||
}
|
||||
}
|
||||
|
||||
// Start the plaintext HTTP attempt first.
|
||||
go try(u80)
|
||||
// Start the plaintext HTTP attempt first, unless disabled by the envknob.
|
||||
if !forceNoise443() {
|
||||
go try(u80)
|
||||
}
|
||||
|
||||
// In case outbound port 80 blocked or MITM'ed poorly, start a backup timer
|
||||
// to dial port 443 if port 80 doesn't either succeed or fail quickly.
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"nhooyr.io/websocket"
|
||||
@@ -35,7 +36,7 @@ func AcceptHTTP(ctx context.Context, w http.ResponseWriter, r *http.Request, pri
|
||||
}
|
||||
|
||||
func acceptHTTP(ctx context.Context, w http.ResponseWriter, r *http.Request, private key.MachinePrivate, earlyWrite func(protocolVersion int, w io.Writer) error) (_ *controlbase.Conn, retErr error) {
|
||||
next := r.Header.Get("Upgrade")
|
||||
next := strings.ToLower(r.Header.Get("Upgrade"))
|
||||
if next == "" {
|
||||
http.Error(w, "missing next protocol", http.StatusBadRequest)
|
||||
return nil, errors.New("no next protocol in HTTP request")
|
||||
|
||||
@@ -329,6 +329,13 @@ func NoLogsNoSupport() bool {
|
||||
return Bool("TS_NO_LOGS_NO_SUPPORT")
|
||||
}
|
||||
|
||||
var allowRemoteUpdate = RegisterBool("TS_ALLOW_ADMIN_CONSOLE_REMOTE_UPDATE")
|
||||
|
||||
// AllowsRemoteUpdate reports whether this node has opted-in to letting the
|
||||
// Tailscale control plane initiate a Tailscale update (e.g. on behalf of an
|
||||
// admin on the admin console).
|
||||
func AllowsRemoteUpdate() bool { return allowRemoteUpdate() }
|
||||
|
||||
// SetNoLogsNoSupport enables no-logs-no-support mode.
|
||||
func SetNoLogsNoSupport() {
|
||||
Setenv("TS_NO_LOGS_NO_SUPPORT", "true")
|
||||
|
||||
@@ -154,4 +154,4 @@
|
||||
in
|
||||
flake-utils.lib.eachDefaultSystem (system: flakeForSystem nixpkgs system);
|
||||
}
|
||||
# nix-direnv cache busting line: sha256-imidcDJGVor43PqdTX7Js4/tjQ0JA2E1GdjuyLiPDHI= sha256-+5icFKDHXt3JMbUjLQGes4R+GeUi48xRgGd0yPKVrw0=
|
||||
# nix-direnv cache busting line: sha256-imidcDJGVor43PqdTX7Js4/tjQ0JA2E1GdjuyLiPDHI= sha256-zBfANuVhYtDOOiZu6SPVmM0cTEsOHVdycOz5EZDapKk=
|
||||
|
||||
25
go.mod
25
go.mod
@@ -9,11 +9,11 @@ require (
|
||||
github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74
|
||||
github.com/andybalholm/brotli v1.0.3
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be
|
||||
github.com/aws/aws-sdk-go-v2 v1.11.2
|
||||
github.com/aws/aws-sdk-go-v2 v1.17.3
|
||||
github.com/aws/aws-sdk-go-v2/config v1.11.0
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.7.4
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.21.0
|
||||
github.com/aws/aws-sdk-go-v2/service/ssm v1.17.1
|
||||
github.com/aws/aws-sdk-go-v2/service/ssm v1.35.0
|
||||
github.com/coreos/go-iptables v0.6.0
|
||||
github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf
|
||||
github.com/creack/pty v1.1.17
|
||||
@@ -64,21 +64,21 @@ require (
|
||||
github.com/tc-hib/winres v0.1.6
|
||||
github.com/tcnksm/go-httpstat v0.2.0
|
||||
github.com/toqueteos/webbrowser v1.2.0
|
||||
github.com/u-root/u-root v0.9.1-0.20221111022710-6e9699743f5d
|
||||
github.com/u-root/u-root v0.9.1-0.20230109201855-948a78c969ad
|
||||
github.com/vishvananda/netlink v1.1.1-0.20211118161826-650dca95af54
|
||||
go.uber.org/zap v1.21.0
|
||||
go4.org/mem v0.0.0-20210711025021-927187094b94
|
||||
go4.org/netipx v0.0.0-20220725152314-7e7bdc8411bf
|
||||
golang.org/x/crypto v0.3.0
|
||||
golang.org/x/exp v0.0.0-20221205204356-47842c84f3db
|
||||
golang.org/x/net v0.2.0
|
||||
golang.org/x/net v0.5.0
|
||||
golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5
|
||||
golang.org/x/sync v0.1.0
|
||||
golang.org/x/sys v0.3.1-0.20221220025402-2204b6615fb8
|
||||
golang.org/x/term v0.2.0
|
||||
golang.org/x/sys v0.4.0
|
||||
golang.org/x/term v0.4.0
|
||||
golang.org/x/time v0.0.0-20220609170525-579cf78fd858
|
||||
golang.org/x/tools v0.2.0
|
||||
golang.zx2c4.com/wintun v0.0.0-20211104114900-415007cec224
|
||||
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.20220517111757-f4a2f64ce238
|
||||
@@ -104,7 +104,7 @@ require (
|
||||
github.com/Masterminds/semver/v3 v3.1.1 // indirect
|
||||
github.com/Masterminds/sprig v2.22.0+incompatible // indirect
|
||||
github.com/OpenPeeDeeP/depguard v1.0.1 // indirect
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20211112122917-428f8eabeeb3 // indirect
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20221026131551-cf6655e29de4 // indirect
|
||||
github.com/PuerkitoBio/purell v1.1.1 // indirect
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
|
||||
github.com/acomagu/bufpipe v1.0.3 // indirect
|
||||
@@ -114,15 +114,15 @@ require (
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.0.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.6.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.8.2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.0.2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.27 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.21 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.5.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.5.2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.9.2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.6.2 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.11.1 // indirect
|
||||
github.com/aws/smithy-go v1.9.0 // indirect
|
||||
github.com/aws/smithy-go v1.13.5 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/bkielbasa/cyclop v1.2.0 // indirect
|
||||
github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb // indirect
|
||||
@@ -134,6 +134,7 @@ require (
|
||||
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
||||
github.com/charithe/durationcheck v0.0.9 // indirect
|
||||
github.com/chavacava/garif v0.0.0-20210405164556-e8a0a408d6af // indirect
|
||||
github.com/cloudflare/circl v1.1.0 // indirect
|
||||
github.com/containerd/stargz-snapshotter/estargz v0.11.4 // indirect
|
||||
github.com/daixiang0/gci v0.2.9 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
@@ -302,7 +303,7 @@ require (
|
||||
golang.org/x/exp/typeparams v0.0.0-20220328175248-053ad81199eb // indirect
|
||||
golang.org/x/image v0.0.0-20201208152932-35266b937fa6 // indirect
|
||||
golang.org/x/mod v0.6.0 // indirect
|
||||
golang.org/x/text v0.4.0 // indirect
|
||||
golang.org/x/text v0.6.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
|
||||
|
||||
@@ -1 +1 @@
|
||||
sha256-+5icFKDHXt3JMbUjLQGes4R+GeUi48xRgGd0yPKVrw0=
|
||||
sha256-zBfANuVhYtDOOiZu6SPVmM0cTEsOHVdycOz5EZDapKk=
|
||||
|
||||
49
go.sum
49
go.sum
@@ -90,8 +90,8 @@ github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAE
|
||||
github.com/OpenPeeDeeP/depguard v1.0.1 h1:VlW4R6jmBIv3/u1JNlawEvJMM4J+dPORPaZasQee8Us=
|
||||
github.com/OpenPeeDeeP/depguard v1.0.1/go.mod h1:xsIw86fROiiwelg+jB2uM9PiKihMMmUx/1V+TNhjQvM=
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7/go.mod h1:z4/9nQmJSSwwds7ejkxaJwO37dru3geImFUdJlaLzQo=
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20211112122917-428f8eabeeb3 h1:XcF0cTDJeiuZ5NU8w7WUDge0HRwwNRmxj/GGk6KSA6g=
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20211112122917-428f8eabeeb3/go.mod h1:z4/9nQmJSSwwds7ejkxaJwO37dru3geImFUdJlaLzQo=
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20221026131551-cf6655e29de4 h1:ra2OtmuW0AE5csawV4YXMNGNQQXvLRps3z2Z59OPO+I=
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20221026131551-cf6655e29de4/go.mod h1:UBYPn8k0D56RtnR8RFQMjmh4KrZzWJ5o7Z9SYjossQ8=
|
||||
github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI=
|
||||
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
|
||||
@@ -137,8 +137,9 @@ github.com/ashanbrown/makezero v0.0.0-20210520155254-b6261585ddde/go.mod h1:oG9D
|
||||
github.com/aws/aws-sdk-go v1.23.20/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
|
||||
github.com/aws/aws-sdk-go v1.25.37/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
|
||||
github.com/aws/aws-sdk-go v1.36.30/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro=
|
||||
github.com/aws/aws-sdk-go-v2 v1.11.2 h1:SDiCYqxdIYi6HgQfAWRhgdZrdnOuGyLDJVRSWLeHWvs=
|
||||
github.com/aws/aws-sdk-go-v2 v1.11.2/go.mod h1:SQfA+m2ltnu1cA0soUkj4dRSsmITiVQUJvBIZjzfPyQ=
|
||||
github.com/aws/aws-sdk-go-v2 v1.17.3 h1:shN7NlnVzvDUgPQ+1rLMSxY8OWRNDRYtiqe0p/PgrhY=
|
||||
github.com/aws/aws-sdk-go-v2 v1.17.3/go.mod h1:uzbQtefpm44goOPmdKyAlXSNcwlRgF3ePWVW6EtJvvw=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.0.0 h1:yVUAwvJC/0WNPbyl0nA3j1L6CW1CN8wBubCRqtG7JLI=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.0.0/go.mod h1:Xn6sxgRuIDflLRJFj5Ev7UxABIkNbccFPV/p8itDReM=
|
||||
github.com/aws/aws-sdk-go-v2/config v1.11.0 h1:Czlld5zBB61A3/aoegA9/buZulwL9mHHfizh/Oq+Kqs=
|
||||
@@ -149,10 +150,12 @@ github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.8.2 h1:KiN5TPOLrEjbGCvdTQR4t0U4
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.8.2/go.mod h1:dF2F6tXEOgmW5X1ZFO/EPtWrcm7XkW07KNcJUGNtt4s=
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.7.4 h1:P8dY1eHwdKQtMLTSn4Lg0A+vEHTqBnTkYxgy5kzK4Y0=
|
||||
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.7.4/go.mod h1:FqSlw++zBunV8Kt5rPETKxIPGO8axbW4L8v25oql7ok=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.2 h1:XJLnluKuUxQG255zPNe+04izXl7GSyUVafIsgfv9aw4=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.2/go.mod h1:SgKKNBIoDC/E1ZCDhhMW3yalWjwuLjMcpLzsM/QQnWo=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.0.2 h1:EauRoYZVNPlidZSZJDscjJBQ22JhVF2+tdteatax2Ak=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.27 h1:I3cakv2Uy1vNmmhRQmFptYDxOvBnwCdNwyw63N0RaRU=
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.27/go.mod h1:a1/UpzeyBBerajpnP5nGZa9mGzsBn5cOKxm6NWQsvoI=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.0.2/go.mod h1:xT4XX6w5Sa3dhg50JrYyy3e4WPYo/+WjY/BXtqXVunU=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.21 h1:5NbbMrIzmUn/TXFqAle6mgrH5m9cOvMLRGL7pnG8tRE=
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.21/go.mod h1:+Gxn8jYn5k9ebfHEqlhrMirFjSW0v0C9fI+KN5vk2kE=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.2 h1:IQup8Q6lorXeiA/rK72PeToWoWK8h7VAPgHNWdSrtgE=
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.3.2/go.mod h1:VITe/MdW6EMXPb0o0txu/fsonXbMHUU2OC2Qp7ivU4o=
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.5.0 h1:lPLbw4Gn59uoKqvOfSnkJr54XWk5Ak1NK20ZEiSWb3U=
|
||||
@@ -163,14 +166,15 @@ github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.9.2 h1:GnPGH1FGc4fkn0J
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.9.2/go.mod h1:eDUYjOYt4Uio7xfHi5jOsO393ZG8TSfZB92a3ZNadWM=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.21.0 h1:vUM2P60BI755i35Gyik4s/lXKcnpEbnvw2Vud+soqpI=
|
||||
github.com/aws/aws-sdk-go-v2/service/s3 v1.21.0/go.mod h1:lQ5AeEW2XWzu8hwQ3dCqZFWORQ3RntO0Kq135Xd9VCo=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssm v1.17.1 h1:E/2WewR1wegBnthK8Yz+E87E8Mm4RJC/7R6vg6oAfl0=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssm v1.17.1/go.mod h1:jqRk4h1lv2pV4G1DTYRj71JIMEoU/gEGvLU5O6ZnpLM=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssm v1.35.0 h1:QWCcOeLTrjvf7UdYIadzrhNH3PI6T9jXOV64Ez5YUgg=
|
||||
github.com/aws/aws-sdk-go-v2/service/ssm v1.35.0/go.mod h1:Hf7wSogKP1XCJ9GgW8erZDL6IZ1NLwLN7bYdV/Gn/LI=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.6.2 h1:2IDmvSb86KT44lSg1uU4ONpzgWLOuApRl6Tg54mZ6Dk=
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.6.2/go.mod h1:KnIpszaIdwI33tmc/W/GGXyn22c1USYxA/2KyvoeDY0=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.11.1 h1:QKR7wy5e650q70PFKMfGF9sTo0rZgUevSSJ4wxmyWXk=
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.11.1/go.mod h1:UV2N5HaPfdbDpkgkz4sRzWCvQswZjdO1FfqCWl0t7RA=
|
||||
github.com/aws/smithy-go v1.9.0 h1:c7FUdEqrQA1/UVKKCNDFQPNKGp4FQg3YW4Ck5SLTG58=
|
||||
github.com/aws/smithy-go v1.9.0/go.mod h1:SObp3lf9smib00L/v3U2eAKG8FyQ7iLrJnQiAmR5n+E=
|
||||
github.com/aws/smithy-go v1.13.5 h1:hgz0X/DX0dGqTYpGALqXJoRKRj5oQ7150i5FdTePzO8=
|
||||
github.com/aws/smithy-go v1.13.5/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA=
|
||||
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
|
||||
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
@@ -195,6 +199,7 @@ github.com/breml/bidichk v0.2.1 h1:SRNtZuLdfkxtocj+xyHXKC1Uv3jVi6EPYx+NHSTNQvE=
|
||||
github.com/breml/bidichk v0.2.1/go.mod h1:zbfeitpevDUGI7V91Uzzuwrn4Vls8MoBMrwtt78jmso=
|
||||
github.com/butuzov/ireturn v0.1.1 h1:QvrO2QF2+/Cx1WA/vETCIYBKtRjc30vesdoPUNo1EbY=
|
||||
github.com/butuzov/ireturn v0.1.1/go.mod h1:Wh6Zl3IMtTpaIKbmwzqi6olnM9ptYQxxVacMsOEFPoc=
|
||||
github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
|
||||
github.com/cavaliercoder/go-cpio v0.0.0-20180626203310-925f9528c45e h1:hHg27A0RSSp2Om9lubZpiMgVbvn39bsUmW9U5h0twqc=
|
||||
github.com/cavaliercoder/go-cpio v0.0.0-20180626203310-925f9528c45e/go.mod h1:oDpT4efm8tSYHXV5tHSdRvBet/b/QzxZ+XyyPehvm3A=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
@@ -212,6 +217,8 @@ github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMn
|
||||
github.com/cilium/ebpf v0.8.1 h1:bLSSEbBLqGPXxls55pGr5qWZaTqcmfDJHhou7t254ao=
|
||||
github.com/cilium/ebpf v0.8.1/go.mod h1:f5zLIM0FSNuAkSyLAN7X+Hy6yznlF1mNiWUMfxMtrgk=
|
||||
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=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||
@@ -1194,8 +1201,9 @@ github.com/tommy-muehle/go-mnd/v2 v2.4.0 h1:1t0f8Uiaq+fqKteUR4N9Umr6E99R+lDnLnq7
|
||||
github.com/tommy-muehle/go-mnd/v2 v2.4.0/go.mod h1:WsUAkMJMYww6l/ufffCD3m+P7LEvr8TnZn9lwVDlgzw=
|
||||
github.com/toqueteos/webbrowser v1.2.0 h1:tVP/gpK69Fx+qMJKsLE7TD8LuGWPnEV71wBN9rrstGQ=
|
||||
github.com/toqueteos/webbrowser v1.2.0/go.mod h1:XWoZq4cyp9WeUeak7w7LXRUQf1F1ATJMir8RTqb4ayM=
|
||||
github.com/u-root/u-root v0.9.1-0.20221111022710-6e9699743f5d h1:sT5Q2xFrqgm/3yrCkVLkVuEFpG07UXz9ALqxxN1SmZc=
|
||||
github.com/u-root/u-root v0.9.1-0.20221111022710-6e9699743f5d/go.mod h1:jMbuI3nENTNzHW9mYwQ57b8/DSuJTq+joYY18x/WGxE=
|
||||
github.com/u-root/gobusybox/src v0.0.0-20221229083637-46b2883a7f90 h1:zTk5683I9K62wtZ6eUa6vu6IWwVHXPnoKK5n2unAwv0=
|
||||
github.com/u-root/u-root v0.9.1-0.20230109201855-948a78c969ad h1:0lEUXaz1vhlAtoMpu18vhb16s5rGRpNCl2trxc2/Qbg=
|
||||
github.com/u-root/u-root v0.9.1-0.20230109201855-948a78c969ad/go.mod h1:DBkDtiZyONk9hzVEdB/PWI9B4TxDkElWlVTHseglrZY=
|
||||
github.com/u-root/uio v0.0.0-20221213070652-c3537552635f h1:dpx1PHxYqAnXzbryJrWP1NQLzEjwcVgFLhkknuFQ7ww=
|
||||
github.com/u-root/uio v0.0.0-20221213070652-c3537552635f/go.mod h1:IogEAUBXDEwX7oR/BMmCctShYs80ql4hF0ySdzGxf7E=
|
||||
github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo=
|
||||
@@ -1433,8 +1441,8 @@ 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.2.0 h1:sZfSu1wtKLGlWI4ZZayP0ck9Y73K1ynO6gqzTdBVdPU=
|
||||
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
|
||||
golang.org/x/net v0.5.0 h1:GyT4nK/YDHSqa1c4753ouYCDajOYKTja9Xb/OHtgvSw=
|
||||
golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
@@ -1564,6 +1572,7 @@ golang.org/x/sys v0.0.0-20210915083310-ed5796bab164/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20210917161153-d61c044b1678/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211002104244-808efd93c36d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211013075003-97ac67df715c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211102192858-4dd72447c267/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
@@ -1574,13 +1583,13 @@ golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/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.3.1-0.20221220025402-2204b6615fb8 h1:/VqMvhQCyzfuc826eNrpWmMb3AwD2Sxz/HMsYIhwcIs=
|
||||
golang.org/x/sys v0.3.1-0.20221220025402-2204b6615fb8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18=
|
||||
golang.org/x/sys v0.4.0/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=
|
||||
golang.org/x/term v0.2.0 h1:z85xZCsEl7bi/KwbNADeBYoOP0++7W1ipu+aGnpwzRM=
|
||||
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
|
||||
golang.org/x/term v0.4.0 h1:O7UWfv5+A2qiuulQk30kVinPoMtoIPeVaKLEgLpVkvg=
|
||||
golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
@@ -1590,8 +1599,8 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg=
|
||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.6.0 h1:3XmdazWV+ubf7QgHSTWeykHOci5oeekaGJBLkrkaw4k=
|
||||
golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
@@ -1716,8 +1725,8 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
|
||||
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=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.zx2c4.com/wintun v0.0.0-20211104114900-415007cec224 h1:Ug9qvr1myri/zFN6xL17LSCBGFDnphBBhzmILHsM5TY=
|
||||
golang.zx2c4.com/wintun v0.0.0-20211104114900-415007cec224/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
|
||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
|
||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
|
||||
golang.zx2c4.com/wireguard/windows v0.5.3 h1:On6j2Rpn3OEMXqBq00QEDC7bWSZrPIHKIus8eIuExIE=
|
||||
golang.zx2c4.com/wireguard/windows v0.5.3/go.mod h1:9TEe8TJmtwyQebdFwAkEWOPr3prrtqm+REGFifP60hI=
|
||||
gomodules.xyz/jsonpatch/v2 v2.2.0 h1:4pT439QV83L+G9FkcCriY6EkpcK6r6bK+A5FBUMI7qY=
|
||||
|
||||
@@ -70,6 +70,9 @@ const (
|
||||
|
||||
// SysDNSManager is the name of the net/dns manager subsystem.
|
||||
SysDNSManager = Subsystem("dns-manager")
|
||||
|
||||
// SysTKA is the name of the tailnet key authority subsystem.
|
||||
SysTKA = Subsystem("tailnet-lock")
|
||||
)
|
||||
|
||||
// NewWarnable returns a new warnable item that the caller can mark
|
||||
@@ -194,6 +197,12 @@ func SetDNSManagerHealth(err error) { setErr(SysDNSManager, err) }
|
||||
// DNSOSHealth returns the net/dns.OSConfigurator error state.
|
||||
func DNSOSHealth() error { return get(SysDNSOS) }
|
||||
|
||||
// SetTKAHealth sets the health of the tailnet key authority.
|
||||
func SetTKAHealth(err error) { setErr(SysTKA, err) }
|
||||
|
||||
// TKAHealth returns the tailnet key authority error state.
|
||||
func TKAHealth() error { return get(SysTKA) }
|
||||
|
||||
// SetLocalLogConfigHealth sets the error state of this client's local log configuration.
|
||||
func SetLocalLogConfigHealth(err error) {
|
||||
mu.Lock()
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"io"
|
||||
"os"
|
||||
"runtime"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
@@ -46,10 +47,13 @@ func New() *tailcfg.Hostinfo {
|
||||
Desktop: desktop(),
|
||||
Package: packageTypeCached(),
|
||||
GoArch: runtime.GOARCH,
|
||||
GoArchVar: lazyGoArchVar.Get(),
|
||||
GoVersion: runtime.Version(),
|
||||
Machine: condCall(unameMachine),
|
||||
DeviceModel: deviceModel(),
|
||||
Cloud: string(cloudenv.Get()),
|
||||
NoLogsNoSupport: envknob.NoLogsNoSupport(),
|
||||
AllowsUpdate: envknob.AllowsRemoteUpdate(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,6 +64,7 @@ var (
|
||||
distroName func() string
|
||||
distroVersion func() string
|
||||
distroCodeName func() string
|
||||
unameMachine func() string
|
||||
)
|
||||
|
||||
func condCall[T any](fn func() T) T {
|
||||
@@ -72,6 +77,7 @@ func condCall[T any](fn func() T) T {
|
||||
|
||||
var (
|
||||
lazyInContainer = &lazyAtomicValue[opt.Bool]{f: ptr.To(inContainer)}
|
||||
lazyGoArchVar = &lazyAtomicValue[string]{f: ptr.To(goArchVar)}
|
||||
)
|
||||
|
||||
type lazyAtomicValue[T any] struct {
|
||||
@@ -133,6 +139,7 @@ const (
|
||||
FlyDotIo = EnvType("fly")
|
||||
Kubernetes = EnvType("k8s")
|
||||
DockerDesktop = EnvType("dde")
|
||||
Replit = EnvType("repl")
|
||||
)
|
||||
|
||||
var envType atomic.Value // of EnvType
|
||||
@@ -220,6 +227,9 @@ func getEnvType() EnvType {
|
||||
if inDockerDesktop() {
|
||||
return DockerDesktop
|
||||
}
|
||||
if inReplit() {
|
||||
return Replit
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -307,6 +317,14 @@ func inFlyDotIo() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func inReplit() bool {
|
||||
// https://docs.replit.com/programming-ide/getting-repl-metadata
|
||||
if os.Getenv("REPL_OWNER") != "" && os.Getenv("REPL_SLUG") != "" {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func inKubernetes() bool {
|
||||
if os.Getenv("KUBERNETES_SERVICE_HOST") != "" && os.Getenv("KUBERNETES_SERVICE_PORT") != "" {
|
||||
return true
|
||||
@@ -321,6 +339,27 @@ func inDockerDesktop() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// goArchVar returns the GOARM or GOAMD64 etc value that the binary was built
|
||||
// with.
|
||||
func goArchVar() string {
|
||||
bi, ok := debug.ReadBuildInfo()
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
// Look for GOARM, GOAMD64, GO386, etc. Note that the little-endian
|
||||
// "le"-suffixed GOARCH values don't have their own environment variable.
|
||||
//
|
||||
// See https://pkg.go.dev/cmd/go#hdr-Environment_variables and the
|
||||
// "Architecture-specific environment variables" section:
|
||||
wantKey := "GO" + strings.ToUpper(strings.TrimSuffix(runtime.GOARCH, "le"))
|
||||
for _, s := range bi.Settings {
|
||||
if s.Key == wantKey {
|
||||
return s.Value
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type etcAptSrcResult struct {
|
||||
mod time.Time
|
||||
disabled bool
|
||||
|
||||
39
hostinfo/hostinfo_uname.go
Normal file
39
hostinfo/hostinfo_uname.go
Normal file
@@ -0,0 +1,39 @@
|
||||
// Copyright (c) 2023 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build linux || freebsd || openbsd || darwin
|
||||
|
||||
package hostinfo
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
"tailscale.com/types/ptr"
|
||||
)
|
||||
|
||||
func init() {
|
||||
unameMachine = lazyUnameMachine.Get
|
||||
}
|
||||
|
||||
var lazyUnameMachine = &lazyAtomicValue[string]{f: ptr.To(unameMachineUnix)}
|
||||
|
||||
func unameMachineUnix() string {
|
||||
switch runtime.GOOS {
|
||||
case "android":
|
||||
// Don't call on Android for now. We're late in the 1.36 release cycle
|
||||
// and don't want to test syscall filters on various Android versions to
|
||||
// see what's permitted. Notably, the hostinfo_linux.go file has build
|
||||
// tag !android, so maybe Uname is verboten.
|
||||
return ""
|
||||
case "ios":
|
||||
// For similar reasons, don't call on iOS. There aren't many iOS devices
|
||||
// and we know their CPU properties so calling this is only risk and no
|
||||
// reward.
|
||||
return ""
|
||||
}
|
||||
var un unix.Utsname
|
||||
unix.Uname(&un)
|
||||
return unix.ByteSliceToString(un.Machine[:])
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
"golang.org/x/sys/windows/registry"
|
||||
@@ -69,6 +70,10 @@ func packageTypeWindows() string {
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
home, _ := os.UserHomeDir()
|
||||
if strings.HasPrefix(exe, filepath.Join(home, "scoop", "apps", "tailscale")) {
|
||||
return "scoop"
|
||||
}
|
||||
dir := filepath.Dir(exe)
|
||||
nsisUninstaller := filepath.Join(dir, "Uninstall-Tailscale.exe")
|
||||
_, err = os.Stat(nsisUninstaller)
|
||||
|
||||
@@ -65,6 +65,8 @@ const (
|
||||
NotifyInitialState // if set, the first Notify message (sent immediately) will contain the current State + BrowseToURL
|
||||
NotifyInitialPrefs // if set, the first Notify message (sent immediately) will contain the current Prefs
|
||||
NotifyInitialNetMap // if set, the first Notify message (sent immediately) will contain the current NetMap
|
||||
|
||||
NotifyNoPrivateKeys // if set, private keys that would normally be sent in updates are zeroed out
|
||||
)
|
||||
|
||||
// Notify is a communication from a backend (e.g. tailscaled) to a frontend
|
||||
|
||||
@@ -6,14 +6,23 @@ package ipnlocal
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/util/clientmetric"
|
||||
"tailscale.com/util/goroutines"
|
||||
"tailscale.com/version"
|
||||
"tailscale.com/version/distro"
|
||||
)
|
||||
|
||||
func (b *LocalBackend) handleC2N(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -26,6 +35,18 @@ func (b *LocalBackend) handleC2N(w http.ResponseWriter, r *http.Request) {
|
||||
// Test handler.
|
||||
body, _ := io.ReadAll(r.Body)
|
||||
w.Write(body)
|
||||
case "/update":
|
||||
b.handleC2NUpdate(w, r)
|
||||
case "/logtail/flush":
|
||||
if r.Method != "POST" {
|
||||
http.Error(w, "bad method", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
if b.TryFlushLogs() {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
} else {
|
||||
http.Error(w, "no log flusher wired up", http.StatusInternalServerError)
|
||||
}
|
||||
case "/debug/goroutines":
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
w.Write(goroutines.ScrubbedGoroutineDump())
|
||||
@@ -67,3 +88,108 @@ func (b *LocalBackend) handleC2N(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "unknown c2n path", http.StatusBadRequest)
|
||||
}
|
||||
}
|
||||
|
||||
func (b *LocalBackend) handleC2NUpdate(w http.ResponseWriter, r *http.Request) {
|
||||
// TODO(bradfitz): add some sort of semaphore that prevents two concurrent
|
||||
// updates, or if one happened in the past 5 minutes, or something.
|
||||
|
||||
// TODO(bradfitz): move this type to some leaf package
|
||||
type updateResponse struct {
|
||||
Err string // error message, if any
|
||||
Enabled bool // user has opted-in to remote updates
|
||||
Supported bool // Tailscale supports updating this OS/platform
|
||||
Started bool
|
||||
}
|
||||
var res updateResponse
|
||||
res.Enabled = envknob.AllowsRemoteUpdate()
|
||||
res.Supported = runtime.GOOS == "windows" || (runtime.GOOS == "linux" && distro.Get() == distro.Debian)
|
||||
|
||||
switch r.Method {
|
||||
case "GET", "POST":
|
||||
default:
|
||||
http.Error(w, "bad method", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
defer func() {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(res)
|
||||
}()
|
||||
|
||||
if r.Method == "GET" {
|
||||
return
|
||||
}
|
||||
|
||||
if !res.Enabled {
|
||||
res.Err = "not enabled"
|
||||
return
|
||||
}
|
||||
|
||||
if !res.Supported {
|
||||
res.Err = "not supported"
|
||||
return
|
||||
}
|
||||
cmdTS, err := findCmdTailscale()
|
||||
if err != nil {
|
||||
res.Err = fmt.Sprintf("failed to find cmd/tailscale binary: %v", err)
|
||||
return
|
||||
}
|
||||
var ver struct {
|
||||
Long string `json:"long"`
|
||||
}
|
||||
out, err := exec.Command(cmdTS, "version", "--json").Output()
|
||||
if err != nil {
|
||||
res.Err = fmt.Sprintf("failed to find cmd/tailscale binary: %v", err)
|
||||
return
|
||||
}
|
||||
if err := json.Unmarshal(out, &ver); err != nil {
|
||||
res.Err = "invalid JSON from cmd/tailscale version --json"
|
||||
return
|
||||
}
|
||||
if ver.Long != version.Long {
|
||||
res.Err = "cmd/tailscale version mismatch"
|
||||
return
|
||||
}
|
||||
cmd := exec.Command(cmdTS, "update", "--yes")
|
||||
if err := cmd.Start(); err != nil {
|
||||
res.Err = fmt.Sprintf("failed to start cmd/tailscale update: %v", err)
|
||||
return
|
||||
}
|
||||
res.Started = true
|
||||
|
||||
// TODO(bradfitz,andrew): There might be a race condition here on Windows:
|
||||
// * We start the update process.
|
||||
// * tailscale.exe copies itself and kicks off the update process
|
||||
// * msiexec stops this process during the update before the selfCopy exits(?)
|
||||
// * This doesn't return because the process is dead.
|
||||
//
|
||||
// This seems fairly unlikely, but worth checking.
|
||||
defer cmd.Wait()
|
||||
return
|
||||
}
|
||||
|
||||
// findCmdTailscale looks for the cmd/tailscale that corresponds to the
|
||||
// currently running cmd/tailscaled. It's up to the caller to verify that the
|
||||
// two match, but this function does its best to find the right one. Notably, it
|
||||
// doesn't use $PATH for security reasons.
|
||||
func findCmdTailscale() (string, error) {
|
||||
self, err := os.Executable()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
switch runtime.GOOS {
|
||||
case "linux":
|
||||
if self == "/usr/sbin/tailscaled" {
|
||||
return "/usr/bin/tailscale", nil
|
||||
}
|
||||
return "", errors.New("tailscale not found in expected place")
|
||||
case "windows":
|
||||
dir := filepath.Dir(self)
|
||||
ts := filepath.Join(dir, "tailscale.exe")
|
||||
if fi, err := os.Stat(ts); err == nil && fi.Mode().IsRegular() {
|
||||
return ts, nil
|
||||
}
|
||||
return "", errors.New("tailscale.exe not found in expected place")
|
||||
}
|
||||
return "", fmt.Errorf("unsupported OS %v", runtime.GOOS)
|
||||
}
|
||||
|
||||
215
ipn/ipnlocal/expiry.go
Normal file
215
ipn/ipnlocal/expiry.go
Normal file
@@ -0,0 +1,215 @@
|
||||
// Copyright (c) 2023 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ipnlocal
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"tailscale.com/syncs"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/netmap"
|
||||
)
|
||||
|
||||
// For extra defense-in-depth, when we're testing expired nodes we check
|
||||
// ControlTime against this 'epoch' (set to the approximate time that this code
|
||||
// was written) such that if control (or Headscale, etc.) sends a ControlTime
|
||||
// that's sufficiently far in the past, we can safely ignore it.
|
||||
var flagExpiredPeersEpoch = time.Unix(1673373066, 0)
|
||||
|
||||
// If the offset between the current time and the time received from control is
|
||||
// larger than this, we store an offset in our expiryManager to adjust future
|
||||
// clock timings.
|
||||
const minClockDelta = 1 * time.Minute
|
||||
|
||||
// expiryManager tracks the state of expired nodes and the delta from the
|
||||
// current clock time to the time returned from control, and allows mutating a
|
||||
// netmap to mark peers as expired based on the current delta-adjusted time.
|
||||
type expiryManager struct {
|
||||
// previouslyExpired stores nodes that have already expired so we can
|
||||
// only log on state transitions.
|
||||
previouslyExpired map[tailcfg.StableNodeID]bool
|
||||
|
||||
// clockDelta stores the delta between the current time and the time
|
||||
// received from control such that:
|
||||
// time.Now().Add(clockDelta) == MapResponse.ControlTime
|
||||
clockDelta syncs.AtomicValue[time.Duration]
|
||||
|
||||
logf logger.Logf
|
||||
timeNow func() time.Time
|
||||
}
|
||||
|
||||
func newExpiryManager(logf logger.Logf) *expiryManager {
|
||||
return &expiryManager{
|
||||
previouslyExpired: map[tailcfg.StableNodeID]bool{},
|
||||
logf: logf,
|
||||
timeNow: time.Now,
|
||||
}
|
||||
}
|
||||
|
||||
// onControlTime is called whenever we receive a new timestamp from the control
|
||||
// server to store the delta.
|
||||
func (em *expiryManager) onControlTime(t time.Time) {
|
||||
localNow := em.timeNow()
|
||||
delta := t.Sub(localNow)
|
||||
if delta.Abs() > minClockDelta {
|
||||
em.logf("[v1] netmap: flagExpiredPeers: setting clock delta to %v", delta)
|
||||
em.clockDelta.Store(delta)
|
||||
} else {
|
||||
em.clockDelta.Store(0)
|
||||
}
|
||||
}
|
||||
|
||||
// flagExpiredPeers updates mapRes.Peers, mutating all peers that have expired,
|
||||
// taking into account any clock skew detected by using the ControlTime field
|
||||
// in the MapResponse. We don't actually remove expired peers from the Peers
|
||||
// array; instead, we clear some fields of the Node object, and set
|
||||
// Node.Expired so other parts of the codebase can provide more clear error
|
||||
// messages when attempting to e.g. ping an expired node.
|
||||
//
|
||||
// The localNow time should be the output of time.Now for the local system; it
|
||||
// will be adjusted by any stored clock skew from ControlTime.
|
||||
//
|
||||
// This is additionally a defense-in-depth against something going wrong with
|
||||
// control such that we start seeing expired peers with a valid Endpoints or
|
||||
// DERP field.
|
||||
//
|
||||
// This function is safe to call concurrently with onControlTime but not
|
||||
// concurrently with any other call to flagExpiredPeers.
|
||||
func (em *expiryManager) flagExpiredPeers(netmap *netmap.NetworkMap, localNow time.Time) {
|
||||
// Adjust our current time by any saved delta to adjust for clock skew.
|
||||
controlNow := localNow.Add(em.clockDelta.Load())
|
||||
if controlNow.Before(flagExpiredPeersEpoch) {
|
||||
em.logf("netmap: flagExpiredPeers: [unexpected] delta-adjusted current time is before hardcoded epoch; skipping")
|
||||
return
|
||||
}
|
||||
|
||||
for _, peer := range netmap.Peers {
|
||||
// Nodes that don't expire have KeyExpiry set to the zero time;
|
||||
// skip those and peers that are already marked as expired
|
||||
// (e.g. from control).
|
||||
if peer.KeyExpiry.IsZero() || peer.KeyExpiry.After(controlNow) {
|
||||
delete(em.previouslyExpired, peer.StableID)
|
||||
continue
|
||||
} else if peer.Expired {
|
||||
continue
|
||||
}
|
||||
|
||||
if !em.previouslyExpired[peer.StableID] {
|
||||
em.logf("[v1] netmap: flagExpiredPeers: clearing expired peer %v", peer.StableID)
|
||||
em.previouslyExpired[peer.StableID] = true
|
||||
}
|
||||
|
||||
// Actually mark the node as expired
|
||||
peer.Expired = true
|
||||
|
||||
// Control clears the Endpoints and DERP fields of expired
|
||||
// nodes; do so here as well. The Expired bool is the correct
|
||||
// thing to set, but this replicates the previous behaviour.
|
||||
//
|
||||
// NOTE: this is insufficient to actually break connectivity,
|
||||
// since we discover endpoints via DERP, and due to DERP return
|
||||
// path optimization.
|
||||
peer.Endpoints = nil
|
||||
peer.DERP = ""
|
||||
|
||||
// Defense-in-depth: break the node's public key as well, in
|
||||
// case something tries to communicate.
|
||||
peer.Key = key.NodePublicWithBadOldPrefix(peer.Key)
|
||||
}
|
||||
}
|
||||
|
||||
// nextPeerExpiry returns the time that the next node in the netmap expires
|
||||
// (including the self node), based on their KeyExpiry. It skips nodes that are
|
||||
// already marked as Expired. If there are no nodes expiring in the future,
|
||||
// then the zero Time will be returned.
|
||||
//
|
||||
// The localNow time should be the output of time.Now for the local system; it
|
||||
// will be adjusted by any stored clock skew from ControlTime.
|
||||
//
|
||||
// This function is safe to call concurrently with other methods of this expiryManager.
|
||||
func (em *expiryManager) nextPeerExpiry(nm *netmap.NetworkMap, localNow time.Time) time.Time {
|
||||
if nm == nil {
|
||||
return time.Time{}
|
||||
}
|
||||
|
||||
controlNow := localNow.Add(em.clockDelta.Load())
|
||||
if controlNow.Before(flagExpiredPeersEpoch) {
|
||||
em.logf("netmap: nextPeerExpiry: [unexpected] delta-adjusted current time is before hardcoded epoch; skipping")
|
||||
return time.Time{}
|
||||
}
|
||||
|
||||
var nextExpiry time.Time // zero if none
|
||||
for _, peer := range nm.Peers {
|
||||
if peer.KeyExpiry.IsZero() {
|
||||
continue // tagged node
|
||||
} else if peer.Expired {
|
||||
// Peer already expired; Expired is set by the
|
||||
// flagExpiredPeers function, above.
|
||||
continue
|
||||
} else if peer.KeyExpiry.Before(controlNow) {
|
||||
// This peer already expired, and peer.Expired
|
||||
// isn't set for some reason. Skip this node.
|
||||
continue
|
||||
}
|
||||
|
||||
// nextExpiry being zero is a sentinel that we haven't yet set
|
||||
// an expiry; otherwise, only update if this node's expiry is
|
||||
// sooner than the currently-stored one (since we want the
|
||||
// soonest-occuring expiry time).
|
||||
if nextExpiry.IsZero() || peer.KeyExpiry.Before(nextExpiry) {
|
||||
nextExpiry = peer.KeyExpiry
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure that we also fire this timer if our own node key expires.
|
||||
if nm.SelfNode != nil {
|
||||
selfExpiry := nm.SelfNode.KeyExpiry
|
||||
|
||||
if selfExpiry.IsZero() {
|
||||
// No expiry for self node
|
||||
} else if selfExpiry.Before(controlNow) {
|
||||
// Self node already expired; we don't want to return a
|
||||
// time in the past, so skip this.
|
||||
} else if nextExpiry.IsZero() || selfExpiry.Before(nextExpiry) {
|
||||
// Self node expires after now, but before the soonest
|
||||
// peer in the netmap; update our next expiry to this
|
||||
// time.
|
||||
nextExpiry = selfExpiry
|
||||
}
|
||||
}
|
||||
|
||||
// As an additional defense in depth, never return a time that is
|
||||
// before the current time from the perspective of the local system
|
||||
// (since timers with a zero or negative duration will fire
|
||||
// immediately and can cause unnecessary reconfigurations).
|
||||
//
|
||||
// This can happen if the local clock is running fast; for example:
|
||||
// localTime = 2pm
|
||||
// controlTime = 1pm (real time)
|
||||
// nextExpiry = 1:30pm (real time)
|
||||
//
|
||||
// In the above case, we'd return a nextExpiry of 1:30pm while the
|
||||
// current clock reads 2pm; in this case, setting a timer for
|
||||
// nextExpiry.Sub(now) would result in a negative duration and a timer
|
||||
// that fired immediately.
|
||||
//
|
||||
// In this particular edge-case, return an expiry time 30 seconds after
|
||||
// the local time so that any timers created based on this expiry won't
|
||||
// fire too quickly.
|
||||
//
|
||||
// The alternative would be to do all comparisons in local time,
|
||||
// unadjusted for clock skew, but that doesn't handle cases where the
|
||||
// local clock is "fixed" between netmap updates.
|
||||
if !nextExpiry.IsZero() && nextExpiry.Before(localNow) {
|
||||
em.logf("netmap: nextPeerExpiry: skipping nextExpiry %q before local time %q due to clock skew",
|
||||
nextExpiry.UTC().Format(time.RFC3339),
|
||||
localNow.UTC().Format(time.RFC3339))
|
||||
return localNow.Add(30 * time.Second)
|
||||
}
|
||||
|
||||
return nextExpiry
|
||||
}
|
||||
302
ipn/ipnlocal/expiry_test.go
Normal file
302
ipn/ipnlocal/expiry_test.go
Normal file
@@ -0,0 +1,302 @@
|
||||
// Copyright (c) 2023 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ipnlocal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/netmap"
|
||||
)
|
||||
|
||||
func TestFlagExpiredPeers(t *testing.T) {
|
||||
n := func(id tailcfg.NodeID, name string, expiry time.Time, mod ...func(*tailcfg.Node)) *tailcfg.Node {
|
||||
n := &tailcfg.Node{ID: id, Name: name, KeyExpiry: expiry}
|
||||
for _, f := range mod {
|
||||
f(n)
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
now := time.Unix(1673373129, 0)
|
||||
|
||||
timeInPast := now.Add(-1 * time.Hour)
|
||||
timeInFuture := now.Add(1 * time.Hour)
|
||||
|
||||
timeBeforeEpoch := flagExpiredPeersEpoch.Add(-1 * time.Second)
|
||||
if now.Before(timeBeforeEpoch) {
|
||||
panic("current time in test cannot be before epoch")
|
||||
}
|
||||
|
||||
var expiredKey key.NodePublic
|
||||
if err := expiredKey.UnmarshalText([]byte("nodekey:6da774d5d7740000000000000000000000000000000000000000000000000000")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
controlTime *time.Time
|
||||
netmap *netmap.NetworkMap
|
||||
want []*tailcfg.Node
|
||||
}{
|
||||
{
|
||||
name: "no_expiry",
|
||||
controlTime: &now,
|
||||
netmap: &netmap.NetworkMap{
|
||||
Peers: []*tailcfg.Node{
|
||||
n(1, "foo", timeInFuture),
|
||||
n(2, "bar", timeInFuture),
|
||||
},
|
||||
},
|
||||
want: []*tailcfg.Node{
|
||||
n(1, "foo", timeInFuture),
|
||||
n(2, "bar", timeInFuture),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "expiry",
|
||||
controlTime: &now,
|
||||
netmap: &netmap.NetworkMap{
|
||||
Peers: []*tailcfg.Node{
|
||||
n(1, "foo", timeInFuture),
|
||||
n(2, "bar", timeInPast),
|
||||
},
|
||||
},
|
||||
want: []*tailcfg.Node{
|
||||
n(1, "foo", timeInFuture),
|
||||
n(2, "bar", timeInPast, func(n *tailcfg.Node) {
|
||||
n.Expired = true
|
||||
n.Key = expiredKey
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "bad_ControlTime",
|
||||
// controlTime here is intentionally before our hardcoded epoch
|
||||
controlTime: &timeBeforeEpoch,
|
||||
|
||||
netmap: &netmap.NetworkMap{
|
||||
Peers: []*tailcfg.Node{
|
||||
n(1, "foo", timeInFuture),
|
||||
n(2, "bar", timeBeforeEpoch.Add(-1*time.Hour)), // before ControlTime
|
||||
},
|
||||
},
|
||||
want: []*tailcfg.Node{
|
||||
n(1, "foo", timeInFuture),
|
||||
n(2, "bar", timeBeforeEpoch.Add(-1*time.Hour)), // should have expired, but ControlTime is before epoch
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "tagged_node",
|
||||
controlTime: &now,
|
||||
netmap: &netmap.NetworkMap{
|
||||
Peers: []*tailcfg.Node{
|
||||
n(1, "foo", timeInFuture),
|
||||
n(2, "bar", time.Time{}), // tagged node; zero expiry
|
||||
},
|
||||
},
|
||||
want: []*tailcfg.Node{
|
||||
n(1, "foo", timeInFuture),
|
||||
n(2, "bar", time.Time{}), // not expired
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
em := newExpiryManager(t.Logf)
|
||||
em.timeNow = func() time.Time { return now }
|
||||
|
||||
if tt.controlTime != nil {
|
||||
em.onControlTime(*tt.controlTime)
|
||||
}
|
||||
em.flagExpiredPeers(tt.netmap, now)
|
||||
if !reflect.DeepEqual(tt.netmap.Peers, tt.want) {
|
||||
t.Errorf("wrong results\n got: %s\nwant: %s", formatNodes(tt.netmap.Peers), formatNodes(tt.want))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNextPeerExpiry(t *testing.T) {
|
||||
n := func(id tailcfg.NodeID, name string, expiry time.Time, mod ...func(*tailcfg.Node)) *tailcfg.Node {
|
||||
n := &tailcfg.Node{ID: id, Name: name, KeyExpiry: expiry}
|
||||
for _, f := range mod {
|
||||
f(n)
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
now := time.Unix(1675725516, 0)
|
||||
|
||||
noExpiry := time.Time{}
|
||||
timeInPast := now.Add(-1 * time.Hour)
|
||||
timeInFuture := now.Add(1 * time.Hour)
|
||||
timeInMoreFuture := now.Add(2 * time.Hour)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
netmap *netmap.NetworkMap
|
||||
want time.Time
|
||||
}{
|
||||
{
|
||||
name: "no_expiry",
|
||||
netmap: &netmap.NetworkMap{
|
||||
Peers: []*tailcfg.Node{
|
||||
n(1, "foo", noExpiry),
|
||||
n(2, "bar", noExpiry),
|
||||
},
|
||||
SelfNode: n(3, "self", noExpiry),
|
||||
},
|
||||
want: noExpiry,
|
||||
},
|
||||
{
|
||||
name: "future_expiry_from_peer",
|
||||
netmap: &netmap.NetworkMap{
|
||||
Peers: []*tailcfg.Node{
|
||||
n(1, "foo", noExpiry),
|
||||
n(2, "bar", timeInFuture),
|
||||
},
|
||||
SelfNode: n(3, "self", noExpiry),
|
||||
},
|
||||
want: timeInFuture,
|
||||
},
|
||||
{
|
||||
name: "future_expiry_from_self",
|
||||
netmap: &netmap.NetworkMap{
|
||||
Peers: []*tailcfg.Node{
|
||||
n(1, "foo", noExpiry),
|
||||
n(2, "bar", noExpiry),
|
||||
},
|
||||
SelfNode: n(3, "self", timeInFuture),
|
||||
},
|
||||
want: timeInFuture,
|
||||
},
|
||||
{
|
||||
name: "future_expiry_from_multiple_peers",
|
||||
netmap: &netmap.NetworkMap{
|
||||
Peers: []*tailcfg.Node{
|
||||
n(1, "foo", timeInFuture),
|
||||
n(2, "bar", timeInMoreFuture),
|
||||
},
|
||||
SelfNode: n(3, "self", noExpiry),
|
||||
},
|
||||
want: timeInFuture,
|
||||
},
|
||||
{
|
||||
name: "future_expiry_from_peer_and_self",
|
||||
netmap: &netmap.NetworkMap{
|
||||
Peers: []*tailcfg.Node{
|
||||
n(1, "foo", timeInMoreFuture),
|
||||
},
|
||||
SelfNode: n(2, "self", timeInFuture),
|
||||
},
|
||||
want: timeInFuture,
|
||||
},
|
||||
{
|
||||
name: "only_self",
|
||||
netmap: &netmap.NetworkMap{
|
||||
Peers: []*tailcfg.Node{},
|
||||
SelfNode: n(1, "self", timeInFuture),
|
||||
},
|
||||
want: timeInFuture,
|
||||
},
|
||||
{
|
||||
name: "peer_already_expired",
|
||||
netmap: &netmap.NetworkMap{
|
||||
Peers: []*tailcfg.Node{
|
||||
n(1, "foo", timeInPast),
|
||||
},
|
||||
SelfNode: n(2, "self", timeInFuture),
|
||||
},
|
||||
want: timeInFuture,
|
||||
},
|
||||
{
|
||||
name: "self_already_expired",
|
||||
netmap: &netmap.NetworkMap{
|
||||
Peers: []*tailcfg.Node{
|
||||
n(1, "foo", timeInFuture),
|
||||
},
|
||||
SelfNode: n(2, "self", timeInPast),
|
||||
},
|
||||
want: timeInFuture,
|
||||
},
|
||||
{
|
||||
name: "all_nodes_already_expired",
|
||||
netmap: &netmap.NetworkMap{
|
||||
Peers: []*tailcfg.Node{
|
||||
n(1, "foo", timeInPast),
|
||||
},
|
||||
SelfNode: n(2, "self", timeInPast),
|
||||
},
|
||||
want: noExpiry,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
em := newExpiryManager(t.Logf)
|
||||
em.timeNow = func() time.Time { return now }
|
||||
got := em.nextPeerExpiry(tt.netmap, now)
|
||||
if got != tt.want {
|
||||
t.Errorf("got %q, want %q", got.Format(time.RFC3339), tt.want.Format(time.RFC3339))
|
||||
} else if !got.IsZero() && got.Before(now) {
|
||||
t.Errorf("unexpectedly got expiry %q before now %q", got.Format(time.RFC3339), now.Format(time.RFC3339))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("ClockSkew", func(t *testing.T) {
|
||||
t.Logf("local time: %q", now.Format(time.RFC3339))
|
||||
em := newExpiryManager(t.Logf)
|
||||
em.timeNow = func() time.Time { return now }
|
||||
|
||||
// The local clock is "running fast"; our clock skew is -2h
|
||||
em.clockDelta.Store(-2 * time.Hour)
|
||||
t.Logf("'real' time: %q", now.Add(-2*time.Hour).Format(time.RFC3339))
|
||||
|
||||
// If we don't adjust for the local time, this would return a
|
||||
// time in the past.
|
||||
nm := &netmap.NetworkMap{
|
||||
Peers: []*tailcfg.Node{
|
||||
n(1, "foo", timeInPast),
|
||||
},
|
||||
}
|
||||
got := em.nextPeerExpiry(nm, now)
|
||||
want := now.Add(30 * time.Second)
|
||||
if got != want {
|
||||
t.Errorf("got %q, want %q", got.Format(time.RFC3339), want.Format(time.RFC3339))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func formatNodes(nodes []*tailcfg.Node) string {
|
||||
var sb strings.Builder
|
||||
for i, n := range nodes {
|
||||
if i > 0 {
|
||||
sb.WriteString(", ")
|
||||
}
|
||||
fmt.Fprintf(&sb, "(%d, %q", n.ID, n.Name)
|
||||
|
||||
if n.Online != nil {
|
||||
fmt.Fprintf(&sb, ", online=%v", *n.Online)
|
||||
}
|
||||
if n.LastSeen != nil {
|
||||
fmt.Fprintf(&sb, ", lastSeen=%v", n.LastSeen.Unix())
|
||||
}
|
||||
if n.Key != (key.NodePublic{}) {
|
||||
fmt.Fprintf(&sb, ", key=%v", n.Key.String())
|
||||
}
|
||||
if n.Expired {
|
||||
fmt.Fprintf(&sb, ", expired=true")
|
||||
}
|
||||
sb.WriteString(")")
|
||||
}
|
||||
return sb.String()
|
||||
}
|
||||
@@ -5,6 +5,7 @@
|
||||
package ipnlocal
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
@@ -17,6 +18,7 @@ import (
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
@@ -43,8 +45,10 @@ import (
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/ipn/policy"
|
||||
"tailscale.com/net/dns"
|
||||
"tailscale.com/net/dnscache"
|
||||
"tailscale.com/net/dnsfallback"
|
||||
"tailscale.com/net/interfaces"
|
||||
"tailscale.com/net/netns"
|
||||
"tailscale.com/net/netutil"
|
||||
"tailscale.com/net/tsaddr"
|
||||
"tailscale.com/net/tsdial"
|
||||
@@ -139,7 +143,9 @@ type LocalBackend struct {
|
||||
portpollOnce sync.Once // guards starting readPoller
|
||||
gotPortPollRes chan struct{} // closed upon first readPoller result
|
||||
newDecompressor func() (controlclient.Decompressor, error)
|
||||
varRoot string // or empty if SetVarRoot never called
|
||||
varRoot string // or empty if SetVarRoot never called
|
||||
logFlushFunc func() // or nil if SetLogFlusher wasn't called
|
||||
em *expiryManager // non-nil
|
||||
sshAtomicBool atomic.Bool
|
||||
shutdownCalled bool // if Shutdown has been called
|
||||
|
||||
@@ -150,6 +156,7 @@ type LocalBackend struct {
|
||||
filterAtomic atomic.Pointer[filter.Filter]
|
||||
containsViaIPFuncAtomic syncs.AtomicValue[func(netip.Addr) bool]
|
||||
shouldInterceptTCPPortAtomic syncs.AtomicValue[func(uint16) bool]
|
||||
numClientStatusCalls atomic.Uint32
|
||||
|
||||
// The mutex protects the following elements.
|
||||
mu sync.Mutex
|
||||
@@ -169,6 +176,7 @@ type LocalBackend struct {
|
||||
hostinfo *tailcfg.Hostinfo
|
||||
// netMap is not mutated in-place once set.
|
||||
netMap *netmap.NetworkMap
|
||||
nmExpiryTimer *time.Timer // for updating netMap on node expiry; can be nil
|
||||
nodeByAddr map[netip.Addr]*tailcfg.Node
|
||||
activeLogin string // last logged LoginName from netMap
|
||||
engineStatus ipn.EngineStatus
|
||||
@@ -274,6 +282,7 @@ func NewLocalBackend(logf logger.Logf, logid string, store ipn.StateStore, state
|
||||
backendLogID: logid,
|
||||
state: ipn.NoState,
|
||||
portpoll: portpoll,
|
||||
em: newExpiryManager(logf),
|
||||
gotPortPollRes: make(chan struct{}),
|
||||
loginFlags: loginFlags,
|
||||
}
|
||||
@@ -578,6 +587,7 @@ func (b *LocalBackend) updateStatus(sb *ipnstate.StatusBuilder, extraLocked func
|
||||
defer b.mu.Unlock()
|
||||
sb.MutateStatus(func(s *ipnstate.Status) {
|
||||
s.Version = version.Long
|
||||
s.TUN = !wgengine.IsNetstack(b.e)
|
||||
s.BackendState = b.state.String()
|
||||
s.AuthURL = b.authURLSticky
|
||||
if err := health.OverallError(); err != nil {
|
||||
@@ -713,6 +723,14 @@ func peerStatusFromNode(ps *ipnstate.PeerStatus, n *tailcfg.Node) {
|
||||
v := views.IPPrefixSliceOf(n.PrimaryRoutes)
|
||||
ps.PrimaryRoutes = &v
|
||||
}
|
||||
|
||||
if n.Expired {
|
||||
ps.Expired = true
|
||||
}
|
||||
if t := n.KeyExpiry; !t.IsZero() {
|
||||
t = t.Round(time.Second)
|
||||
ps.KeyExpiry = &t
|
||||
}
|
||||
}
|
||||
|
||||
// WhoIs reports the node and user who owns the node with the given IP:port.
|
||||
@@ -799,7 +817,51 @@ func (b *LocalBackend) setClientStatus(st controlclient.Status) {
|
||||
return
|
||||
}
|
||||
|
||||
// Track the number of calls
|
||||
currCall := b.numClientStatusCalls.Add(1)
|
||||
|
||||
b.mu.Lock()
|
||||
|
||||
// Handle node expiry in the netmap
|
||||
if st.NetMap != nil {
|
||||
now := time.Now()
|
||||
b.em.flagExpiredPeers(st.NetMap, now)
|
||||
|
||||
// Always stop the existing netmap timer if we have a netmap;
|
||||
// it's possible that we have no nodes expiring, so we should
|
||||
// always cancel the timer and then possibly restart it below.
|
||||
if b.nmExpiryTimer != nil {
|
||||
// Ignore if we can't stop; the atomic check in the
|
||||
// AfterFunc (below) will skip running.
|
||||
b.nmExpiryTimer.Stop()
|
||||
|
||||
// Nil so we don't attempt to stop on the next netmap
|
||||
b.nmExpiryTimer = nil
|
||||
}
|
||||
|
||||
// Figure out when the next node in the netmap is expiring so we can
|
||||
// start a timer to reconfigure at that point.
|
||||
nextExpiry := b.em.nextPeerExpiry(st.NetMap, now)
|
||||
if !nextExpiry.IsZero() {
|
||||
tmrDuration := nextExpiry.Sub(now) + 10*time.Second
|
||||
b.nmExpiryTimer = time.AfterFunc(tmrDuration, func() {
|
||||
// Skip if the world has moved on past the
|
||||
// saved call (e.g. if we race stopping this
|
||||
// timer).
|
||||
if b.numClientStatusCalls.Load() != currCall {
|
||||
return
|
||||
}
|
||||
|
||||
b.logf("setClientStatus: netmap expiry timer triggered after %v", tmrDuration)
|
||||
|
||||
// Call ourselves with the current status again; the logic in
|
||||
// setClientStatus will take care of updating the expired field
|
||||
// of peers in the netmap.
|
||||
b.setClientStatus(st)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
wasBlocked := b.blocked
|
||||
keyExpiryExtended := false
|
||||
if st.NetMap != nil {
|
||||
@@ -1308,6 +1370,7 @@ func (b *LocalBackend) Start(opts ipn.Options) error {
|
||||
Pinger: b,
|
||||
PopBrowserURL: b.tellClientToBrowseToURL,
|
||||
OnClientVersion: b.onClientVersion,
|
||||
OnControlTime: b.em.onControlTime,
|
||||
Dialer: b.Dialer(),
|
||||
Status: b.setClientStatus,
|
||||
C2NHandler: http.HandlerFunc(b.handleC2N),
|
||||
@@ -1733,15 +1796,37 @@ func (b *LocalBackend) readPoller() {
|
||||
//
|
||||
// WatchNotifications blocks until ctx is done.
|
||||
//
|
||||
// The provided fn will only be called with non-nil pointers. The caller must
|
||||
// not modify roNotify. If fn returns false, the watch also stops.
|
||||
// The provided onWatchAdded, if non-nil, will be called once the watcher
|
||||
// is installed.
|
||||
//
|
||||
// The provided fn will be called for each notification. It will only be
|
||||
// called with non-nil pointers. The caller must not modify roNotify. If
|
||||
// fn returns false, the watch also stops.
|
||||
//
|
||||
// Failure to consume many notifications in a row will result in dropped
|
||||
// notifications. There is currently (2022-11-22) no mechanism provided to
|
||||
// detect when a message has been dropped.
|
||||
func (b *LocalBackend) WatchNotifications(ctx context.Context, mask ipn.NotifyWatchOpt, fn func(roNotify *ipn.Notify) (keepGoing bool)) {
|
||||
func (b *LocalBackend) WatchNotifications(ctx context.Context, mask ipn.NotifyWatchOpt, onWatchAdded func(), fn func(roNotify *ipn.Notify) (keepGoing bool)) {
|
||||
ch := make(chan *ipn.Notify, 128)
|
||||
|
||||
origFn := fn
|
||||
if mask&ipn.NotifyNoPrivateKeys != 0 {
|
||||
fn = func(n *ipn.Notify) bool {
|
||||
if n.NetMap == nil || n.NetMap.PrivateKey.IsZero() {
|
||||
return origFn(n)
|
||||
}
|
||||
|
||||
// The netmap in n is shared across all watchers, so to mutate it for a
|
||||
// single watcher we have to clone the notify and the netmap. We can
|
||||
// make shallow clones, at least.
|
||||
nm2 := *n.NetMap
|
||||
n2 := *n
|
||||
n2.NetMap = &nm2
|
||||
n2.NetMap.PrivateKey = key.NodePrivate{}
|
||||
return origFn(&n2)
|
||||
}
|
||||
}
|
||||
|
||||
var ini *ipn.Notify
|
||||
|
||||
b.mu.Lock()
|
||||
@@ -1771,6 +1856,10 @@ func (b *LocalBackend) WatchNotifications(ctx context.Context, mask ipn.NotifyWa
|
||||
b.mu.Unlock()
|
||||
}()
|
||||
|
||||
if onWatchAdded != nil {
|
||||
onWatchAdded()
|
||||
}
|
||||
|
||||
if ini != nil {
|
||||
if !fn(ini) {
|
||||
return
|
||||
@@ -2255,6 +2344,9 @@ func (b *LocalBackend) pingPeerAPI(ctx context.Context, ip netip.Addr) (peer *ta
|
||||
if !ok {
|
||||
return nil, "", fmt.Errorf("no peer found with Tailscale IP %v", ip)
|
||||
}
|
||||
if peer.Expired {
|
||||
return nil, "", errors.New("peer's node key has expired")
|
||||
}
|
||||
base := peerAPIBase(nm, peer)
|
||||
if base == "" {
|
||||
return nil, "", fmt.Errorf("no PeerAPI base found for peer %v (%v)", peer.ID, ip)
|
||||
@@ -2386,6 +2478,7 @@ func (b *LocalBackend) checkSSHPrefsLocked(p *ipn.Prefs) error {
|
||||
if distro.Get() == distro.QNAP && !envknob.UseWIPCode() {
|
||||
return errors.New("The Tailscale SSH server does not run on QNAP.")
|
||||
}
|
||||
checkSELinux()
|
||||
// otherwise okay
|
||||
case "darwin":
|
||||
// okay only in tailscaled mode for now.
|
||||
@@ -2395,7 +2488,7 @@ func (b *LocalBackend) checkSSHPrefsLocked(p *ipn.Prefs) error {
|
||||
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":
|
||||
case "freebsd", "openbsd":
|
||||
default:
|
||||
return errors.New("The Tailscale SSH server is not supported on " + runtime.GOOS)
|
||||
}
|
||||
@@ -2997,6 +3090,25 @@ func (b *LocalBackend) SetVarRoot(dir string) {
|
||||
b.varRoot = dir
|
||||
}
|
||||
|
||||
// SetLogFlusher sets a func to be called to flush log uploads.
|
||||
//
|
||||
// It should only be called before the LocalBackend is used.
|
||||
func (b *LocalBackend) SetLogFlusher(flushFunc func()) {
|
||||
b.logFlushFunc = flushFunc
|
||||
}
|
||||
|
||||
// TryFlushLogs calls the log flush function. It returns false if a log flush
|
||||
// function was never initialized with SetLogFlusher.
|
||||
//
|
||||
// TryFlushLogs should not block.
|
||||
func (b *LocalBackend) TryFlushLogs() bool {
|
||||
if b.logFlushFunc == nil {
|
||||
return false
|
||||
}
|
||||
b.logFlushFunc()
|
||||
return true
|
||||
}
|
||||
|
||||
// TailscaleVarRoot returns the root directory of Tailscale's writable
|
||||
// storage area. (e.g. "/var/lib/tailscale")
|
||||
//
|
||||
@@ -3689,6 +3801,12 @@ func (b *LocalBackend) setNetMapLocked(nm *netmap.NetworkMap) {
|
||||
}
|
||||
b.capFileSharing = fs
|
||||
|
||||
b.setDebugLogsByCapabilityLocked(nm)
|
||||
|
||||
// See the netns package for documentation on what this capability does.
|
||||
netns.SetBindToInterfaceByRoute(hasCapability(nm, tailcfg.CapabilityBindToInterfaceByRoute))
|
||||
netns.SetDisableBindConnToInterface(hasCapability(nm, tailcfg.CapabilityDebugDisableBindConnToInterface))
|
||||
|
||||
b.setTCPPortsInterceptedFromNetmapAndPrefsLocked(b.pm.CurrentPrefs())
|
||||
if nm == nil {
|
||||
b.nodeByAddr = nil
|
||||
@@ -3724,6 +3842,18 @@ func (b *LocalBackend) setNetMapLocked(nm *netmap.NetworkMap) {
|
||||
}
|
||||
}
|
||||
|
||||
// setDebugLogsByCapabilityLocked sets debug logging based on the self node's
|
||||
// capabilities in the provided NetMap.
|
||||
func (b *LocalBackend) setDebugLogsByCapabilityLocked(nm *netmap.NetworkMap) {
|
||||
// These are sufficiently cheap (atomic bools) that we don't need to
|
||||
// store state and compare.
|
||||
if hasCapability(nm, tailcfg.CapabilityDebugTSDNSResolution) {
|
||||
dnscache.SetDebugLoggingEnabled(true)
|
||||
} else {
|
||||
dnscache.SetDebugLoggingEnabled(false)
|
||||
}
|
||||
}
|
||||
|
||||
func (b *LocalBackend) reloadServeConfigLocked(prefs ipn.PrefsView) {
|
||||
if b.netMap == nil || b.netMap.SelfNode == nil || !prefs.Valid() || b.pm.CurrentProfile().ID == "" {
|
||||
// We're not logged in, so we don't have a profile.
|
||||
@@ -4231,20 +4361,32 @@ func exitNodeCanProxyDNS(nm *netmap.NetworkMap, exitNodeID tailcfg.StableNodeID)
|
||||
return "", false
|
||||
}
|
||||
for _, p := range nm.Peers {
|
||||
if p.StableID != exitNodeID {
|
||||
continue
|
||||
}
|
||||
services := p.Hostinfo.Services()
|
||||
for i, n := 0, services.Len(); i < n; i++ {
|
||||
s := services.At(i)
|
||||
if s.Proto == tailcfg.PeerAPIDNS && s.Port >= 1 {
|
||||
return peerAPIBase(nm, p) + "/dns-query", true
|
||||
}
|
||||
if p.StableID == exitNodeID && peerCanProxyDNS(p) {
|
||||
return peerAPIBase(nm, p) + "/dns-query", true
|
||||
}
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
func peerCanProxyDNS(p *tailcfg.Node) bool {
|
||||
if p.Cap >= 26 {
|
||||
// Actually added at 25
|
||||
// (https://github.com/tailscale/tailscale/blob/3ae6f898cfdb58fd0e30937147dd6ce28c6808dd/tailcfg/tailcfg.go#L51)
|
||||
// so anything >= 26 can do it.
|
||||
return true
|
||||
}
|
||||
// If p.Cap is not populated (e.g. older control server), then do the old
|
||||
// thing of searching through services.
|
||||
services := p.Hostinfo.Services()
|
||||
for i, n := 0, services.Len(); i < n; i++ {
|
||||
s := services.At(i)
|
||||
if s.Proto == tailcfg.PeerAPIDNS && s.Port >= 1 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (b *LocalBackend) DebugRebind() error {
|
||||
mc, err := b.magicConn()
|
||||
if err != nil {
|
||||
@@ -4368,11 +4510,26 @@ func (b *LocalBackend) sshServerOrInit() (_ SSHServer, err error) {
|
||||
return b.sshServer, nil
|
||||
}
|
||||
|
||||
var warnSSHSELinux = health.NewWarnable()
|
||||
|
||||
func checkSELinux() {
|
||||
if runtime.GOOS != "linux" {
|
||||
return
|
||||
}
|
||||
out, _ := exec.Command("getenforce").Output()
|
||||
if string(bytes.TrimSpace(out)) == "Enforcing" {
|
||||
warnSSHSELinux.Set(errors.New("SELinux is enabled; Tailscale SSH may not work. See https://tailscale.com/s/ssh-selinux"))
|
||||
} else {
|
||||
warnSSHSELinux.Set(nil)
|
||||
}
|
||||
}
|
||||
|
||||
func (b *LocalBackend) HandleSSHConn(c net.Conn) (err error) {
|
||||
s, err := b.sshServerOrInit()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
checkSELinux()
|
||||
return s.HandleSSHConn(c)
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
package ipnlocal
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
@@ -820,3 +821,38 @@ type legacyBackend interface {
|
||||
// Verify that LocalBackend still implements the legacyBackend interface
|
||||
// for now, at least until the macOS and iOS clients move off of it.
|
||||
var _ legacyBackend = (*LocalBackend)(nil)
|
||||
|
||||
func TestWatchNotificationsCallbacks(t *testing.T) {
|
||||
b := new(LocalBackend)
|
||||
n := new(ipn.Notify)
|
||||
b.WatchNotifications(context.Background(), 0, func() {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
|
||||
// Ensure a watcher has been installed.
|
||||
if len(b.notifyWatchers) != 1 {
|
||||
t.Fatalf("unexpected number of watchers in new LocalBackend, want: 1 got: %v", len(b.notifyWatchers))
|
||||
}
|
||||
// Send a notification. Range over notifyWatchers to get the channel
|
||||
// because WatchNotifications doesn't expose the handle for it.
|
||||
for _, c := range b.notifyWatchers {
|
||||
select {
|
||||
case c <- n:
|
||||
default:
|
||||
t.Fatalf("could not send notification")
|
||||
}
|
||||
}
|
||||
}, func(roNotify *ipn.Notify) bool {
|
||||
if roNotify != n {
|
||||
t.Fatalf("unexpected notification received. want: %v got: %v", n, roNotify)
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
// Ensure watchers have been cleaned up.
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
if len(b.notifyWatchers) != 0 {
|
||||
t.Fatalf("unexpected number of watchers in new LocalBackend, want: 0 got: %v", len(b.notifyWatchers))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"time"
|
||||
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/health"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/net/tsaddr"
|
||||
@@ -60,9 +61,11 @@ func (b *LocalBackend) permitTKAInitLocked() bool {
|
||||
func (b *LocalBackend) tkaFilterNetmapLocked(nm *netmap.NetworkMap) {
|
||||
// TODO(tom): Remove this guard for 1.35 and later.
|
||||
if b.tka == nil && !b.permitTKAInitLocked() {
|
||||
health.SetTKAHealth(nil)
|
||||
return
|
||||
}
|
||||
if b.tka == nil {
|
||||
health.SetTKAHealth(nil)
|
||||
return // TKA not enabled.
|
||||
}
|
||||
|
||||
@@ -111,6 +114,13 @@ func (b *LocalBackend) tkaFilterNetmapLocked(nm *netmap.NetworkMap) {
|
||||
} else {
|
||||
b.tka.filtered = nil
|
||||
}
|
||||
|
||||
// Check that we ourselves are not locked out, report a health issue if so.
|
||||
if nm.SelfNode != nil && b.tka.authority.NodeKeyAuthorized(nm.SelfNode.Key, nm.SelfNode.KeySignature) != nil {
|
||||
health.SetTKAHealth(errors.New("this node is locked out; it will not have connectivity until it is signed. For more info, see https://tailscale.com/s/locked-out"))
|
||||
} else {
|
||||
health.SetTKAHealth(nil)
|
||||
}
|
||||
}
|
||||
|
||||
// tkaSyncIfNeeded examines TKA info reported from the control plane,
|
||||
@@ -177,6 +187,7 @@ func (b *LocalBackend) tkaSyncIfNeeded(nm *netmap.NetworkMap, prefs ipn.PrefsVie
|
||||
b.logf("Disablement failed, leaving TKA enabled. Error: %v", err)
|
||||
} else {
|
||||
isEnabled = false
|
||||
health.SetTKAHealth(nil)
|
||||
}
|
||||
} else {
|
||||
return fmt.Errorf("[bug] unreachable invariant of wantEnabled /w isEnabled")
|
||||
@@ -666,7 +677,11 @@ func (b *LocalBackend) NetworkLockModify(addKeys, removeKeys []tka.Key) (err err
|
||||
}
|
||||
}
|
||||
for _, removeKey := range removeKeys {
|
||||
if err := updater.RemoveKey(removeKey.ID()); err != nil {
|
||||
keyID, err := removeKey.ID()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := updater.RemoveKey(keyID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -321,7 +321,7 @@ func TestTKASync(t *testing.T) {
|
||||
name: "control has an update",
|
||||
controlAUMs: func(t *testing.T, a *tka.Authority, storage tka.Chonk, signer tka.Signer) []tka.AUM {
|
||||
b := a.NewUpdater(signer)
|
||||
if err := b.RemoveKey(someKey.ID()); err != nil {
|
||||
if err := b.RemoveKey(someKey.MustID()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
aums, err := b.Finalize(storage)
|
||||
@@ -336,7 +336,7 @@ func TestTKASync(t *testing.T) {
|
||||
name: "node has an update",
|
||||
nodeAUMs: func(t *testing.T, a *tka.Authority, storage tka.Chonk, signer tka.Signer) []tka.AUM {
|
||||
b := a.NewUpdater(signer)
|
||||
if err := b.RemoveKey(someKey.ID()); err != nil {
|
||||
if err := b.RemoveKey(someKey.MustID()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
aums, err := b.Finalize(storage)
|
||||
@@ -351,7 +351,7 @@ func TestTKASync(t *testing.T) {
|
||||
name: "node and control diverge",
|
||||
controlAUMs: func(t *testing.T, a *tka.Authority, storage tka.Chonk, signer tka.Signer) []tka.AUM {
|
||||
b := a.NewUpdater(signer)
|
||||
if err := b.SetKeyMeta(someKey.ID(), map[string]string{"ye": "swiggity"}); err != nil {
|
||||
if err := b.SetKeyMeta(someKey.MustID(), map[string]string{"ye": "swiggity"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
aums, err := b.Finalize(storage)
|
||||
@@ -362,7 +362,7 @@ func TestTKASync(t *testing.T) {
|
||||
},
|
||||
nodeAUMs: func(t *testing.T, a *tka.Authority, storage tka.Chonk, signer tka.Signer) []tka.AUM {
|
||||
b := a.NewUpdater(signer)
|
||||
if err := b.SetKeyMeta(someKey.ID(), map[string]string{"ye": "swooty"}); err != nil {
|
||||
if err := b.SetKeyMeta(someKey.MustID(), map[string]string{"ye": "swooty"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
aums, err := b.Finalize(storage)
|
||||
|
||||
@@ -903,6 +903,9 @@ func (h *peerAPIHandler) canDebug() bool {
|
||||
|
||||
// canWakeOnLAN reports whether h can send a Wake-on-LAN packet from this node.
|
||||
func (h *peerAPIHandler) canWakeOnLAN() bool {
|
||||
if h.peerNode.UnsignedPeerAPIOnly {
|
||||
return false
|
||||
}
|
||||
return h.isSelf || h.peerHasCap(tailcfg.CapabilityWakeOnLAN)
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net/netip"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
@@ -19,6 +20,7 @@ import (
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/clientmetric"
|
||||
"tailscale.com/util/strs"
|
||||
"tailscale.com/util/winutil"
|
||||
"tailscale.com/version"
|
||||
)
|
||||
|
||||
@@ -184,7 +186,7 @@ func init() {
|
||||
func (pm *profileManager) SetPrefs(prefsIn ipn.PrefsView) error {
|
||||
prefs := prefsIn.AsStruct().View()
|
||||
newPersist := prefs.Persist().AsStruct()
|
||||
if newPersist == nil || newPersist.LoginName == "" {
|
||||
if newPersist == nil || newPersist.NodeID == "" {
|
||||
return pm.setPrefsLocked(prefs)
|
||||
}
|
||||
up := newPersist.UserProfile
|
||||
@@ -322,7 +324,7 @@ func (pm *profileManager) setAsUserSelectedProfileLocked() error {
|
||||
func (pm *profileManager) loadSavedPrefs(key ipn.StateKey) (ipn.PrefsView, error) {
|
||||
bs, err := pm.store.ReadState(key)
|
||||
if err == ipn.ErrStateNotExist || len(bs) == 0 {
|
||||
return emptyPrefs, nil
|
||||
return defaultPrefs, nil
|
||||
}
|
||||
if err != nil {
|
||||
return ipn.PrefsView{}, err
|
||||
@@ -394,15 +396,29 @@ func (pm *profileManager) writeKnownProfiles() error {
|
||||
func (pm *profileManager) NewProfile() {
|
||||
metricNewProfile.Add(1)
|
||||
|
||||
pm.prefs = emptyPrefs
|
||||
pm.prefs = defaultPrefs
|
||||
pm.isNewProfile = true
|
||||
pm.currentProfile = &ipn.LoginProfile{}
|
||||
}
|
||||
|
||||
// emptyPrefs is the default prefs for a new profile.
|
||||
var emptyPrefs = func() ipn.PrefsView {
|
||||
// defaultPrefs is the default prefs for a new profile.
|
||||
var defaultPrefs = func() ipn.PrefsView {
|
||||
prefs := ipn.NewPrefs()
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// Allow Incoming (used by the UI) is the negation of ShieldsUp (used by the
|
||||
// backend), so this has to convert between the two conventions.
|
||||
prefs.ShieldsUp = winutil.GetPolicyString("AllowIncomingConnections", "") == "never"
|
||||
prefs.ForceDaemon = winutil.GetPolicyString("UnattendedMode", "") == "always"
|
||||
|
||||
return prefs.View()
|
||||
}()
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ package ipnlocal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"tailscale.com/ipn"
|
||||
@@ -53,7 +54,7 @@ func TestProfileCurrentUserSwitch(t *testing.T) {
|
||||
} else if pm.currentProfile.ID != "" {
|
||||
t.Fatalf("currentProfile.ID = %q, want empty", pm.currentProfile.ID)
|
||||
}
|
||||
if !pm.CurrentPrefs().Equals(emptyPrefs) {
|
||||
if !pm.CurrentPrefs().Equals(defaultPrefs) {
|
||||
t.Fatalf("CurrentPrefs() = %v, want emptyPrefs", pm.CurrentPrefs().Pretty())
|
||||
}
|
||||
|
||||
@@ -67,7 +68,7 @@ func TestProfileCurrentUserSwitch(t *testing.T) {
|
||||
} else if pm.currentProfile.ID != "" {
|
||||
t.Fatalf("currentProfile.ID = %q, want empty", pm.currentProfile.ID)
|
||||
}
|
||||
if !pm.CurrentPrefs().Equals(emptyPrefs) {
|
||||
if !pm.CurrentPrefs().Equals(defaultPrefs) {
|
||||
t.Fatalf("CurrentPrefs() = %v, want emptyPrefs", pm.CurrentPrefs().Pretty())
|
||||
}
|
||||
}
|
||||
@@ -159,7 +160,7 @@ func TestProfileManagement(t *testing.T) {
|
||||
}
|
||||
wantCurProfile := ""
|
||||
wantProfiles := map[string]ipn.PrefsView{
|
||||
"": emptyPrefs,
|
||||
"": defaultPrefs,
|
||||
}
|
||||
checkProfiles := func(t *testing.T) {
|
||||
t.Helper()
|
||||
@@ -237,7 +238,7 @@ func TestProfileManagement(t *testing.T) {
|
||||
t.Logf("Create new profile")
|
||||
pm.NewProfile()
|
||||
wantCurProfile = ""
|
||||
wantProfiles[""] = emptyPrefs
|
||||
wantProfiles[""] = defaultPrefs
|
||||
checkProfiles(t)
|
||||
|
||||
{
|
||||
@@ -276,7 +277,7 @@ func TestProfileManagement(t *testing.T) {
|
||||
t.Logf("Create new profile - 2")
|
||||
pm.NewProfile()
|
||||
wantCurProfile = ""
|
||||
wantProfiles[""] = emptyPrefs
|
||||
wantProfiles[""] = defaultPrefs
|
||||
checkProfiles(t)
|
||||
|
||||
t.Logf("Login with the existing profile")
|
||||
@@ -310,7 +311,7 @@ func TestProfileManagementWindows(t *testing.T) {
|
||||
}
|
||||
wantCurProfile := ""
|
||||
wantProfiles := map[string]ipn.PrefsView{
|
||||
"": emptyPrefs,
|
||||
"": defaultPrefs,
|
||||
}
|
||||
checkProfiles := func(t *testing.T) {
|
||||
t.Helper()
|
||||
@@ -338,6 +339,7 @@ func TestProfileManagementWindows(t *testing.T) {
|
||||
ID: id,
|
||||
LoginName: loginName,
|
||||
},
|
||||
NodeID: tailcfg.StableNodeID(strconv.Itoa(int(id))),
|
||||
}
|
||||
if err := pm.SetPrefs(p.View()); err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -363,7 +365,7 @@ func TestProfileManagementWindows(t *testing.T) {
|
||||
t.Logf("Create new profile")
|
||||
pm.NewProfile()
|
||||
wantCurProfile = ""
|
||||
wantProfiles[""] = emptyPrefs
|
||||
wantProfiles[""] = defaultPrefs
|
||||
checkProfiles(t)
|
||||
|
||||
t.Logf("Save as test profile")
|
||||
@@ -380,7 +382,7 @@ func TestProfileManagementWindows(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
wantCurProfile = ""
|
||||
wantProfiles[""] = emptyPrefs
|
||||
wantProfiles[""] = defaultPrefs
|
||||
checkProfiles(t)
|
||||
|
||||
{
|
||||
|
||||
@@ -33,6 +33,7 @@ import (
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/mak"
|
||||
"tailscale.com/util/strs"
|
||||
"tailscale.com/version"
|
||||
)
|
||||
|
||||
// serveHTTPContextKey is the context.Value key for a *serveHTTPContext.
|
||||
@@ -91,7 +92,38 @@ func (s *serveListener) Close() error {
|
||||
// Listen is retried until the context is canceled.
|
||||
func (s *serveListener) Run() {
|
||||
for {
|
||||
ln, err := net.Listen("tcp", s.ap.String())
|
||||
ip := s.ap.Addr()
|
||||
ipStr := ip.String()
|
||||
|
||||
var lc net.ListenConfig
|
||||
if initListenConfig != nil {
|
||||
// On macOS, this sets the lc.Control hook to
|
||||
// setsockopt the interface index to bind to. This is
|
||||
// required by the network sandbox to allow binding to
|
||||
// a specific interface. Without this hook, the system
|
||||
// chooses a default interface to bind to.
|
||||
if err := initListenConfig(&lc, ip, s.b.prevIfState, s.b.dialer.TUNName()); err != nil {
|
||||
s.logf("serve failed to init listen config %v, backing off: %v", s.ap, err)
|
||||
s.bo.BackOff(s.ctx, err)
|
||||
continue
|
||||
}
|
||||
// On macOS (AppStore or macsys) and if we're binding to a privileged port,
|
||||
if version.IsSandboxedMacOS() && s.ap.Port() < 1024 {
|
||||
// On macOS, we need to bind to ""/all-interfaces due to
|
||||
// the network sandbox. Ideally we would only bind to the
|
||||
// Tailscale interface, but macOS errors out if we try to
|
||||
// to listen on privileged ports binding only to a specific
|
||||
// interface. (#6364)
|
||||
ipStr = ""
|
||||
}
|
||||
}
|
||||
|
||||
tcp4or6 := "tcp4"
|
||||
if ip.Is6() {
|
||||
tcp4or6 = "tcp6"
|
||||
}
|
||||
|
||||
ln, err := lc.Listen(s.ctx, tcp4or6, net.JoinHostPort(ipStr, fmt.Sprint(s.ap.Port())))
|
||||
if err != nil {
|
||||
if s.shouldWarnAboutListenError(err) {
|
||||
s.logf("serve failed to listen on %v, backing off: %v", s.ap, err)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build linux || (darwin && !ios) || freebsd
|
||||
//go:build linux || (darwin && !ios) || freebsd || openbsd
|
||||
|
||||
package ipnlocal
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build ios || (!linux && !darwin && !freebsd)
|
||||
//go:build ios || (!linux && !darwin && !freebsd && !openbsd)
|
||||
|
||||
package ipnlocal
|
||||
|
||||
|
||||
@@ -474,6 +474,7 @@ func TestStateMachine(t *testing.T) {
|
||||
notifies.expect(3)
|
||||
cc.persist.LoginName = "user1"
|
||||
cc.persist.UserProfile.LoginName = "user1"
|
||||
cc.persist.NodeID = "node1"
|
||||
cc.send(nil, "", true, &netmap.NetworkMap{})
|
||||
{
|
||||
nn := notifies.drain(3)
|
||||
@@ -700,6 +701,7 @@ func TestStateMachine(t *testing.T) {
|
||||
notifies.expect(3)
|
||||
cc.persist.LoginName = "user2"
|
||||
cc.persist.UserProfile.LoginName = "user2"
|
||||
cc.persist.NodeID = "node2"
|
||||
cc.send(nil, "", true, &netmap.NetworkMap{
|
||||
MachineStatus: tailcfg.MachineAuthorized,
|
||||
})
|
||||
@@ -836,6 +838,7 @@ func TestStateMachine(t *testing.T) {
|
||||
notifies.expect(3)
|
||||
cc.persist.LoginName = "user3"
|
||||
cc.persist.UserProfile.LoginName = "user3"
|
||||
cc.persist.NodeID = "node3"
|
||||
cc.send(nil, "", true, &netmap.NetworkMap{
|
||||
MachineStatus: tailcfg.MachineAuthorized,
|
||||
})
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/ptr"
|
||||
"tailscale.com/types/views"
|
||||
"tailscale.com/util/dnsname"
|
||||
)
|
||||
@@ -31,6 +32,10 @@ type Status struct {
|
||||
// Version is the daemon's long version (see version.Long).
|
||||
Version string
|
||||
|
||||
// TUN is whether /dev/net/tun (or equivalent kernel interface) is being
|
||||
// used. If false, it's running in userspace mode.
|
||||
TUN bool
|
||||
|
||||
// BackendState is an ipn.State string value:
|
||||
// "NoState", "NeedsLogin", "NeedsMachineAuth", "Stopped",
|
||||
// "Starting", "Running".
|
||||
@@ -242,6 +247,15 @@ type PeerStatus struct {
|
||||
// InEngine means that this peer is tracked by the wireguard engine.
|
||||
// In theory, all of InNetworkMap and InMagicSock and InEngine should all be true.
|
||||
InEngine bool
|
||||
|
||||
// Expired means that this peer's node key has expired, based on either
|
||||
// information from control or optimisically set on the client if the
|
||||
// expiration time has passed.
|
||||
Expired bool `json:",omitempty"`
|
||||
|
||||
// KeyExpiry, if present, is the time at which the node key expired or
|
||||
// will expire.
|
||||
KeyExpiry *time.Time `json:",omitempty"`
|
||||
}
|
||||
|
||||
type StatusBuilder struct {
|
||||
@@ -423,6 +437,12 @@ func (sb *StatusBuilder) AddPeer(peer key.NodePublic, st *PeerStatus) {
|
||||
if st.PeerAPIURL != nil {
|
||||
e.PeerAPIURL = st.PeerAPIURL
|
||||
}
|
||||
if st.Expired {
|
||||
e.Expired = true
|
||||
}
|
||||
if t := st.KeyExpiry; t != nil {
|
||||
e.KeyExpiry = ptr.To(*t)
|
||||
}
|
||||
}
|
||||
|
||||
type StatusUpdater interface {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@ import (
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnlocal"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/logtail"
|
||||
"tailscale.com/net/netutil"
|
||||
"tailscale.com/safesocket"
|
||||
"tailscale.com/tailcfg"
|
||||
@@ -61,39 +62,42 @@ var handler = map[string]localAPIHandler{
|
||||
|
||||
// The other /localapi/v0/NAME handlers are exact matches and contain only NAME
|
||||
// without a trailing slash:
|
||||
"bugreport": (*Handler).serveBugReport,
|
||||
"check-ip-forwarding": (*Handler).serveCheckIPForwarding,
|
||||
"check-prefs": (*Handler).serveCheckPrefs,
|
||||
"component-debug-logging": (*Handler).serveComponentDebugLogging,
|
||||
"debug": (*Handler).serveDebug,
|
||||
"debug-derp-region": (*Handler).serveDebugDERPRegion,
|
||||
"derpmap": (*Handler).serveDERPMap,
|
||||
"dev-set-state-store": (*Handler).serveDevSetStateStore,
|
||||
"dial": (*Handler).serveDial,
|
||||
"file-targets": (*Handler).serveFileTargets,
|
||||
"goroutines": (*Handler).serveGoroutines,
|
||||
"id-token": (*Handler).serveIDToken,
|
||||
"login-interactive": (*Handler).serveLoginInteractive,
|
||||
"logout": (*Handler).serveLogout,
|
||||
"metrics": (*Handler).serveMetrics,
|
||||
"ping": (*Handler).servePing,
|
||||
"prefs": (*Handler).servePrefs,
|
||||
"pprof": (*Handler).servePprof,
|
||||
"serve-config": (*Handler).serveServeConfig,
|
||||
"set-dns": (*Handler).serveSetDNS,
|
||||
"set-expiry-sooner": (*Handler).serveSetExpirySooner,
|
||||
"start": (*Handler).serveStart,
|
||||
"status": (*Handler).serveStatus,
|
||||
"tka/init": (*Handler).serveTKAInit,
|
||||
"tka/log": (*Handler).serveTKALog,
|
||||
"tka/modify": (*Handler).serveTKAModify,
|
||||
"tka/sign": (*Handler).serveTKASign,
|
||||
"tka/status": (*Handler).serveTKAStatus,
|
||||
"tka/disable": (*Handler).serveTKADisable,
|
||||
"tka/force-local-disable": (*Handler).serveTKALocalDisable,
|
||||
"upload-client-metrics": (*Handler).serveUploadClientMetrics,
|
||||
"watch-ipn-bus": (*Handler).serveWatchIPNBus,
|
||||
"whois": (*Handler).serveWhoIs,
|
||||
"bugreport": (*Handler).serveBugReport,
|
||||
"check-ip-forwarding": (*Handler).serveCheckIPForwarding,
|
||||
"check-prefs": (*Handler).serveCheckPrefs,
|
||||
"component-debug-logging": (*Handler).serveComponentDebugLogging,
|
||||
"debug": (*Handler).serveDebug,
|
||||
"debug-derp-region": (*Handler).serveDebugDERPRegion,
|
||||
"debug-packet-filter-matches": (*Handler).serveDebugPacketFilterMatches,
|
||||
"debug-packet-filter-rules": (*Handler).serveDebugPacketFilterRules,
|
||||
"derpmap": (*Handler).serveDERPMap,
|
||||
"dev-set-state-store": (*Handler).serveDevSetStateStore,
|
||||
"dial": (*Handler).serveDial,
|
||||
"file-targets": (*Handler).serveFileTargets,
|
||||
"goroutines": (*Handler).serveGoroutines,
|
||||
"id-token": (*Handler).serveIDToken,
|
||||
"login-interactive": (*Handler).serveLoginInteractive,
|
||||
"logout": (*Handler).serveLogout,
|
||||
"logtap": (*Handler).serveLogTap,
|
||||
"metrics": (*Handler).serveMetrics,
|
||||
"ping": (*Handler).servePing,
|
||||
"prefs": (*Handler).servePrefs,
|
||||
"pprof": (*Handler).servePprof,
|
||||
"serve-config": (*Handler).serveServeConfig,
|
||||
"set-dns": (*Handler).serveSetDNS,
|
||||
"set-expiry-sooner": (*Handler).serveSetExpirySooner,
|
||||
"start": (*Handler).serveStart,
|
||||
"status": (*Handler).serveStatus,
|
||||
"tka/init": (*Handler).serveTKAInit,
|
||||
"tka/log": (*Handler).serveTKALog,
|
||||
"tka/modify": (*Handler).serveTKAModify,
|
||||
"tka/sign": (*Handler).serveTKASign,
|
||||
"tka/status": (*Handler).serveTKAStatus,
|
||||
"tka/disable": (*Handler).serveTKADisable,
|
||||
"tka/force-local-disable": (*Handler).serveTKALocalDisable,
|
||||
"upload-client-metrics": (*Handler).serveUploadClientMetrics,
|
||||
"watch-ipn-bus": (*Handler).serveWatchIPNBus,
|
||||
"whois": (*Handler).serveWhoIs,
|
||||
}
|
||||
|
||||
func randHex(n int) string {
|
||||
@@ -151,6 +155,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
w.Header().Set("Tailscale-Version", version.Long)
|
||||
w.Header().Set("Tailscale-Cap", strconv.Itoa(int(tailcfg.CurrentCapabilityVersion)))
|
||||
w.Header().Set("Content-Security-Policy", `default-src 'none'; frame-ancestors 'none'; script-src 'none'; script-src-elem 'none'; script-src-attr 'none'`)
|
||||
w.Header().Set("X-Frame-Options", "DENY")
|
||||
w.Header().Set("X-Content-Type-Options", "nosniff")
|
||||
@@ -290,6 +295,7 @@ func (h *Handler) serveBugReport(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "only POST allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
defer h.b.TryFlushLogs() // kick off upload after bugreport's done logging
|
||||
|
||||
logMarker := func() string {
|
||||
return fmt.Sprintf("BUG-%v-%v-%v", h.backendLogID, time.Now().UTC().Format("20060102150405Z"), randHex(8))
|
||||
@@ -418,6 +424,45 @@ func (h *Handler) serveGoroutines(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write(buf)
|
||||
}
|
||||
|
||||
// serveLogTap taps into the tailscaled/logtail server output and streams
|
||||
// it to the client.
|
||||
func (h *Handler) serveLogTap(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
// Require write access (~root) as the logs could contain something
|
||||
// sensitive.
|
||||
if !h.PermitWrite {
|
||||
http.Error(w, "logtap access denied", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
if r.Method != "GET" {
|
||||
http.Error(w, "GET required", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
f, ok := w.(http.Flusher)
|
||||
if !ok {
|
||||
http.Error(w, "streaming unsupported", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
io.WriteString(w, `{"text":"[logtap connected]\n"}`+"\n")
|
||||
f.Flush()
|
||||
|
||||
msgc := make(chan string, 16)
|
||||
unreg := logtail.RegisterLogTap(msgc)
|
||||
defer unreg()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case msg := <-msgc:
|
||||
io.WriteString(w, msg)
|
||||
f.Flush()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) serveMetrics(w http.ResponseWriter, r *http.Request) {
|
||||
// Require write access out of paranoia that the metrics
|
||||
// might contain something sensitive.
|
||||
@@ -506,6 +551,40 @@ func (h *Handler) serveDevSetStateStore(w http.ResponseWriter, r *http.Request)
|
||||
io.WriteString(w, "done\n")
|
||||
}
|
||||
|
||||
func (h *Handler) serveDebugPacketFilterRules(w http.ResponseWriter, r *http.Request) {
|
||||
if !h.PermitWrite {
|
||||
http.Error(w, "debug access denied", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
nm := h.b.NetMap()
|
||||
if nm == nil {
|
||||
http.Error(w, "no netmap", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
enc := json.NewEncoder(w)
|
||||
enc.SetIndent("", "\t")
|
||||
enc.Encode(nm.PacketFilterRules)
|
||||
}
|
||||
|
||||
func (h *Handler) serveDebugPacketFilterMatches(w http.ResponseWriter, r *http.Request) {
|
||||
if !h.PermitWrite {
|
||||
http.Error(w, "debug access denied", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
nm := h.b.NetMap()
|
||||
if nm == nil {
|
||||
http.Error(w, "no netmap", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
enc := json.NewEncoder(w)
|
||||
enc.SetIndent("", "\t")
|
||||
enc.Encode(nm.PacketFilter)
|
||||
}
|
||||
|
||||
func (h *Handler) serveComponentDebugLogging(w http.ResponseWriter, r *http.Request) {
|
||||
if !h.PermitWrite {
|
||||
http.Error(w, "debug access denied", http.StatusForbidden)
|
||||
@@ -643,7 +722,6 @@ func (h *Handler) serveWatchIPNBus(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
f.Flush()
|
||||
|
||||
var mask ipn.NotifyWatchOpt
|
||||
if s := r.FormValue("mask"); s != "" {
|
||||
@@ -655,7 +733,7 @@ func (h *Handler) serveWatchIPNBus(w http.ResponseWriter, r *http.Request) {
|
||||
mask = ipn.NotifyWatchOpt(v)
|
||||
}
|
||||
ctx := r.Context()
|
||||
h.b.WatchNotifications(ctx, mask, func(roNotify *ipn.Notify) (keepGoing bool) {
|
||||
h.b.WatchNotifications(ctx, mask, f.Flush, func(roNotify *ipn.Notify) (keepGoing bool) {
|
||||
js, err := json.Marshal(roNotify)
|
||||
if err != nil {
|
||||
h.logf("json.Marshal: %v", err)
|
||||
@@ -987,6 +1065,10 @@ func (h *Handler) serveDERPMap(w http.ResponseWriter, r *http.Request) {
|
||||
// serveSetExpirySooner sets the expiry date on the current machine, specified
|
||||
// by an `expiry` unix timestamp as POST or query param.
|
||||
func (h *Handler) serveSetExpirySooner(w http.ResponseWriter, r *http.Request) {
|
||||
if !h.PermitWrite {
|
||||
http.Error(w, "access denied", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
if r.Method != "POST" {
|
||||
http.Error(w, "POST required", http.StatusMethodNotAllowed)
|
||||
return
|
||||
|
||||
@@ -105,7 +105,7 @@ func (s *awsStore) LoadState() error {
|
||||
context.TODO(),
|
||||
&ssm.GetParameterInput{
|
||||
Name: aws.String(s.ParameterName()),
|
||||
WithDecryption: true,
|
||||
WithDecryption: aws.Bool(true),
|
||||
},
|
||||
)
|
||||
|
||||
@@ -167,7 +167,7 @@ func (s *awsStore) persistState() error {
|
||||
&ssm.PutParameterInput{
|
||||
Name: aws.String(s.ParameterName()),
|
||||
Value: aws.String(string(bs)),
|
||||
Overwrite: true,
|
||||
Overwrite: aws.Bool(true),
|
||||
Tier: ssmTypes.ParameterTierStandard,
|
||||
Type: ssmTypes.ParameterTypeSecureString,
|
||||
},
|
||||
|
||||
@@ -56,13 +56,13 @@ Client][]. See also the dependencies in the [Tailscale CLI][].
|
||||
- [golang.org/x/exp](https://pkg.go.dev/golang.org/x/exp) ([BSD-3-Clause](https://cs.opensource.google/go/x/exp/+/47842c84:LICENSE))
|
||||
- [golang.org/x/exp/shiny](https://pkg.go.dev/golang.org/x/exp/shiny) ([BSD-3-Clause](https://cs.opensource.google/go/x/exp/+/334a2380:shiny/LICENSE))
|
||||
- [golang.org/x/image](https://pkg.go.dev/golang.org/x/image) ([BSD-3-Clause](https://cs.opensource.google/go/x/image/+/062f8c9f:LICENSE))
|
||||
- [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://cs.opensource.google/go/x/net/+/v0.2.0:LICENSE))
|
||||
- [golang.org/x/sync/errgroup](https://pkg.go.dev/golang.org/x/sync/errgroup) ([BSD-3-Clause](https://cs.opensource.google/go/x/sync/+/886fb937:LICENSE))
|
||||
- [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/v0.2.0:LICENSE))
|
||||
- [golang.org/x/term](https://pkg.go.dev/golang.org/x/term) ([BSD-3-Clause](https://cs.opensource.google/go/x/term/+/v0.2.0:LICENSE))
|
||||
- [golang.org/x/text](https://pkg.go.dev/golang.org/x/text) ([BSD-3-Clause](https://cs.opensource.google/go/x/text/+/v0.4.0:LICENSE))
|
||||
- [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://cs.opensource.google/go/x/net/+/v0.5.0:LICENSE))
|
||||
- [golang.org/x/sync/errgroup](https://pkg.go.dev/golang.org/x/sync/errgroup) ([BSD-3-Clause](https://cs.opensource.google/go/x/sync/+/v0.1.0:LICENSE))
|
||||
- [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/v0.4.0:LICENSE))
|
||||
- [golang.org/x/term](https://pkg.go.dev/golang.org/x/term) ([BSD-3-Clause](https://cs.opensource.google/go/x/term/+/v0.4.0:LICENSE))
|
||||
- [golang.org/x/text](https://pkg.go.dev/golang.org/x/text) ([BSD-3-Clause](https://cs.opensource.google/go/x/text/+/v0.6.0:LICENSE))
|
||||
- [golang.org/x/time/rate](https://pkg.go.dev/golang.org/x/time/rate) ([BSD-3-Clause](https://cs.opensource.google/go/x/time/+/579cf78f:LICENSE))
|
||||
- [gvisor.dev/gvisor/pkg](https://pkg.go.dev/gvisor.dev/gvisor/pkg) ([Apache-2.0](https://github.com/google/gvisor/blob/846276b3dbc5/LICENSE))
|
||||
- [gvisor.dev/gvisor/pkg](https://pkg.go.dev/gvisor.dev/gvisor/pkg) ([Apache-2.0](https://github.com/google/gvisor/blob/703fd9b7fbc0/LICENSE))
|
||||
- [inet.af/netaddr](https://pkg.go.dev/inet.af/netaddr) ([BSD-3-Clause](https://github.com/inetaf/netaddr/blob/097006376321/LICENSE))
|
||||
- [inet.af/peercred](https://pkg.go.dev/inet.af/peercred) ([BSD-3-Clause](https://github.com/inetaf/peercred/blob/0893ea02156a/LICENSE))
|
||||
- [nhooyr.io/websocket](https://pkg.go.dev/nhooyr.io/websocket) ([MIT](https://github.com/nhooyr/websocket/blob/v1.8.7/LICENSE.txt))
|
||||
|
||||
@@ -43,11 +43,11 @@ and [iOS][]. See also the dependencies in the [Tailscale CLI][].
|
||||
- [go4.org/netipx](https://pkg.go.dev/go4.org/netipx) ([BSD-3-Clause](https://github.com/go4org/netipx/blob/7e7bdc8411bf/LICENSE))
|
||||
- [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) ([BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/v0.3.0:LICENSE))
|
||||
- [golang.org/x/exp](https://pkg.go.dev/golang.org/x/exp) ([BSD-3-Clause](https://cs.opensource.google/go/x/exp/+/47842c84:LICENSE))
|
||||
- [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://cs.opensource.google/go/x/net/+/v0.2.0:LICENSE))
|
||||
- [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://cs.opensource.google/go/x/net/+/v0.5.0:LICENSE))
|
||||
- [golang.org/x/sync/errgroup](https://pkg.go.dev/golang.org/x/sync/errgroup) ([BSD-3-Clause](https://cs.opensource.google/go/x/sync/+/v0.1.0:LICENSE))
|
||||
- [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/2204b661:LICENSE))
|
||||
- [golang.org/x/term](https://pkg.go.dev/golang.org/x/term) ([BSD-3-Clause](https://cs.opensource.google/go/x/term/+/v0.2.0:LICENSE))
|
||||
- [golang.org/x/text](https://pkg.go.dev/golang.org/x/text) ([BSD-3-Clause](https://cs.opensource.google/go/x/text/+/v0.4.0:LICENSE))
|
||||
- [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/v0.4.0:LICENSE))
|
||||
- [golang.org/x/term](https://pkg.go.dev/golang.org/x/term) ([BSD-3-Clause](https://cs.opensource.google/go/x/term/+/v0.4.0:LICENSE))
|
||||
- [golang.org/x/text](https://pkg.go.dev/golang.org/x/text) ([BSD-3-Clause](https://cs.opensource.google/go/x/text/+/v0.6.0:LICENSE))
|
||||
- [golang.org/x/time/rate](https://pkg.go.dev/golang.org/x/time/rate) ([BSD-3-Clause](https://cs.opensource.google/go/x/time/+/579cf78f:LICENSE))
|
||||
- [gvisor.dev/gvisor/pkg](https://pkg.go.dev/gvisor.dev/gvisor/pkg) ([Apache-2.0](https://github.com/google/gvisor/blob/703fd9b7fbc0/LICENSE))
|
||||
- [inet.af/peercred](https://pkg.go.dev/inet.af/peercred) ([BSD-3-Clause](https://github.com/inetaf/peercred/blob/0893ea02156a/LICENSE))
|
||||
|
||||
@@ -18,19 +18,20 @@ Some packages may only be included on certain architectures or operating systems
|
||||
- [github.com/akutz/memconn](https://pkg.go.dev/github.com/akutz/memconn) ([Apache-2.0](https://github.com/akutz/memconn/blob/v0.1.0/LICENSE))
|
||||
- [github.com/alexbrainman/sspi](https://pkg.go.dev/github.com/alexbrainman/sspi) ([BSD-3-Clause](https://github.com/alexbrainman/sspi/blob/909beea2cc74/LICENSE))
|
||||
- [github.com/anmitsu/go-shlex](https://pkg.go.dev/github.com/anmitsu/go-shlex) ([MIT](https://github.com/anmitsu/go-shlex/blob/38f4b401e2be/LICENSE))
|
||||
- [github.com/aws/aws-sdk-go-v2](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/v1.11.2/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/v1.17.3/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2/config](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/config) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/config/v1.11.0/config/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2/credentials](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/credentials) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/credentials/v1.6.4/credentials/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2/feature/ec2/imds](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/feature/ec2/imds) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/feature/ec2/imds/v1.8.2/feature/ec2/imds/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2/internal/configsources](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/internal/configsources) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/internal/configsources/v1.1.2/internal/configsources/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2/internal/endpoints/v2](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/internal/endpoints/v2) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/internal/endpoints/v2.0.2/internal/endpoints/v2/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2/internal/configsources](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/internal/configsources) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/internal/configsources/v1.1.27/internal/configsources/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2/internal/endpoints/v2](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/internal/endpoints/v2) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/internal/endpoints/v2.4.21/internal/endpoints/v2/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2/internal/ini](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/internal/ini) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/internal/ini/v1.3.2/internal/ini/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2/internal/sync/singleflight](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/internal/sync/singleflight) ([BSD-3-Clause](https://github.com/aws/aws-sdk-go-v2/blob/v1.11.2/internal/sync/singleflight/LICENSE))
|
||||
- [github.com/aws/aws-sdk-go-v2/internal/sync/singleflight](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/internal/sync/singleflight) ([BSD-3-Clause](https://github.com/aws/aws-sdk-go-v2/blob/v1.17.3/internal/sync/singleflight/LICENSE))
|
||||
- [github.com/aws/aws-sdk-go-v2/service/internal/presigned-url](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/internal/presigned-url) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/service/internal/presigned-url/v1.5.2/service/internal/presigned-url/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2/service/ssm](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/ssm) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/service/ssm/v1.17.1/service/ssm/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2/service/ssm](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/ssm) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/service/ssm/v1.35.0/service/ssm/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2/service/sso](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/sso) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/service/sso/v1.6.2/service/sso/LICENSE.txt))
|
||||
- [github.com/aws/aws-sdk-go-v2/service/sts](https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/service/sts) ([Apache-2.0](https://github.com/aws/aws-sdk-go-v2/blob/service/sts/v1.11.1/service/sts/LICENSE.txt))
|
||||
- [github.com/aws/smithy-go](https://pkg.go.dev/github.com/aws/smithy-go) ([Apache-2.0](https://github.com/aws/smithy-go/blob/v1.9.0/LICENSE))
|
||||
- [github.com/aws/smithy-go](https://pkg.go.dev/github.com/aws/smithy-go) ([Apache-2.0](https://github.com/aws/smithy-go/blob/v1.13.5/LICENSE))
|
||||
- [github.com/aws/smithy-go/internal/sync/singleflight](https://pkg.go.dev/github.com/aws/smithy-go/internal/sync/singleflight) ([BSD-3-Clause](https://github.com/aws/smithy-go/blob/v1.13.5/internal/sync/singleflight/LICENSE))
|
||||
- [github.com/coreos/go-iptables/iptables](https://pkg.go.dev/github.com/coreos/go-iptables/iptables) ([Apache-2.0](https://github.com/coreos/go-iptables/blob/v0.6.0/LICENSE))
|
||||
- [github.com/creack/pty](https://pkg.go.dev/github.com/creack/pty) ([MIT](https://github.com/creack/pty/blob/v1.1.17/LICENSE))
|
||||
- [github.com/dblohm7/wingoes](https://pkg.go.dev/github.com/dblohm7/wingoes) ([BSD-3-Clause](https://github.com/dblohm7/wingoes/blob/6ac47ab19aa5/LICENSE))
|
||||
@@ -69,7 +70,7 @@ Some packages may only be included on certain architectures or operating systems
|
||||
- [github.com/tailscale/wireguard-go](https://pkg.go.dev/github.com/tailscale/wireguard-go) ([MIT](https://github.com/tailscale/wireguard-go/blob/4fa124729667/LICENSE))
|
||||
- [github.com/tcnksm/go-httpstat](https://pkg.go.dev/github.com/tcnksm/go-httpstat) ([MIT](https://github.com/tcnksm/go-httpstat/blob/v0.2.0/LICENSE))
|
||||
- [github.com/toqueteos/webbrowser](https://pkg.go.dev/github.com/toqueteos/webbrowser) ([MIT](https://github.com/toqueteos/webbrowser/blob/v1.2.0/LICENSE.md))
|
||||
- [github.com/u-root/u-root/pkg/termios](https://pkg.go.dev/github.com/u-root/u-root/pkg/termios) ([BSD-3-Clause](https://github.com/u-root/u-root/blob/6e9699743f5d/LICENSE))
|
||||
- [github.com/u-root/u-root/pkg/termios](https://pkg.go.dev/github.com/u-root/u-root/pkg/termios) ([BSD-3-Clause](https://github.com/u-root/u-root/blob/948a78c969ad/LICENSE))
|
||||
- [github.com/u-root/uio](https://pkg.go.dev/github.com/u-root/uio) ([BSD-3-Clause](https://github.com/u-root/uio/blob/c3537552635f/LICENSE))
|
||||
- [github.com/vishvananda/netlink/nl](https://pkg.go.dev/github.com/vishvananda/netlink/nl) ([Apache-2.0](https://github.com/vishvananda/netlink/blob/650dca95af54/LICENSE))
|
||||
- [github.com/vishvananda/netns](https://pkg.go.dev/github.com/vishvananda/netns) ([Apache-2.0](https://github.com/vishvananda/netns/blob/50045581ed74/LICENSE))
|
||||
@@ -78,11 +79,11 @@ Some packages may only be included on certain architectures or operating systems
|
||||
- [go4.org/netipx](https://pkg.go.dev/go4.org/netipx) ([BSD-3-Clause](https://github.com/go4org/netipx/blob/7e7bdc8411bf/LICENSE))
|
||||
- [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) ([BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/v0.3.0:LICENSE))
|
||||
- [golang.org/x/exp](https://pkg.go.dev/golang.org/x/exp) ([BSD-3-Clause](https://cs.opensource.google/go/x/exp/+/47842c84:LICENSE))
|
||||
- [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://cs.opensource.google/go/x/net/+/v0.2.0:LICENSE))
|
||||
- [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://cs.opensource.google/go/x/net/+/v0.5.0:LICENSE))
|
||||
- [golang.org/x/sync/errgroup](https://pkg.go.dev/golang.org/x/sync/errgroup) ([BSD-3-Clause](https://cs.opensource.google/go/x/sync/+/v0.1.0:LICENSE))
|
||||
- [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/2204b661:LICENSE))
|
||||
- [golang.org/x/term](https://pkg.go.dev/golang.org/x/term) ([BSD-3-Clause](https://cs.opensource.google/go/x/term/+/v0.2.0:LICENSE))
|
||||
- [golang.org/x/text](https://pkg.go.dev/golang.org/x/text) ([BSD-3-Clause](https://cs.opensource.google/go/x/text/+/v0.4.0:LICENSE))
|
||||
- [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/v0.4.0:LICENSE))
|
||||
- [golang.org/x/term](https://pkg.go.dev/golang.org/x/term) ([BSD-3-Clause](https://cs.opensource.google/go/x/term/+/v0.4.0:LICENSE))
|
||||
- [golang.org/x/text](https://pkg.go.dev/golang.org/x/text) ([BSD-3-Clause](https://cs.opensource.google/go/x/text/+/v0.6.0:LICENSE))
|
||||
- [golang.org/x/time/rate](https://pkg.go.dev/golang.org/x/time/rate) ([BSD-3-Clause](https://cs.opensource.google/go/x/time/+/579cf78f:LICENSE))
|
||||
- [golang.zx2c4.com/wintun](https://pkg.go.dev/golang.zx2c4.com/wintun) ([MIT](https://git.zx2c4.com/wintun-go/tree/LICENSE?id=415007cec224))
|
||||
- [golang.zx2c4.com/wireguard/windows/tunnel/winipcfg](https://pkg.go.dev/golang.zx2c4.com/wireguard/windows/tunnel/winipcfg) ([MIT](https://git.zx2c4.com/wireguard-windows/tree/COPYING?h=v0.5.3))
|
||||
|
||||
@@ -26,17 +26,17 @@ Windows][]. See also the dependencies in the [Tailscale CLI][].
|
||||
- [github.com/mdlayher/socket](https://pkg.go.dev/github.com/mdlayher/socket) ([MIT](https://github.com/mdlayher/socket/blob/v0.2.3/LICENSE.md))
|
||||
- [github.com/mitchellh/go-ps](https://pkg.go.dev/github.com/mitchellh/go-ps) ([MIT](https://github.com/mitchellh/go-ps/blob/v1.0.0/LICENSE.md))
|
||||
- [github.com/skip2/go-qrcode](https://pkg.go.dev/github.com/skip2/go-qrcode) ([MIT](https://github.com/skip2/go-qrcode/blob/da1b6568686e/LICENSE))
|
||||
- [github.com/tailscale/walk](https://pkg.go.dev/github.com/tailscale/walk) ([BSD-3-Clause](https://github.com/tailscale/walk/blob/df8c77379597/LICENSE))
|
||||
- [github.com/tailscale/walk](https://pkg.go.dev/github.com/tailscale/walk) ([BSD-3-Clause](https://github.com/tailscale/walk/blob/e7f9a47617c0/LICENSE))
|
||||
- [github.com/tailscale/win](https://pkg.go.dev/github.com/tailscale/win) ([BSD-3-Clause](https://github.com/tailscale/win/blob/e8ccca099752/LICENSE))
|
||||
- [github.com/x448/float16](https://pkg.go.dev/github.com/x448/float16) ([MIT](https://github.com/x448/float16/blob/v0.8.4/LICENSE))
|
||||
- [go4.org/mem](https://pkg.go.dev/go4.org/mem) ([Apache-2.0](https://github.com/go4org/mem/blob/4f986261bf13/LICENSE))
|
||||
- [go4.org/netipx](https://pkg.go.dev/go4.org/netipx) ([BSD-3-Clause](https://github.com/go4org/netipx/blob/7e7bdc8411bf/LICENSE))
|
||||
- [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) ([BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/v0.3.0:LICENSE))
|
||||
- [golang.org/x/exp](https://pkg.go.dev/golang.org/x/exp) ([BSD-3-Clause](https://cs.opensource.google/go/x/exp/+/47842c84:LICENSE))
|
||||
- [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://cs.opensource.google/go/x/net/+/v0.2.0:LICENSE))
|
||||
- [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://cs.opensource.google/go/x/net/+/v0.5.0:LICENSE))
|
||||
- [golang.org/x/sync/errgroup](https://pkg.go.dev/golang.org/x/sync/errgroup) ([BSD-3-Clause](https://cs.opensource.google/go/x/sync/+/v0.1.0:LICENSE))
|
||||
- [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/2204b661:LICENSE))
|
||||
- [golang.org/x/term](https://pkg.go.dev/golang.org/x/term) ([BSD-3-Clause](https://cs.opensource.google/go/x/term/+/v0.2.0:LICENSE))
|
||||
- [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/v0.4.0:LICENSE))
|
||||
- [golang.org/x/term](https://pkg.go.dev/golang.org/x/term) ([BSD-3-Clause](https://cs.opensource.google/go/x/term/+/v0.4.0:LICENSE))
|
||||
- [golang.zx2c4.com/wintun](https://pkg.go.dev/golang.zx2c4.com/wintun) ([MIT](https://git.zx2c4.com/wintun-go/tree/LICENSE?id=415007cec224))
|
||||
- [golang.zx2c4.com/wireguard/windows/tunnel/winipcfg](https://pkg.go.dev/golang.zx2c4.com/wireguard/windows/tunnel/winipcfg) ([MIT](https://git.zx2c4.com/wireguard-windows/tree/COPYING?h=v0.5.3))
|
||||
- [gopkg.in/Knetic/govaluate.v3](https://pkg.go.dev/gopkg.in/Knetic/govaluate.v3) ([MIT](https://github.com/Knetic/govaluate/blob/v3.0.0/LICENSE))
|
||||
|
||||
@@ -26,6 +26,7 @@ import (
|
||||
"tailscale.com/logtail/backoff"
|
||||
"tailscale.com/net/interfaces"
|
||||
tslogger "tailscale.com/types/logger"
|
||||
"tailscale.com/util/set"
|
||||
"tailscale.com/wgengine/monitor"
|
||||
)
|
||||
|
||||
@@ -66,13 +67,12 @@ type Config struct {
|
||||
// that's safe to embed in a JSON string literal without further escaping.
|
||||
MetricsDelta func() string
|
||||
|
||||
// FlushDelay is how long to wait to accumulate logs before
|
||||
// uploading them.
|
||||
// FlushDelayFn, if non-nil is a func that returns how long to wait to
|
||||
// accumulate logs before uploading them. 0 or negative means to upload
|
||||
// immediately.
|
||||
//
|
||||
// If zero, a default value is used. (currently 2 seconds)
|
||||
//
|
||||
// Negative means to upload immediately.
|
||||
FlushDelay time.Duration
|
||||
// If nil, a default value is used. (currently 2 seconds)
|
||||
FlushDelayFn func() time.Duration
|
||||
|
||||
// IncludeProcID, if true, results in an ephemeral process identifier being
|
||||
// included in logs. The ID is random and not guaranteed to be globally
|
||||
@@ -118,13 +118,13 @@ func NewLogger(cfg Config, logf tslogger.Logf) *Logger {
|
||||
}
|
||||
}
|
||||
if s := envknob.String("TS_DEBUG_LOGTAIL_FLUSHDELAY"); s != "" {
|
||||
var err error
|
||||
cfg.FlushDelay, err = time.ParseDuration(s)
|
||||
if err != nil {
|
||||
if delay, err := time.ParseDuration(s); err == nil {
|
||||
cfg.FlushDelayFn = func() time.Duration { return delay }
|
||||
} else {
|
||||
log.Fatalf("invalid TS_DEBUG_LOGTAIL_FLUSHDELAY: %v", err)
|
||||
}
|
||||
} else if cfg.FlushDelay == 0 && !envknob.Bool("IN_TS_TEST") {
|
||||
cfg.FlushDelay = defaultFlushDelay
|
||||
} else if cfg.FlushDelayFn == nil && envknob.Bool("IN_TS_TEST") {
|
||||
cfg.FlushDelayFn = func() time.Duration { return 0 }
|
||||
}
|
||||
|
||||
stdLogf := func(f string, a ...any) {
|
||||
@@ -145,7 +145,7 @@ func NewLogger(cfg Config, logf tslogger.Logf) *Logger {
|
||||
skipClientTime: cfg.SkipClientTime,
|
||||
drainWake: make(chan struct{}, 1),
|
||||
sentinel: make(chan int32, 16),
|
||||
flushDelay: cfg.FlushDelay,
|
||||
flushDelayFn: cfg.FlushDelayFn,
|
||||
timeNow: cfg.TimeNow,
|
||||
bo: backoff.NewBackoff("logtail", stdLogf, 30*time.Second),
|
||||
metricsDelta: cfg.MetricsDelta,
|
||||
@@ -179,8 +179,8 @@ type Logger struct {
|
||||
skipClientTime bool
|
||||
linkMonitor *monitor.Mon
|
||||
buffer Buffer
|
||||
drainWake chan struct{} // signal to speed up drain
|
||||
flushDelay time.Duration // negative or zero to upload agressively, or >0 to batch at this delay
|
||||
drainWake chan struct{} // signal to speed up drain
|
||||
flushDelayFn func() time.Duration // negative or zero return value to upload aggressively, or >0 to batch at this delay
|
||||
flushPending atomic.Bool
|
||||
sentinel chan int32
|
||||
timeNow func() time.Time
|
||||
@@ -462,12 +462,24 @@ func (l *Logger) upload(ctx context.Context, body []byte, origlen int) (uploaded
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Flush uploads all logs to the server.
|
||||
// It blocks until complete or there is an unrecoverable error.
|
||||
// Flush uploads all logs to the server. It blocks until complete or there is an
|
||||
// unrecoverable error.
|
||||
//
|
||||
// TODO(bradfitz): this apparently just returns nil, as of tailscale/corp@9c2ec35.
|
||||
// Finish cleaning this up.
|
||||
func (l *Logger) Flush() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// StartFlush starts a log upload, if anything is pending.
|
||||
//
|
||||
// If l is nil, StartFlush is a no-op.
|
||||
func (l *Logger) StartFlush() {
|
||||
if l != nil {
|
||||
l.tryDrainWake()
|
||||
}
|
||||
}
|
||||
|
||||
// logtailDisabled is whether logtail uploads to logcatcher are disabled.
|
||||
var logtailDisabled atomic.Bool
|
||||
|
||||
@@ -494,18 +506,23 @@ func (l *Logger) tryDrainWake() {
|
||||
}
|
||||
|
||||
func (l *Logger) sendLocked(jsonBlob []byte) (int, error) {
|
||||
tapSend(jsonBlob)
|
||||
if logtailDisabled.Load() {
|
||||
return len(jsonBlob), nil
|
||||
}
|
||||
|
||||
n, err := l.buffer.Write(jsonBlob)
|
||||
|
||||
if l.flushDelay > 0 {
|
||||
flushDelay := defaultFlushDelay
|
||||
if l.flushDelayFn != nil {
|
||||
flushDelay = l.flushDelayFn()
|
||||
}
|
||||
if flushDelay > 0 {
|
||||
if l.flushPending.CompareAndSwap(false, true) {
|
||||
if l.flushTimer == nil {
|
||||
l.flushTimer = time.AfterFunc(l.flushDelay, l.tryDrainWake)
|
||||
l.flushTimer = time.AfterFunc(flushDelay, l.tryDrainWake)
|
||||
} else {
|
||||
l.flushTimer.Reset(l.flushDelay)
|
||||
l.flushTimer.Reset(flushDelay)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -542,7 +559,7 @@ func (l *Logger) encodeText(buf []byte, skipClientTime bool, procID uint32, proc
|
||||
// Put a sanity cap on buf's size.
|
||||
max := 16 << 10
|
||||
if l.lowMem {
|
||||
max = 255
|
||||
max = 4 << 10
|
||||
}
|
||||
var nTruncated int
|
||||
if len(buf) > max {
|
||||
@@ -743,3 +760,48 @@ func parseAndRemoveLogLevel(buf []byte) (level int, cleanBuf []byte) {
|
||||
}
|
||||
return 0, buf
|
||||
}
|
||||
|
||||
var (
|
||||
tapSetSize atomic.Int32
|
||||
tapMu sync.Mutex
|
||||
tapSet set.HandleSet[chan<- string]
|
||||
)
|
||||
|
||||
// RegisterLogTap registers dst to get a copy of every log write. The caller
|
||||
// must call unregister when done watching.
|
||||
//
|
||||
// This would ideally be a method on Logger, but Logger isn't really available
|
||||
// in most places; many writes go via stderr which filch redirects to the
|
||||
// singleton Logger set up early. For better or worse, there's basically only
|
||||
// one Logger within the program. This mechanism at least works well for
|
||||
// tailscaled. It works less well for a binary with multiple tsnet.Servers. Oh
|
||||
// well. This then subscribes to all of them.
|
||||
func RegisterLogTap(dst chan<- string) (unregister func()) {
|
||||
tapMu.Lock()
|
||||
defer tapMu.Unlock()
|
||||
h := tapSet.Add(dst)
|
||||
tapSetSize.Store(int32(len(tapSet)))
|
||||
return func() {
|
||||
tapMu.Lock()
|
||||
defer tapMu.Unlock()
|
||||
delete(tapSet, h)
|
||||
tapSetSize.Store(int32(len(tapSet)))
|
||||
}
|
||||
}
|
||||
|
||||
// tapSend relays the JSON blob to any/all registered local debug log watchers
|
||||
// (somebody running "tailscale debug daemon-logs").
|
||||
func tapSend(jsonBlob []byte) {
|
||||
if tapSetSize.Load() == 0 {
|
||||
return
|
||||
}
|
||||
s := string(jsonBlob)
|
||||
tapMu.Lock()
|
||||
defer tapMu.Unlock()
|
||||
for _, dst := range tapSet {
|
||||
select {
|
||||
case dst <- s:
|
||||
default:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -190,7 +190,7 @@ func TestEncodeSpecialCases(t *testing.T) {
|
||||
// lowMem + long string
|
||||
l.skipClientTime = false
|
||||
l.lowMem = true
|
||||
longStr := strings.Repeat("0", 512)
|
||||
longStr := strings.Repeat("0", 5120)
|
||||
io.WriteString(l, longStr)
|
||||
body = <-ts.uploaded
|
||||
data = unmarshalOne(t, body)
|
||||
@@ -198,8 +198,8 @@ func TestEncodeSpecialCases(t *testing.T) {
|
||||
if !ok {
|
||||
t.Errorf("lowMem: no text %v", data)
|
||||
}
|
||||
if n := len(text.(string)); n > 300 {
|
||||
t.Errorf("lowMem: got %d chars; want <300 chars", n)
|
||||
if n := len(text.(string)); n > 4500 {
|
||||
t.Errorf("lowMem: got %d chars; want <4500 chars", n)
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
@@ -333,10 +333,10 @@ func unmarshalOne(t *testing.T, body []byte) map[string]any {
|
||||
|
||||
func TestEncodeTextTruncation(t *testing.T) {
|
||||
lg := &Logger{timeNow: time.Now, lowMem: true}
|
||||
in := bytes.Repeat([]byte("a"), 300)
|
||||
in := bytes.Repeat([]byte("a"), 5120)
|
||||
b := lg.encodeText(in, true, 0, 0, 0)
|
||||
got := string(b)
|
||||
want := `{"text": "` + strings.Repeat("a", 255) + `…+45"}` + "\n"
|
||||
want := `{"text": "` + strings.Repeat("a", 4096) + `…+1024"}` + "\n"
|
||||
if got != want {
|
||||
t.Errorf("got:\n%qwant:\n%q\n", got, want)
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
"time"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
"tailscale.com/cmd/testwrapper/flakytest"
|
||||
"tailscale.com/types/ipproto"
|
||||
"tailscale.com/types/netlogtype"
|
||||
)
|
||||
@@ -46,6 +47,7 @@ func testPacketV4(proto ipproto.Proto, srcAddr, dstAddr [4]byte, srcPort, dstPor
|
||||
}
|
||||
|
||||
func TestConcurrent(t *testing.T) {
|
||||
flakytest.Mark(t, "https://github.com/tailscale/tailscale/issues/7030")
|
||||
c := qt.New(t)
|
||||
|
||||
const maxPeriod = 10 * time.Millisecond
|
||||
|
||||
@@ -88,6 +88,7 @@ func testDirect(t *testing.T, fs wholeFileFS) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := `# resolv.conf(5) file generated by tailscale
|
||||
# For more info, see https://tailscale.com/s/resolvconf-overwrite
|
||||
# DO NOT EDIT THIS FILE BY HAND -- CHANGES WILL BE OVERWRITTEN
|
||||
|
||||
nameserver 8.8.8.8
|
||||
|
||||
@@ -19,41 +19,21 @@ import (
|
||||
"golang.org/x/exp/slices"
|
||||
"tailscale.com/health"
|
||||
"tailscale.com/net/dns/resolver"
|
||||
"tailscale.com/net/packet"
|
||||
"tailscale.com/net/tsaddr"
|
||||
"tailscale.com/net/tsdial"
|
||||
"tailscale.com/net/tstun"
|
||||
"tailscale.com/types/dnstype"
|
||||
"tailscale.com/types/ipproto"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/clientmetric"
|
||||
"tailscale.com/util/dnsname"
|
||||
"tailscale.com/wgengine/monitor"
|
||||
)
|
||||
|
||||
var (
|
||||
magicDNSIP = tsaddr.TailscaleServiceIP()
|
||||
magicDNSIPv6 = tsaddr.TailscaleServiceIPv6()
|
||||
)
|
||||
|
||||
var (
|
||||
errFullQueue = errors.New("request queue full")
|
||||
)
|
||||
|
||||
// maxActiveQueries returns the maximal number of DNS requests that be
|
||||
// can running.
|
||||
// If EnqueueRequest is called when this many requests are already pending,
|
||||
// the request will be dropped to avoid blocking the caller.
|
||||
func maxActiveQueries() int32 {
|
||||
if runtime.GOOS == "ios" {
|
||||
// For memory paranoia reasons on iOS, match the
|
||||
// historical Tailscale 1.x..1.8 behavior for now
|
||||
// (just before the 1.10 release).
|
||||
return 64
|
||||
}
|
||||
// But for other platforms, allow more burstiness:
|
||||
return 256
|
||||
}
|
||||
// maxActiveQueries returns the maximal number of DNS requests that can
|
||||
// be running.
|
||||
const maxActiveQueries = 256
|
||||
|
||||
// We use file-ignore below instead of ignore because on some platforms,
|
||||
// the lint exception is necessary and on others it is not,
|
||||
@@ -75,13 +55,6 @@ type response struct {
|
||||
type Manager struct {
|
||||
logf logger.Logf
|
||||
|
||||
// When netstack is not used, Manager implements magic DNS.
|
||||
// In this case, responses tracks completed DNS requests
|
||||
// which need a response, and NextPacket() synthesizes a
|
||||
// fake IP+UDP header to finish assembling the response.
|
||||
//
|
||||
// TODO(tom): Rip out once all platforms use netstack.
|
||||
responses chan response
|
||||
activeQueriesAtomic int32
|
||||
|
||||
ctx context.Context // good until Down
|
||||
@@ -98,10 +71,9 @@ func NewManager(logf logger.Logf, oscfg OSConfigurator, linkMon *monitor.Mon, di
|
||||
}
|
||||
logf = logger.WithPrefix(logf, "dns: ")
|
||||
m := &Manager{
|
||||
logf: logf,
|
||||
resolver: resolver.New(logf, linkMon, linkSel, dialer),
|
||||
os: oscfg,
|
||||
responses: make(chan response),
|
||||
logf: logf,
|
||||
resolver: resolver.New(logf, linkMon, linkSel, dialer),
|
||||
os: oscfg,
|
||||
}
|
||||
m.ctx, m.ctxCancel = context.WithCancel(context.Background())
|
||||
m.logf("using %T", m.os)
|
||||
@@ -316,89 +288,6 @@ func toIPsOnly(resolvers []*dnstype.Resolver) (ret []netip.Addr) {
|
||||
return ret
|
||||
}
|
||||
|
||||
// EnqueuePacket is the legacy path for handling magic DNS traffic, and is
|
||||
// called with a DNS request payload.
|
||||
//
|
||||
// TODO(tom): Rip out once all platforms use netstack.
|
||||
func (m *Manager) EnqueuePacket(bs []byte, proto ipproto.Proto, from, to netip.AddrPort) error {
|
||||
if to.Port() != 53 || proto != ipproto.UDP {
|
||||
return nil
|
||||
}
|
||||
|
||||
if n := atomic.AddInt32(&m.activeQueriesAtomic, 1); n > maxActiveQueries() {
|
||||
atomic.AddInt32(&m.activeQueriesAtomic, -1)
|
||||
metricDNSQueryErrorQueue.Add(1)
|
||||
return errFullQueue
|
||||
}
|
||||
|
||||
go func() {
|
||||
resp, err := m.resolver.Query(m.ctx, bs, from)
|
||||
if err != nil {
|
||||
atomic.AddInt32(&m.activeQueriesAtomic, -1)
|
||||
m.logf("dns query: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
select {
|
||||
case <-m.ctx.Done():
|
||||
return
|
||||
case m.responses <- response{resp, from}:
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
// NextPacket is the legacy path for obtaining DNS results in response to
|
||||
// magic DNS queries. It blocks until a response is available.
|
||||
//
|
||||
// TODO(tom): Rip out once all platforms use netstack.
|
||||
func (m *Manager) NextPacket() ([]byte, error) {
|
||||
var resp response
|
||||
select {
|
||||
case <-m.ctx.Done():
|
||||
return nil, net.ErrClosed
|
||||
case resp = <-m.responses:
|
||||
// continue
|
||||
}
|
||||
|
||||
// Unused space is needed further down the stack. To avoid extra
|
||||
// allocations/copying later on, we allocate such space here.
|
||||
const offset = tstun.PacketStartOffset
|
||||
|
||||
var buf []byte
|
||||
switch {
|
||||
case resp.to.Addr().Is4():
|
||||
h := packet.UDP4Header{
|
||||
IP4Header: packet.IP4Header{
|
||||
Src: magicDNSIP,
|
||||
Dst: resp.to.Addr(),
|
||||
},
|
||||
SrcPort: 53,
|
||||
DstPort: resp.to.Port(),
|
||||
}
|
||||
hlen := h.Len()
|
||||
buf = make([]byte, offset+hlen+len(resp.pkt))
|
||||
copy(buf[offset+hlen:], resp.pkt)
|
||||
h.Marshal(buf[offset:])
|
||||
case resp.to.Addr().Is6():
|
||||
h := packet.UDP6Header{
|
||||
IP6Header: packet.IP6Header{
|
||||
Src: magicDNSIPv6,
|
||||
Dst: resp.to.Addr(),
|
||||
},
|
||||
SrcPort: 53,
|
||||
DstPort: resp.to.Port(),
|
||||
}
|
||||
hlen := h.Len()
|
||||
buf = make([]byte, offset+hlen+len(resp.pkt))
|
||||
copy(buf[offset+hlen:], resp.pkt)
|
||||
h.Marshal(buf[offset:])
|
||||
}
|
||||
|
||||
atomic.AddInt32(&m.activeQueriesAtomic, -1)
|
||||
return buf, nil
|
||||
}
|
||||
|
||||
// Query executes a DNS query received from the given address. The query is
|
||||
// provided in bs as a wire-encoded DNS query without any transport header.
|
||||
// This method is called for requests arriving over UDP and TCP.
|
||||
@@ -410,7 +299,7 @@ func (m *Manager) Query(ctx context.Context, bs []byte, from netip.AddrPort) ([]
|
||||
// continue
|
||||
}
|
||||
|
||||
if n := atomic.AddInt32(&m.activeQueriesAtomic, 1); n > maxActiveQueries() {
|
||||
if n := atomic.AddInt32(&m.activeQueriesAtomic, 1); n > maxActiveQueries {
|
||||
atomic.AddInt32(&m.activeQueriesAtomic, -1)
|
||||
metricDNSQueryErrorQueue.Add(1)
|
||||
return nil, errFullQueue
|
||||
|
||||
@@ -42,6 +42,7 @@ type Config struct {
|
||||
func (c *Config) Write(w io.Writer) error {
|
||||
buf := new(bytes.Buffer)
|
||||
io.WriteString(buf, "# resolv.conf(5) file generated by tailscale\n")
|
||||
io.WriteString(buf, "# For more info, see https://tailscale.com/s/resolvconf-overwrite\n")
|
||||
io.WriteString(buf, "# DO NOT EDIT THIS FILE BY HAND -- CHANGES WILL BE OVERWRITTEN\n\n")
|
||||
for _, ns := range c.Nameservers {
|
||||
io.WriteString(buf, "nameserver ")
|
||||
@@ -83,17 +84,26 @@ func Parse(r io.Reader) (*Config, error) {
|
||||
}
|
||||
|
||||
if s, ok := strs.CutPrefix(line, "search"); ok {
|
||||
domain := strings.TrimSpace(s)
|
||||
if len(domain) == len(s) {
|
||||
domains := strings.TrimSpace(s)
|
||||
if len(domains) == len(s) {
|
||||
// No leading space?!
|
||||
return nil, fmt.Errorf("missing space after \"domain\" in %q", line)
|
||||
return nil, fmt.Errorf("missing space after \"search\" in %q", line)
|
||||
}
|
||||
fqdn, err := dnsname.ToFQDN(domain)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parsing search domains %q: %w", line, err)
|
||||
for len(domains) > 0 {
|
||||
domain := domains
|
||||
i := strings.IndexAny(domain, " \t")
|
||||
if i != -1 {
|
||||
domain = domain[:i]
|
||||
domains = strings.TrimSpace(domains[i+1:])
|
||||
} else {
|
||||
domains = ""
|
||||
}
|
||||
fqdn, err := dnsname.ToFQDN(domain)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parsing search domain %q in %q: %w", domain, line, err)
|
||||
}
|
||||
config.SearchDomains = append(config.SearchDomains, fqdn)
|
||||
}
|
||||
config.SearchDomains = append(config.SearchDomains, fqdn)
|
||||
continue
|
||||
}
|
||||
}
|
||||
return config, nil
|
||||
|
||||
@@ -57,6 +57,31 @@ func TestParse(t *testing.T) {
|
||||
},
|
||||
{in: `searchtailsacle.com`, wantErr: true},
|
||||
{in: `search`, wantErr: true},
|
||||
|
||||
// Issue 6875: there can be multiple search domains, and even if they're
|
||||
// over 253 bytes long total.
|
||||
{
|
||||
in: "search search-01.example search-02.example search-03.example search-04.example search-05.example search-06.example search-07.example search-08.example search-09.example search-10.example search-11.example search-12.example search-13.example search-14.example search-15.example\n",
|
||||
want: &Config{
|
||||
SearchDomains: []dnsname.FQDN{
|
||||
"search-01.example.",
|
||||
"search-02.example.",
|
||||
"search-03.example.",
|
||||
"search-04.example.",
|
||||
"search-05.example.",
|
||||
"search-06.example.",
|
||||
"search-07.example.",
|
||||
"search-08.example.",
|
||||
"search-09.example.",
|
||||
"search-10.example.",
|
||||
"search-11.example.",
|
||||
"search-12.example.",
|
||||
"search-13.example.",
|
||||
"search-14.example.",
|
||||
"search-15.example.",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
||||
@@ -18,9 +18,11 @@ import (
|
||||
"net/netip"
|
||||
"runtime"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/cloudenv"
|
||||
"tailscale.com/util/singleflight"
|
||||
)
|
||||
@@ -84,6 +86,11 @@ type Resolver struct {
|
||||
// It is required when SingleHostStaticResult is present.
|
||||
SingleHost string
|
||||
|
||||
// Logf optionally provides a log function to use for debug logs. If
|
||||
// not present, log.Printf will be used. The prefix "dnscache: " will
|
||||
// be added to all log messages printed with this logger.
|
||||
Logf logger.Logf
|
||||
|
||||
sf singleflight.Group[string, ipRes]
|
||||
|
||||
mu sync.Mutex
|
||||
@@ -110,6 +117,20 @@ func (r *Resolver) fwd() *net.Resolver {
|
||||
return net.DefaultResolver
|
||||
}
|
||||
|
||||
// dlogf logs a debug message if debug logging is enabled either globally via
|
||||
// the TS_DEBUG_DNS_CACHE environment variable or via the per-Resolver
|
||||
// configuration.
|
||||
func (r *Resolver) dlogf(format string, args ...any) {
|
||||
logf := r.Logf
|
||||
if logf == nil {
|
||||
logf = log.Printf
|
||||
}
|
||||
|
||||
if debug() || debugLogging.Load() {
|
||||
logf("dnscache: "+format, args...)
|
||||
}
|
||||
}
|
||||
|
||||
// cloudHostResolver returns a Resolver for the current cloud hosting environment.
|
||||
// It currently only supports Google Cloud.
|
||||
func (r *Resolver) cloudHostResolver() (v *net.Resolver, ok bool) {
|
||||
@@ -143,6 +164,27 @@ func (r *Resolver) ttl() time.Duration {
|
||||
|
||||
var debug = envknob.RegisterBool("TS_DEBUG_DNS_CACHE")
|
||||
|
||||
// debugLogging allows enabling debug logging at runtime, via
|
||||
// SetDebugLoggingEnabled.
|
||||
//
|
||||
// This is a global variable instead of a per-Resolver variable because we
|
||||
// create new Resolvers throughout the lifetime of the program (e.g. on every
|
||||
// new Direct client, etc.). When we enable debug logs, though, we want to do
|
||||
// so for every single created Resolver; we'd need to plumb a bunch of new code
|
||||
// through all of the intermediate packages to accomplish the same behaviour as
|
||||
// just using a global variable.
|
||||
var debugLogging atomic.Bool
|
||||
|
||||
// SetDebugLoggingEnabled controls whether debug logging is enabled for this
|
||||
// package.
|
||||
//
|
||||
// These logs are also printed when the TS_DEBUG_DNS_CACHE envknob is set, but
|
||||
// we allow configuring this manually as well so that it can be changed at
|
||||
// runtime.
|
||||
func SetDebugLoggingEnabled(v bool) {
|
||||
debugLogging.Store(v)
|
||||
}
|
||||
|
||||
// LookupIP returns the host's primary IP address (either IPv4 or
|
||||
// IPv6, but preferring IPv4) and optionally its IPv6 address, if
|
||||
// there is both IPv4 and IPv6.
|
||||
@@ -163,20 +205,17 @@ func (r *Resolver) LookupIP(ctx context.Context, host string) (ip, v6 netip.Addr
|
||||
}
|
||||
allIPs = append(allIPs, naIP)
|
||||
}
|
||||
r.dlogf("returning %d static results", len(allIPs))
|
||||
return
|
||||
}
|
||||
if ip, err := netip.ParseAddr(host); err == nil {
|
||||
ip = ip.Unmap()
|
||||
if debug() {
|
||||
log.Printf("dnscache: %q is an IP", host)
|
||||
}
|
||||
r.dlogf("%q is an IP", host)
|
||||
return ip, zaddr, []netip.Addr{ip}, nil
|
||||
}
|
||||
|
||||
if ip, ip6, allIPs, ok := r.lookupIPCache(host); ok {
|
||||
if debug() {
|
||||
log.Printf("dnscache: %q = %v (cached)", host, ip)
|
||||
}
|
||||
r.dlogf("%q = %v (cached)", host, ip)
|
||||
return ip, ip6, allIPs, nil
|
||||
}
|
||||
|
||||
@@ -192,23 +231,17 @@ func (r *Resolver) LookupIP(ctx context.Context, host string) (ip, v6 netip.Addr
|
||||
if res.Err != nil {
|
||||
if r.UseLastGood {
|
||||
if ip, ip6, allIPs, ok := r.lookupIPCacheExpired(host); ok {
|
||||
if debug() {
|
||||
log.Printf("dnscache: %q using %v after error", host, ip)
|
||||
}
|
||||
r.dlogf("%q using %v after error", host, ip)
|
||||
return ip, ip6, allIPs, nil
|
||||
}
|
||||
}
|
||||
if debug() {
|
||||
log.Printf("dnscache: error resolving %q: %v", host, res.Err)
|
||||
}
|
||||
r.dlogf("error resolving %q: %v", host, res.Err)
|
||||
return zaddr, zaddr, nil, res.Err
|
||||
}
|
||||
r := res.Val
|
||||
return r.ip, r.ip6, r.allIPs, nil
|
||||
case <-ctx.Done():
|
||||
if debug() {
|
||||
log.Printf("dnscache: context done while resolving %q: %v", host, ctx.Err())
|
||||
}
|
||||
r.dlogf("context done while resolving %q: %v", host, ctx.Err())
|
||||
return zaddr, zaddr, nil, ctx.Err()
|
||||
}
|
||||
}
|
||||
@@ -250,9 +283,7 @@ func (r *Resolver) lookupTimeoutForHost(host string) time.Duration {
|
||||
|
||||
func (r *Resolver) lookupIP(host string) (ip, ip6 netip.Addr, allIPs []netip.Addr, err error) {
|
||||
if ip, ip6, allIPs, ok := r.lookupIPCache(host); ok {
|
||||
if debug() {
|
||||
log.Printf("dnscache: %q found in cache as %v", host, ip)
|
||||
}
|
||||
r.dlogf("%q found in cache as %v", host, ip)
|
||||
return ip, ip6, allIPs, nil
|
||||
}
|
||||
|
||||
@@ -261,12 +292,18 @@ func (r *Resolver) lookupIP(host string) (ip, ip6 netip.Addr, allIPs []netip.Add
|
||||
ips, err := r.fwd().LookupNetIP(ctx, "ip", host)
|
||||
if err != nil || len(ips) == 0 {
|
||||
if resolver, ok := r.cloudHostResolver(); ok {
|
||||
r.dlogf("resolving %q via cloud resolver", host)
|
||||
ips, err = resolver.LookupNetIP(ctx, "ip", host)
|
||||
}
|
||||
}
|
||||
if (err != nil || len(ips) == 0) && r.LookupIPFallback != nil {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
if err != nil {
|
||||
r.dlogf("resolving %q using fallback resolver due to error", host)
|
||||
} else {
|
||||
r.dlogf("resolving %q using fallback resolver due to no returned IPs", host)
|
||||
}
|
||||
ips, err = r.LookupIPFallback(ctx, host)
|
||||
}
|
||||
if err != nil {
|
||||
@@ -305,15 +342,11 @@ func (r *Resolver) addIPCache(host string, ip, ip6 netip.Addr, allIPs []netip.Ad
|
||||
if ip.IsPrivate() {
|
||||
// Don't cache obviously wrong entries from captive portals.
|
||||
// TODO: use DoH or DoT for the forwarding resolver?
|
||||
if debug() {
|
||||
log.Printf("dnscache: %q resolved to private IP %v; using but not caching", host, ip)
|
||||
}
|
||||
r.dlogf("%q resolved to private IP %v; using but not caching", host, ip)
|
||||
return
|
||||
}
|
||||
|
||||
if debug() {
|
||||
log.Printf("dnscache: %q resolved to IP %v; caching", host, ip)
|
||||
}
|
||||
r.dlogf("%q resolved to IP %v; caching", host, ip)
|
||||
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
@@ -387,9 +420,7 @@ func (d *dialer) DialContext(ctx context.Context, network, address string) (retC
|
||||
}
|
||||
i4s := v4addrs(allIPs)
|
||||
if len(i4s) < 2 {
|
||||
if debug() {
|
||||
log.Printf("dnscache: dialing %s, %s for %s", network, ip, address)
|
||||
}
|
||||
d.dnsCache.dlogf("dialing %s, %s for %s", network, ip, address)
|
||||
c, err := dc.dialOne(ctx, ip.Unmap())
|
||||
if err == nil || ctx.Err() != nil {
|
||||
return c, err
|
||||
@@ -411,26 +442,20 @@ func (d *dialer) shouldTryBootstrap(ctx context.Context, err error, dc *dialCall
|
||||
|
||||
// Can't try bootstrap DNS if we don't have a fallback function
|
||||
if d.dnsCache.LookupIPFallback == nil {
|
||||
if debug() {
|
||||
log.Printf("dnscache: not using bootstrap DNS: no fallback")
|
||||
}
|
||||
d.dnsCache.dlogf("not using bootstrap DNS: no fallback")
|
||||
return false
|
||||
}
|
||||
|
||||
// We can't retry if the context is canceled, since any further
|
||||
// operations with this context will fail.
|
||||
if ctxErr := ctx.Err(); ctxErr != nil {
|
||||
if debug() {
|
||||
log.Printf("dnscache: not using bootstrap DNS: context error: %v", ctxErr)
|
||||
}
|
||||
d.dnsCache.dlogf("not using bootstrap DNS: context error: %v", ctxErr)
|
||||
return false
|
||||
}
|
||||
|
||||
wasTrustworthy := dc.dnsWasTrustworthy()
|
||||
if wasTrustworthy {
|
||||
if debug() {
|
||||
log.Printf("dnscache: not using bootstrap DNS: DNS was trustworthy")
|
||||
}
|
||||
d.dnsCache.dlogf("not using bootstrap DNS: DNS was trustworthy")
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@ func TestDialer(t *testing.T) {
|
||||
if *dialTest == "" {
|
||||
t.Skip("skipping; --dial-test is blank")
|
||||
}
|
||||
r := new(Resolver)
|
||||
r := &Resolver{Logf: t.Logf}
|
||||
var std net.Dialer
|
||||
dialer := Dialer(std.DialContext, r)
|
||||
t0 := time.Now()
|
||||
@@ -113,6 +113,7 @@ func TestDialCall_uniqueIPs(t *testing.T) {
|
||||
|
||||
func TestResolverAllHostStaticResult(t *testing.T) {
|
||||
r := &Resolver{
|
||||
Logf: t.Logf,
|
||||
SingleHost: "foo.bar",
|
||||
SingleHostStaticResult: []netip.Addr{
|
||||
netip.MustParseAddr("2001:4860:4860::8888"),
|
||||
@@ -185,11 +186,12 @@ func TestShouldTryBootstrap(t *testing.T) {
|
||||
errFailed := errors.New("some failure")
|
||||
|
||||
cacheWithFallback := &Resolver{
|
||||
Logf: t.Logf,
|
||||
LookupIPFallback: func(_ context.Context, _ string) ([]netip.Addr, error) {
|
||||
panic("unimplemented")
|
||||
},
|
||||
}
|
||||
cacheNoFallback := &Resolver{}
|
||||
cacheNoFallback := &Resolver{Logf: t.Logf}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
|
||||
@@ -34,7 +34,7 @@ func (t Tuple) String() string {
|
||||
// The zero value is valid to use.
|
||||
//
|
||||
// It is not safe for concurrent access.
|
||||
type Cache struct {
|
||||
type Cache[Value any] struct {
|
||||
// MaxEntries is the maximum number of cache entries before
|
||||
// an item is evicted. Zero means no limit.
|
||||
MaxEntries int
|
||||
@@ -44,9 +44,9 @@ type Cache struct {
|
||||
}
|
||||
|
||||
// entry is the container/list element type.
|
||||
type entry struct {
|
||||
type entry[Value any] struct {
|
||||
key Tuple
|
||||
value any
|
||||
value Value
|
||||
}
|
||||
|
||||
// Add adds a value to the cache, set or updating its associated
|
||||
@@ -54,17 +54,17 @@ type entry struct {
|
||||
//
|
||||
// If MaxEntries is non-zero and the length of the cache is greater
|
||||
// after any addition, the least recently used value is evicted.
|
||||
func (c *Cache) Add(key Tuple, value any) {
|
||||
func (c *Cache[Value]) Add(key Tuple, value Value) {
|
||||
if c.m == nil {
|
||||
c.m = make(map[Tuple]*list.Element)
|
||||
c.ll = list.New()
|
||||
}
|
||||
if ee, ok := c.m[key]; ok {
|
||||
c.ll.MoveToFront(ee)
|
||||
ee.Value.(*entry).value = value
|
||||
ee.Value.(*entry[Value]).value = value
|
||||
return
|
||||
}
|
||||
ele := c.ll.PushFront(&entry{key, value})
|
||||
ele := c.ll.PushFront(&entry[Value]{key, value})
|
||||
c.m[key] = ele
|
||||
if c.MaxEntries != 0 && c.Len() > c.MaxEntries {
|
||||
c.RemoveOldest()
|
||||
@@ -73,23 +73,23 @@ func (c *Cache) Add(key Tuple, value any) {
|
||||
|
||||
// Get looks up a key's value from the cache, also reporting
|
||||
// whether it was present.
|
||||
func (c *Cache) Get(key Tuple) (value any, ok bool) {
|
||||
func (c *Cache[Value]) Get(key Tuple) (value *Value, ok bool) {
|
||||
if ele, hit := c.m[key]; hit {
|
||||
c.ll.MoveToFront(ele)
|
||||
return ele.Value.(*entry).value, true
|
||||
return &ele.Value.(*entry[Value]).value, true
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// Remove removes the provided key from the cache if it was present.
|
||||
func (c *Cache) Remove(key Tuple) {
|
||||
func (c *Cache[Value]) Remove(key Tuple) {
|
||||
if ele, hit := c.m[key]; hit {
|
||||
c.removeElement(ele)
|
||||
}
|
||||
}
|
||||
|
||||
// RemoveOldest removes the oldest item from the cache, if any.
|
||||
func (c *Cache) RemoveOldest() {
|
||||
func (c *Cache[Value]) RemoveOldest() {
|
||||
if c.ll != nil {
|
||||
if ele := c.ll.Back(); ele != nil {
|
||||
c.removeElement(ele)
|
||||
@@ -97,10 +97,10 @@ func (c *Cache) RemoveOldest() {
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Cache) removeElement(e *list.Element) {
|
||||
func (c *Cache[Value]) removeElement(e *list.Element) {
|
||||
c.ll.Remove(e)
|
||||
delete(c.m, e.Value.(*entry).key)
|
||||
delete(c.m, e.Value.(*entry[Value]).key)
|
||||
}
|
||||
|
||||
// Len returns the number of items in the cache.
|
||||
func (c *Cache) Len() int { return len(c.m) }
|
||||
func (c *Cache[Value]) Len() int { return len(c.m) }
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
)
|
||||
|
||||
func TestCache(t *testing.T) {
|
||||
c := &Cache{MaxEntries: 2}
|
||||
c := &Cache[int]{MaxEntries: 2}
|
||||
|
||||
k1 := Tuple{Src: netip.MustParseAddrPort("1.1.1.1:1"), Dst: netip.MustParseAddrPort("1.1.1.1:1")}
|
||||
k2 := Tuple{Src: netip.MustParseAddrPort("1.1.1.1:1"), Dst: netip.MustParseAddrPort("2.2.2.2:2")}
|
||||
@@ -25,13 +25,13 @@ func TestCache(t *testing.T) {
|
||||
t.Fatalf("Len = %d; want %d", got, want)
|
||||
}
|
||||
}
|
||||
wantVal := func(key Tuple, want any) {
|
||||
wantVal := func(key Tuple, want int) {
|
||||
t.Helper()
|
||||
got, ok := c.Get(key)
|
||||
if !ok {
|
||||
t.Fatalf("Get(%q) failed; want value %v", key, want)
|
||||
}
|
||||
if got != want {
|
||||
if *got != want {
|
||||
t.Fatalf("Get(%q) = %v; want %v", key, got, want)
|
||||
}
|
||||
}
|
||||
@@ -73,7 +73,7 @@ func TestCache(t *testing.T) {
|
||||
if !ok {
|
||||
t.Fatal("missing k3")
|
||||
}
|
||||
if got != 30 {
|
||||
if *got != 30 {
|
||||
t.Fatalf("got = %d; want 30", got)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -20,7 +20,6 @@ import (
|
||||
"golang.org/x/net/route"
|
||||
"golang.org/x/sys/unix"
|
||||
"tailscale.com/net/netaddr"
|
||||
"tailscale.com/syncs"
|
||||
)
|
||||
|
||||
func defaultRoute() (d DefaultRouteDetails, err error) {
|
||||
@@ -41,13 +40,6 @@ func defaultRoute() (d DefaultRouteDetails, err error) {
|
||||
// owns the default route. It returns the first IPv4 or IPv6 default route it
|
||||
// finds (it does not prefer one or the other).
|
||||
func DefaultRouteInterfaceIndex() (int, error) {
|
||||
if f := defaultRouteInterfaceIndexFunc.Load(); f != nil {
|
||||
if ifIndex := f(); ifIndex != 0 {
|
||||
return ifIndex, nil
|
||||
}
|
||||
// Fallthrough if we can't use the alternate implementation.
|
||||
}
|
||||
|
||||
// $ netstat -nr
|
||||
// Routing tables
|
||||
// Internet:
|
||||
@@ -76,22 +68,17 @@ func DefaultRouteInterfaceIndex() (int, error) {
|
||||
continue
|
||||
}
|
||||
if isDefaultGateway(rm) {
|
||||
if delegatedIndex, err := getDelegatedInterface(rm.Index); err == nil && delegatedIndex != 0 {
|
||||
return delegatedIndex, nil
|
||||
} else if err != nil {
|
||||
log.Printf("interfaces_bsd: could not get delegated interface: %v", err)
|
||||
}
|
||||
return rm.Index, nil
|
||||
}
|
||||
}
|
||||
return 0, errors.New("no gateway index found")
|
||||
}
|
||||
|
||||
var defaultRouteInterfaceIndexFunc syncs.AtomicValue[func() int]
|
||||
|
||||
// SetDefaultRouteInterfaceIndexFunc allows an alternate implementation of
|
||||
// DefaultRouteInterfaceIndex to be provided. If none is set, or if f() returns a 0
|
||||
// (indicating an unknown interface index), then the default implementation (that parses
|
||||
// the routing table) will be used.
|
||||
func SetDefaultRouteInterfaceIndexFunc(f func() int) {
|
||||
defaultRouteInterfaceIndexFunc.Store(f)
|
||||
}
|
||||
|
||||
func init() {
|
||||
likelyHomeRouterIP = likelyHomeRouterIPBSDFetchRIB
|
||||
}
|
||||
|
||||
@@ -5,9 +5,15 @@
|
||||
package interfaces
|
||||
|
||||
import (
|
||||
"net"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/net/route"
|
||||
"golang.org/x/sys/unix"
|
||||
"tailscale.com/util/mak"
|
||||
)
|
||||
|
||||
// fetchRoutingTable calls route.FetchRIB, fetching NET_RT_DUMP2.
|
||||
@@ -18,3 +24,73 @@ func fetchRoutingTable() (rib []byte, err error) {
|
||||
func parseRoutingTable(rib []byte) ([]route.Message, error) {
|
||||
return route.ParseRIB(syscall.NET_RT_IFLIST2, rib)
|
||||
}
|
||||
|
||||
var ifNames struct {
|
||||
sync.Mutex
|
||||
m map[int]string // ifindex => name
|
||||
}
|
||||
|
||||
// getDelegatedInterface returns the interface index of the underlying interface
|
||||
// for the given interface index. 0 is returned if the interface does not
|
||||
// delegate.
|
||||
func getDelegatedInterface(ifIndex int) (int, error) {
|
||||
ifNames.Lock()
|
||||
defer ifNames.Unlock()
|
||||
|
||||
// To get the delegated interface, we do what ifconfig does and use the
|
||||
// SIOCGIFDELEGATE ioctl. It operates in term of a ifreq struct, which
|
||||
// has to be populated with a interface name. To avoid having to do a
|
||||
// interface index -> name lookup every time, we cache interface names
|
||||
// (since indexes and names are stable after boot).
|
||||
ifName, ok := ifNames.m[ifIndex]
|
||||
if !ok {
|
||||
iface, err := net.InterfaceByIndex(ifIndex)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
ifName = iface.Name
|
||||
mak.Set(&ifNames.m, ifIndex, ifName)
|
||||
}
|
||||
|
||||
// Only tunnels (like Tailscale itself) have a delegated interface, avoid
|
||||
// the ioctl if we can.
|
||||
if !strings.HasPrefix(ifName, "utun") {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
// We don't cache the result of the ioctl, since the delegated interface can
|
||||
// change, e.g. if the user changes the preferred service order in the
|
||||
// network preference pane.
|
||||
fd, err := unix.Socket(unix.AF_INET, unix.SOCK_DGRAM, 0)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer unix.Close(fd)
|
||||
|
||||
// Match the ifreq struct/union from the bsd/net/if.h header in the Darwin
|
||||
// open source release.
|
||||
var ifr struct {
|
||||
ifr_name [unix.IFNAMSIZ]byte
|
||||
ifr_delegated uint32
|
||||
}
|
||||
copy(ifr.ifr_name[:], ifName)
|
||||
|
||||
// SIOCGIFDELEGATE is not in the Go x/sys package or in the public macOS
|
||||
// <sys/sockio.h> headers. However, it is in the Darwin/xnu open source
|
||||
// release (and is used by ifconfig, see
|
||||
// https://github.com/apple-oss-distributions/network_cmds/blob/6ccdc225ad5aa0d23ea5e7d374956245d2462427/ifconfig.tproj/ifconfig.c#L2183-L2187).
|
||||
// We generate its value by evaluating the `_IOWR('i', 157, struct ifreq)`
|
||||
// macro, which is how it's defined in
|
||||
// https://github.com/apple/darwin-xnu/blob/2ff845c2e033bd0ff64b5b6aa6063a1f8f65aa32/bsd/sys/sockio.h#L264
|
||||
const SIOCGIFDELEGATE = 0xc020699d
|
||||
|
||||
_, _, errno := syscall.Syscall(
|
||||
syscall.SYS_IOCTL,
|
||||
uintptr(fd),
|
||||
uintptr(SIOCGIFDELEGATE),
|
||||
uintptr(unsafe.Pointer(&ifr)))
|
||||
if errno != 0 {
|
||||
return 0, errno
|
||||
}
|
||||
return int(ifr.ifr_delegated), nil
|
||||
}
|
||||
|
||||
@@ -23,3 +23,7 @@ func fetchRoutingTable() (rib []byte, err error) {
|
||||
func parseRoutingTable(rib []byte) ([]route.Message, error) {
|
||||
return route.ParseRIB(syscall.NET_RT_IFLIST, rib)
|
||||
}
|
||||
|
||||
func getDelegatedInterface(ifIndex int) (int, error) {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
@@ -32,6 +32,27 @@ func SetEnabled(on bool) {
|
||||
disabled.Store(!on)
|
||||
}
|
||||
|
||||
var bindToInterfaceByRoute atomic.Bool
|
||||
|
||||
// SetBindToInterfaceByRoute enables or disables whether we use the system's
|
||||
// route information to bind to a particular interface. It is the same as
|
||||
// setting the TS_BIND_TO_INTERFACE_BY_ROUTE.
|
||||
//
|
||||
// Currently, this only changes the behaviour on macOS.
|
||||
func SetBindToInterfaceByRoute(v bool) {
|
||||
bindToInterfaceByRoute.Store(v)
|
||||
}
|
||||
|
||||
var disableBindConnToInterface atomic.Bool
|
||||
|
||||
// SetDisableBindConnToInterface disables the (normal) behavior of binding
|
||||
// connections to the default network interface.
|
||||
//
|
||||
// Currently, this only has an effect on Darwin.
|
||||
func SetDisableBindConnToInterface(v bool) {
|
||||
disableBindConnToInterface.Store(v)
|
||||
}
|
||||
|
||||
// Listener returns a new net.Listener with its Control hook func
|
||||
// initialized as necessary to run in logical network namespace that
|
||||
// doesn't route back into Tailscale.
|
||||
|
||||
@@ -11,10 +11,14 @@ import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"net/netip"
|
||||
"os"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/net/route"
|
||||
"golang.org/x/sys/unix"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/net/interfaces"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
@@ -25,6 +29,10 @@ func control(logf logger.Logf) func(network, address string, c syscall.RawConn)
|
||||
}
|
||||
}
|
||||
|
||||
var bindToInterfaceByRouteEnv = envknob.RegisterBool("TS_BIND_TO_INTERFACE_BY_ROUTE")
|
||||
|
||||
var errInterfaceIndexInvalid = errors.New("interface index invalid")
|
||||
|
||||
// controlLogf marks c as necessary to dial in a separate network namespace.
|
||||
//
|
||||
// It's intentionally the same signature as net.Dialer.Control
|
||||
@@ -34,15 +42,150 @@ func controlLogf(logf logger.Logf, network, address string, c syscall.RawConn) e
|
||||
// Don't bind to an interface for localhost connections.
|
||||
return nil
|
||||
}
|
||||
idx, err := interfaces.DefaultRouteInterfaceIndex()
|
||||
|
||||
if disableBindConnToInterface.Load() {
|
||||
logf("netns_darwin: binding connection to interfaces disabled")
|
||||
return nil
|
||||
}
|
||||
|
||||
idx, err := getInterfaceIndex(logf, address)
|
||||
if err != nil {
|
||||
logf("[unexpected] netns: DefaultRouteInterfaceIndex: %v", err)
|
||||
// callee logged
|
||||
return nil
|
||||
}
|
||||
|
||||
return bindConnToInterface(c, network, address, idx, logf)
|
||||
}
|
||||
|
||||
func getInterfaceIndex(logf logger.Logf, address string) (int, error) {
|
||||
// Helper so we can log errors.
|
||||
defaultIdx := func() (int, error) {
|
||||
idx, err := interfaces.DefaultRouteInterfaceIndex()
|
||||
if err != nil {
|
||||
logf("[unexpected] netns: DefaultRouteInterfaceIndex: %v", err)
|
||||
return -1, err
|
||||
}
|
||||
return idx, nil
|
||||
}
|
||||
|
||||
useRoute := bindToInterfaceByRoute.Load() || bindToInterfaceByRouteEnv()
|
||||
if !useRoute {
|
||||
return defaultIdx()
|
||||
}
|
||||
|
||||
host, _, err := net.SplitHostPort(address)
|
||||
if err != nil {
|
||||
// No port number; use the string directly.
|
||||
host = address
|
||||
}
|
||||
|
||||
// If the address doesn't parse, use the default index.
|
||||
addr, err := netip.ParseAddr(host)
|
||||
if err != nil {
|
||||
logf("[unexpected] netns: error parsing address %q: %v", host, err)
|
||||
return defaultIdx()
|
||||
}
|
||||
|
||||
idx, err := interfaceIndexFor(addr, true /* canRecurse */)
|
||||
if err != nil {
|
||||
logf("netns: error in interfaceIndexFor: %v", err)
|
||||
return defaultIdx()
|
||||
}
|
||||
|
||||
// Verify that we didn't just choose the Tailscale interface;
|
||||
// if so, we fall back to binding from the default.
|
||||
_, tsif, err2 := interfaces.Tailscale()
|
||||
if err2 == nil && tsif.Index == idx {
|
||||
logf("[unexpected] netns: interfaceIndexFor returned Tailscale interface")
|
||||
return defaultIdx()
|
||||
}
|
||||
|
||||
return idx, err
|
||||
}
|
||||
|
||||
// interfaceIndexFor returns the interface index that we should bind to in
|
||||
// order to send traffic to the provided address.
|
||||
func interfaceIndexFor(addr netip.Addr, canRecurse bool) (int, error) {
|
||||
fd, err := unix.Socket(unix.AF_ROUTE, unix.SOCK_RAW, unix.AF_UNSPEC)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("creating AF_ROUTE socket: %w", err)
|
||||
}
|
||||
defer unix.Close(fd)
|
||||
|
||||
var routeAddr route.Addr
|
||||
if addr.Is4() {
|
||||
routeAddr = &route.Inet4Addr{IP: addr.As4()}
|
||||
} else {
|
||||
routeAddr = &route.Inet6Addr{IP: addr.As16()}
|
||||
}
|
||||
|
||||
rm := route.RouteMessage{
|
||||
Version: unix.RTM_VERSION,
|
||||
Type: unix.RTM_GET,
|
||||
Flags: unix.RTF_UP,
|
||||
ID: uintptr(os.Getpid()),
|
||||
Seq: 1,
|
||||
Addrs: []route.Addr{
|
||||
unix.RTAX_DST: routeAddr,
|
||||
},
|
||||
}
|
||||
b, err := rm.Marshal()
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("marshaling RouteMessage: %w", err)
|
||||
}
|
||||
_, err = unix.Write(fd, b)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("writing message: %w", err)
|
||||
}
|
||||
var buf [2048]byte
|
||||
n, err := unix.Read(fd, buf[:])
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("reading message: %w", err)
|
||||
}
|
||||
msgs, err := route.ParseRIB(route.RIBTypeRoute, buf[:n])
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("route.ParseRIB: %w", err)
|
||||
}
|
||||
if len(msgs) == 0 {
|
||||
return 0, fmt.Errorf("no messages")
|
||||
}
|
||||
|
||||
for _, msg := range msgs {
|
||||
rm, ok := msg.(*route.RouteMessage)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if rm.Version < 3 || rm.Version > 5 || rm.Type != unix.RTM_GET {
|
||||
continue
|
||||
}
|
||||
if len(rm.Addrs) < unix.RTAX_GATEWAY {
|
||||
continue
|
||||
}
|
||||
|
||||
switch addr := rm.Addrs[unix.RTAX_GATEWAY].(type) {
|
||||
case *route.LinkAddr:
|
||||
return addr.Index, nil
|
||||
case *route.Inet4Addr:
|
||||
// We can get a gateway IP; recursively call ourselves
|
||||
// (exactly once) to get the link (and thus index) for
|
||||
// the gateway IP.
|
||||
if canRecurse {
|
||||
return interfaceIndexFor(netip.AddrFrom4(addr.IP), false)
|
||||
}
|
||||
case *route.Inet6Addr:
|
||||
// As above.
|
||||
if canRecurse {
|
||||
return interfaceIndexFor(netip.AddrFrom16(addr.IP), false)
|
||||
}
|
||||
default:
|
||||
// Unknown type; skip it
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
return 0, fmt.Errorf("no valid address found")
|
||||
}
|
||||
|
||||
// SetListenConfigInterfaceIndex sets lc.Control such that sockets are bound
|
||||
// to the provided interface index.
|
||||
func SetListenConfigInterfaceIndex(lc *net.ListenConfig, ifIndex int) error {
|
||||
|
||||
85
net/netns/netns_darwin_test.go
Normal file
85
net/netns/netns_darwin_test.go
Normal file
@@ -0,0 +1,85 @@
|
||||
// Copyright (c) 2023 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package netns
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"tailscale.com/net/interfaces"
|
||||
)
|
||||
|
||||
func TestGetInterfaceIndex(t *testing.T) {
|
||||
oldVal := bindToInterfaceByRoute.Load()
|
||||
t.Cleanup(func() { bindToInterfaceByRoute.Store(oldVal) })
|
||||
bindToInterfaceByRoute.Store(true)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
addr string
|
||||
err string
|
||||
}{
|
||||
{
|
||||
name: "IP_and_port",
|
||||
addr: "8.8.8.8:53",
|
||||
},
|
||||
{
|
||||
name: "bare_ip",
|
||||
addr: "8.8.8.8",
|
||||
},
|
||||
{
|
||||
name: "invalid",
|
||||
addr: "!!!!!",
|
||||
},
|
||||
}
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
idx, err := getInterfaceIndex(t.Logf, tc.addr)
|
||||
if err != nil {
|
||||
if tc.err == "" {
|
||||
t.Fatalf("got unexpected error: %v", err)
|
||||
}
|
||||
if errstr := err.Error(); errstr != tc.err {
|
||||
t.Errorf("expected error %q, got %q", errstr, tc.err)
|
||||
}
|
||||
} else {
|
||||
t.Logf("getInterfaceIndex(%q) = %d", tc.addr, idx)
|
||||
if tc.err != "" {
|
||||
t.Fatalf("wanted error %q", tc.err)
|
||||
}
|
||||
if idx < 0 {
|
||||
t.Fatalf("got invalid index %d", idx)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("NoTailscale", func(t *testing.T) {
|
||||
_, tsif, err := interfaces.Tailscale()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if tsif == nil {
|
||||
t.Skip("no tailscale interface on this machine")
|
||||
}
|
||||
|
||||
defaultIdx, err := interfaces.DefaultRouteInterfaceIndex()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
idx, err := getInterfaceIndex(t.Logf, "100.100.100.100:53")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
t.Logf("tailscaleIdx=%d defaultIdx=%d idx=%d", tsif.Index, defaultIdx, idx)
|
||||
|
||||
if idx == tsif.Index {
|
||||
t.Fatalf("got idx=%d; wanted not Tailscale interface", idx)
|
||||
} else if idx != defaultIdx {
|
||||
t.Fatalf("got idx=%d, want %d", idx, defaultIdx)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -20,6 +20,13 @@ import (
|
||||
// OSMetadata includes any additional OS-specific information that may be
|
||||
// obtained during the retrieval of a given Entry.
|
||||
type OSMetadata interface {
|
||||
// GetModule returns the entry's module name.
|
||||
//
|
||||
// It returns ("", nil) if no entry is found. As of 2023-01-27, any returned
|
||||
// error is silently discarded by its sole caller in portlist_windows.go and
|
||||
// treated equivalently as returning ("", nil), but this may change in the
|
||||
// future. An error should only be returned in casees that are worthy of
|
||||
// being logged at least.
|
||||
GetModule() (string, error)
|
||||
}
|
||||
|
||||
@@ -224,6 +231,13 @@ type moduleInfoConstraint interface {
|
||||
_MIB_TCPROW_OWNER_MODULE | _MIB_TCP6ROW_OWNER_MODULE
|
||||
}
|
||||
|
||||
// moduleInfo implements OSMetadata.GetModule. It calls
|
||||
// getOwnerModuleFromTcpEntry or getOwnerModuleFromTcp6Entry.
|
||||
//
|
||||
// See
|
||||
// https://learn.microsoft.com/en-us/windows/win32/api/iphlpapi/nf-iphlpapi-getownermodulefromtcpentry
|
||||
//
|
||||
// It may return "", nil indicating a successful call but with empty data.
|
||||
func moduleInfo[entryType moduleInfoConstraint](entry *entryType, proc *windows.LazyProc) (string, error) {
|
||||
var buf []byte
|
||||
var desiredLen uint32
|
||||
@@ -240,22 +254,36 @@ func moduleInfo[entryType moduleInfoConstraint](entry *entryType, proc *windows.
|
||||
if err == windows.ERROR_SUCCESS {
|
||||
break
|
||||
}
|
||||
if err == windows.ERROR_NOT_FOUND {
|
||||
return "", nil
|
||||
}
|
||||
if err != windows.ERROR_INSUFFICIENT_BUFFER {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if desiredLen > 1<<20 {
|
||||
// Sanity check before allocating too much.
|
||||
return "", nil
|
||||
}
|
||||
buf = make([]byte, desiredLen)
|
||||
addr = unsafe.Pointer(&buf[0])
|
||||
}
|
||||
|
||||
if addr == nil {
|
||||
// GetOwnerModuleFromTcp*Entry can apparently return ERROR_SUCCESS
|
||||
// (NO_ERROR) on the first call without the usual first
|
||||
// ERROR_INSUFFICIENT_BUFFER result. Windows said success, so interpret
|
||||
// that was sucessfully not having data.
|
||||
return "", nil
|
||||
}
|
||||
basicInfo := (*_TCPIP_OWNER_MODULE_BASIC_INFO)(addr)
|
||||
return windows.UTF16PtrToString(basicInfo.moduleName), nil
|
||||
}
|
||||
|
||||
// GetModule implements OSMetadata.
|
||||
func (m *_MIB_TCPROW_OWNER_MODULE) GetModule() (string, error) {
|
||||
return moduleInfo(m, getOwnerModuleFromTcpEntry)
|
||||
}
|
||||
|
||||
// GetModule implements OSMetadata.
|
||||
func (m *_MIB_TCP6ROW_OWNER_MODULE) GetModule() (string, error) {
|
||||
return moduleInfo(m, getOwnerModuleFromTcp6Entry)
|
||||
}
|
||||
|
||||
@@ -211,9 +211,11 @@ func (q *Parsed) decode4(b []byte) {
|
||||
// Inter-tailscale messages.
|
||||
q.dataofs = q.subofs
|
||||
return
|
||||
default:
|
||||
case ipproto.Fragment:
|
||||
// An IPProto value of 0xff (our Fragment constant for internal use)
|
||||
// should never actually be used in the wild; if we see it,
|
||||
// something's suspicious and we map it back to zero (unknown).
|
||||
q.IPProto = unknown
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// This is a fragment other than the first one.
|
||||
@@ -312,7 +314,10 @@ func (q *Parsed) decode6(b []byte) {
|
||||
// Inter-tailscale messages.
|
||||
q.dataofs = q.subofs
|
||||
return
|
||||
default:
|
||||
case ipproto.Fragment:
|
||||
// An IPProto value of 0xff (our Fragment constant for internal use)
|
||||
// should never actually be used in the wild; if we see it,
|
||||
// something's suspicious and we map it back to zero (unknown).
|
||||
q.IPProto = unknown
|
||||
return
|
||||
}
|
||||
|
||||
@@ -6,12 +6,16 @@ package packet
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"net/netip"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"unicode"
|
||||
|
||||
"tailscale.com/tstest"
|
||||
"tailscale.com/types/ipproto"
|
||||
"tailscale.com/util/must"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -440,6 +444,17 @@ func TestParsedString(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// mustHexDecode is like hex.DecodeString, but panics on error
|
||||
// and ignores whitespace in s.
|
||||
func mustHexDecode(s string) []byte {
|
||||
return must.Get(hex.DecodeString(strings.Map(func(r rune) rune {
|
||||
if unicode.IsSpace(r) {
|
||||
return -1
|
||||
}
|
||||
return r
|
||||
}, s)))
|
||||
}
|
||||
|
||||
func TestDecode(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -459,6 +474,29 @@ func TestDecode(t *testing.T) {
|
||||
{"ipv4_sctp", sctpBuffer, sctpDecode},
|
||||
{"ipv4_frag", tcp4MediumFragmentBuffer, tcp4MediumFragmentDecode},
|
||||
{"ipv4_fragtooshort", tcp4ShortFragmentBuffer, tcp4ShortFragmentDecode},
|
||||
|
||||
{"ip97", mustHexDecode("4500 0019 d186 4000 4061 751d 644a 4603 6449 e549 6865 6c6c 6f"), Parsed{
|
||||
IPVersion: 4,
|
||||
IPProto: 97,
|
||||
Src: netip.MustParseAddrPort("100.74.70.3:0"),
|
||||
Dst: netip.MustParseAddrPort("100.73.229.73:0"),
|
||||
b: mustHexDecode("4500 0019 d186 4000 4061 751d 644a 4603 6449 e549 6865 6c6c 6f"),
|
||||
length: 25,
|
||||
subofs: 20,
|
||||
}},
|
||||
|
||||
// This packet purports to use protocol 0xFF, which is verboten and
|
||||
// used internally as a sentinel value for fragments. So test that
|
||||
// we map packets using 0xFF to Unknown (0) instead.
|
||||
{"bogus_proto_ff", mustHexDecode("4500 0019 d186 4000 40" + "FF" /* bogus FF */ + " 751d 644a 4603 6449 e549 6865 6c6c 6f"), Parsed{
|
||||
IPVersion: 4,
|
||||
IPProto: ipproto.Unknown, // 0, not bogus 0xFF
|
||||
Src: netip.MustParseAddrPort("100.74.70.3:0"),
|
||||
Dst: netip.MustParseAddrPort("100.73.229.73:0"),
|
||||
b: mustHexDecode("4500 0019 d186 4000 40" + "FF" /* bogus FF */ + " 751d 644a 4603 6449 e549 6865 6c6c 6f"),
|
||||
length: 25,
|
||||
subofs: 20,
|
||||
}},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
||||
@@ -100,11 +100,11 @@ type mapping interface {
|
||||
// but can be called asynchronously. Release should be idempotent, and thus even if called
|
||||
// multiple times should not cause additional side-effects.
|
||||
Release(context.Context)
|
||||
// goodUntil will return the lease time that the mapping is valid for.
|
||||
// GoodUntil will return the lease time that the mapping is valid for.
|
||||
GoodUntil() time.Time
|
||||
// renewAfter returns the earliest time that the mapping should be renewed.
|
||||
// RenewAfter returns the earliest time that the mapping should be renewed.
|
||||
RenewAfter() time.Time
|
||||
// externalIPPort indicates what port the mapping can be reached from on the outside.
|
||||
// External indicates what port the mapping can be reached from on the outside.
|
||||
External() netip.AddrPort
|
||||
}
|
||||
|
||||
@@ -797,7 +797,11 @@ func (c *Client) Probe(ctx context.Context) (res ProbeResult, err error) {
|
||||
switch port {
|
||||
case c.upnpPort():
|
||||
metricUPnPResponse.Add(1)
|
||||
if ip == gw && mem.Contains(mem.B(buf[:n]), mem.S(":InternetGatewayDevice:")) {
|
||||
if mem.Contains(mem.B(buf[:n]), mem.S(":InternetGatewayDevice:")) {
|
||||
if ip != gw {
|
||||
// https://github.com/tailscale/tailscale/issues/5502
|
||||
c.logf("UPnP discovery response from %v, but gateway IP is %v", ip, gw)
|
||||
}
|
||||
meta, err := parseUPnPDiscoResponse(buf[:n])
|
||||
if err != nil {
|
||||
metricUPnPParseErr.Add(1)
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
@@ -174,8 +175,10 @@ func getUPnPClient(ctx context.Context, logf logger.Logf, gw netip.Addr, meta uP
|
||||
return nil, fmt.Errorf("unexpected host %q in %q", u.Host, meta.Location)
|
||||
}
|
||||
if ipp.Addr() != gw {
|
||||
return nil, fmt.Errorf("UPnP discovered root %q does not match gateway IP %v; ignoring UPnP",
|
||||
// https://github.com/tailscale/tailscale/issues/5502
|
||||
logf("UPnP discovered root %q does not match gateway IP %v; repointing at gateway which is assumed to be floating",
|
||||
meta.Location, gw)
|
||||
u.Host = net.JoinHostPort(gw.String(), u.Port())
|
||||
}
|
||||
|
||||
// We're fetching a smallish XML document over plain HTTP
|
||||
|
||||
@@ -43,7 +43,7 @@ func TestFallbackRootWorks(t *testing.T) {
|
||||
crtFile := filepath.Join(d, "tlsdial.test.crt")
|
||||
keyFile := filepath.Join(d, "tlsdial.test.key")
|
||||
caFile := filepath.Join(d, "rootCA.pem")
|
||||
cmd := exec.Command(filepath.Join(runtime.GOROOT(), "bin", "go"),
|
||||
cmd := exec.Command("go",
|
||||
"run", "filippo.io/mkcert",
|
||||
"--cert-file="+crtFile,
|
||||
"--key-file="+keyFile,
|
||||
|
||||
@@ -8,11 +8,13 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"unicode"
|
||||
"unsafe"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
@@ -30,6 +32,7 @@ import (
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/netlogtype"
|
||||
"tailscale.com/util/must"
|
||||
"tailscale.com/wgengine/filter"
|
||||
)
|
||||
|
||||
@@ -293,6 +296,17 @@ func TestWriteAndInject(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// mustHexDecode is like hex.DecodeString, but panics on error
|
||||
// and ignores whitespace in s.
|
||||
func mustHexDecode(s string) []byte {
|
||||
return must.Get(hex.DecodeString(strings.Map(func(r rune) rune {
|
||||
if unicode.IsSpace(r) {
|
||||
return -1
|
||||
}
|
||||
return r
|
||||
}, s)))
|
||||
}
|
||||
|
||||
func TestFilter(t *testing.T) {
|
||||
chtun, tun := newChannelTUN(t.Logf, true)
|
||||
defer tun.Close()
|
||||
@@ -310,8 +324,9 @@ func TestFilter(t *testing.T) {
|
||||
drop bool
|
||||
data []byte
|
||||
}{
|
||||
{"junk_in", in, true, []byte("\x45not a valid IPv4 packet")},
|
||||
{"junk_out", out, true, []byte("\x45not a valid IPv4 packet")},
|
||||
{"short_in", in, true, []byte("\x45xxx")},
|
||||
{"short_out", out, true, []byte("\x45xxx")},
|
||||
{"ip97_out", out, false, mustHexDecode("4500 0019 d186 4000 4061 751d 644a 4603 6449 e549 6865 6c6c 6f")},
|
||||
{"bad_port_in", in, true, udp4("5.6.7.8", "1.2.3.4", 22, 22)},
|
||||
{"bad_port_out", out, false, udp4("1.2.3.4", "5.6.7.8", 22, 22)},
|
||||
{"bad_ip_in", in, true, udp4("8.1.1.1", "1.2.3.4", 89, 89)},
|
||||
@@ -386,9 +401,11 @@ func TestFilter(t *testing.T) {
|
||||
|
||||
got, _ := stats.TestExtract()
|
||||
want := map[netlogtype.Connection]netlogtype.Counts{}
|
||||
var wasUDP bool
|
||||
if !tt.drop {
|
||||
var p packet.Parsed
|
||||
p.Decode(tt.data)
|
||||
wasUDP = p.IPProto == ipproto.UDP
|
||||
switch tt.dir {
|
||||
case in:
|
||||
conn := netlogtype.Connection{Proto: ipproto.UDP, Src: p.Dst, Dst: p.Src}
|
||||
@@ -398,8 +415,10 @@ func TestFilter(t *testing.T) {
|
||||
want[conn] = netlogtype.Counts{TxPackets: 1, TxBytes: uint64(len(tt.data))}
|
||||
}
|
||||
}
|
||||
if diff := cmp.Diff(got, want, cmpopts.EquateEmpty()); diff != "" {
|
||||
t.Errorf("stats.TestExtract (-got +want):\n%s", diff)
|
||||
if wasUDP {
|
||||
if diff := cmp.Diff(got, want, cmpopts.EquateEmpty()); diff != "" {
|
||||
t.Errorf("stats.TestExtract (-got +want):\n%s", diff)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -409,7 +428,7 @@ func TestAllocs(t *testing.T) {
|
||||
ftun, tun := newFakeTUN(t.Logf, false)
|
||||
defer tun.Close()
|
||||
|
||||
buf := [][]byte{[]byte{0x00}}
|
||||
buf := [][]byte{{0x00}}
|
||||
err := tstest.MinAllocsPerRun(t, 0, func() {
|
||||
_, err := ftun.Write(buf, 0)
|
||||
if err != nil {
|
||||
@@ -525,6 +544,7 @@ func TestPeerAPIBypass(t *testing.T) {
|
||||
p.Decode(tt.pkt)
|
||||
tt.w.SetFilter(tt.filter)
|
||||
tt.w.disableTSMPRejected = true
|
||||
tt.w.logf = t.Logf
|
||||
if got := tt.w.filterIn(p); got != tt.want {
|
||||
t.Errorf("got = %v; want %v", got, tt.want)
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
check_file() {
|
||||
got=$1
|
||||
|
||||
for year in `seq 2019 2022`; do
|
||||
for year in `seq 2019 2023`; do
|
||||
want=$(cat <<EOF
|
||||
// Copyright (c) $year Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
@@ -37,6 +37,9 @@ for file in $(find $1 -name '*.go' -not -path '*/.git/*'); do
|
||||
;;
|
||||
$1/wgengine/router/ifconfig_windows.go)
|
||||
# WireGuard copyright.
|
||||
;;
|
||||
$1/cmd/tailscale/cli/authenticode_windows.go)
|
||||
# WireGuard copyright.
|
||||
;;
|
||||
*_string.go)
|
||||
# Generated file from go:generate stringer
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user