Compare commits
137 Commits
tom/iptabl
...
crawshaw/a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
31d7ab09a6 | ||
|
|
3555a49518 | ||
|
|
539c073cf0 | ||
|
|
a315336287 | ||
|
|
d05dd41bc1 | ||
|
|
9a264dac01 | ||
|
|
b2855cfd86 | ||
|
|
4ec6d41682 | ||
|
|
a1a43ed266 | ||
|
|
db863bf00f | ||
|
|
f9120eee57 | ||
|
|
49bae7fd5c | ||
|
|
5363a90272 | ||
|
|
b49eb7d55c | ||
|
|
1b4e4cc1e8 | ||
|
|
79755d3ce5 | ||
|
|
1b9ed9f365 | ||
|
|
e7519adc18 | ||
|
|
e24de8a617 | ||
|
|
c070d39287 | ||
|
|
51d488673a | ||
|
|
680f8d9793 | ||
|
|
614a24763b | ||
|
|
0475ed4a7e | ||
|
|
7df85c6031 | ||
|
|
718914b697 | ||
|
|
529e893f70 | ||
|
|
8a187159b2 | ||
|
|
b2994568fe | ||
|
|
f172fc42f7 | ||
|
|
8fe04b035c | ||
|
|
d29ec4d7a4 | ||
|
|
4de1601ef4 | ||
|
|
91b5c50b43 | ||
|
|
ecf6cdd830 | ||
|
|
f16b77de5d | ||
|
|
c8a3d02989 | ||
|
|
6d76764f37 | ||
|
|
b84ec521bf | ||
|
|
82f5f438e0 | ||
|
|
92ad56ddcb | ||
|
|
84e8f25c21 | ||
|
|
dd045a3767 | ||
|
|
a73c423c8a | ||
|
|
3af0d4d0f2 | ||
|
|
c321363d2c | ||
|
|
24ebf161e8 | ||
|
|
a37ee8483f | ||
|
|
7714261566 | ||
|
|
8602061f32 | ||
|
|
73db56af52 | ||
|
|
01ebef0f4f | ||
|
|
62bc1052a2 | ||
|
|
2243dbccb7 | ||
|
|
b1bd96f114 | ||
|
|
fde20f3403 | ||
|
|
7ffd2fe005 | ||
|
|
2934c5114c | ||
|
|
bdf3d2a63f | ||
|
|
c5ce355756 | ||
|
|
7e0ffc17fd | ||
|
|
17348915fa | ||
|
|
1841d0bf98 | ||
|
|
5c69961a57 | ||
|
|
e5636997c5 | ||
|
|
445c8a4671 | ||
|
|
d7c0410ea8 | ||
|
|
4102a687e3 | ||
|
|
5fc8843c4c | ||
|
|
8343b243e7 | ||
|
|
a7efc7bd17 | ||
|
|
d4811f11a0 | ||
|
|
e73657d7aa | ||
|
|
bb7be74756 | ||
|
|
c581ce7b00 | ||
|
|
420d841292 | ||
|
|
58ffe928af | ||
|
|
ab591906c8 | ||
|
|
9214b293e3 | ||
|
|
44f13d32d7 | ||
|
|
18159431ab | ||
|
|
a5fab23e8f | ||
|
|
4b996ad5e3 | ||
|
|
ebd1637e50 | ||
|
|
e97d5634bf | ||
|
|
f981b1d9da | ||
|
|
9bdf0cd8cd | ||
|
|
7686446c60 | ||
|
|
b1867457a6 | ||
|
|
e3beb4429f | ||
|
|
8a0cb4ef20 | ||
|
|
fb4e23506f | ||
|
|
6d04184325 | ||
|
|
8c72aabbdf | ||
|
|
f7cb535693 | ||
|
|
146f51ce76 | ||
|
|
c66e15772f | ||
|
|
e1bdbfe710 | ||
|
|
acc7baac6d | ||
|
|
91794f6498 | ||
|
|
2c447de6cc | ||
|
|
021bedfb89 | ||
|
|
d988c9f098 | ||
|
|
0607832397 | ||
|
|
565dbc599a | ||
|
|
aadf63da1d | ||
|
|
d5781f61a9 | ||
|
|
a7a0baf6b9 | ||
|
|
e9b98dd2e1 | ||
|
|
b9b0bf65a0 | ||
|
|
c6162c2a94 | ||
|
|
aa5e494aba | ||
|
|
ff13c66f55 | ||
|
|
ed248b04a7 | ||
|
|
8158dd2edc | ||
|
|
6632504f45 | ||
|
|
054ef4de56 | ||
|
|
d045462dfb | ||
|
|
d8eb111ac8 | ||
|
|
832031d54b | ||
|
|
42f1d92ae0 | ||
|
|
41bb47de0e | ||
|
|
3562b5bdfa | ||
|
|
5c42990c2f | ||
|
|
65c24b6334 | ||
|
|
4bda41e701 | ||
|
|
9b71008ef2 | ||
|
|
5623ef0271 | ||
|
|
486eecc063 | ||
|
|
b830c9975f | ||
|
|
4a82b317b7 | ||
|
|
f0347e841f | ||
|
|
027111fb5a | ||
|
|
1ce0e558a7 | ||
|
|
74674b110d | ||
|
|
33ee2c058e | ||
|
|
d34dd43562 |
2
.github/workflows/cifuzz.yml
vendored
2
.github/workflows/cifuzz.yml
vendored
@@ -2,7 +2,7 @@ name: CIFuzz
|
||||
on: [pull_request]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
|
||||
54
.github/workflows/cross-android.yml
vendored
Normal file
54
.github/workflows/cross-android.yml
vendored
Normal file
@@ -0,0 +1,54 @@
|
||||
name: Android-Cross
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- '*'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-$${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
if: "!contains(github.event.head_commit.message, '[ci skip]')"
|
||||
|
||||
steps:
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
id: go
|
||||
|
||||
- name: Android smoke build
|
||||
# Super minimal Android build that doesn't even use CGO and doesn't build everything that's needed
|
||||
# and is only arm64. But it's a smoke build: it's not meant to catch everything. But it'll catch
|
||||
# some Android breakages early.
|
||||
# TODO(bradfitz): better; see https://github.com/tailscale/tailscale/issues/4482
|
||||
env:
|
||||
GOOS: android
|
||||
GOARCH: arm64
|
||||
run: go install ./net/netns ./ipn/ipnlocal ./wgengine/magicsock/ ./wgengine/ ./wgengine/router/ ./wgengine/netstack ./util/dnsname/ ./ipn/ ./net/interfaces ./wgengine/router/ ./tailcfg/ ./types/logger/ ./net/dns ./hostinfo ./version
|
||||
|
||||
- uses: k0kubun/action-slack@v2.0.0
|
||||
with:
|
||||
payload: |
|
||||
{
|
||||
"attachments": [{
|
||||
"text": "${{ job.status }}: ${{ github.workflow }} <https://github.com/${{ github.repository }}/commit/${{ github.sha }}/checks|${{ env.COMMIT_DATE }} #${{ env.COMMIT_NUMBER_OF_DAY }}> " +
|
||||
"(<https://github.com/${{ github.repository }}/commit/${{ github.sha }}|" + "${{ github.sha }}".substring(0, 10) + ">) " +
|
||||
"of ${{ github.repository }}@" + "${{ github.ref }}".split('/').reverse()[0] + " by ${{ github.event.head_commit.committer.name }}",
|
||||
"color": "danger"
|
||||
}]
|
||||
}
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
|
||||
if: failure() && github.event_name == 'push'
|
||||
@@ -32,7 +32,7 @@
|
||||
# $ docker exec tailscaled tailscale status
|
||||
|
||||
|
||||
FROM golang:1.18-alpine AS build-env
|
||||
FROM golang:1.19-alpine AS build-env
|
||||
|
||||
WORKDIR /go/src/tailscale
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
1.31.0
|
||||
1.33.0
|
||||
|
||||
7
api.md
7
api.md
@@ -355,6 +355,13 @@ GET /api/v2/tailnet/alice@gmail.com/...
|
||||
curl https://api.tailscale.com/api/v2/tailnet/alice@gmail.com/...
|
||||
```
|
||||
|
||||
Alternatively, you can specify the value "-" to refer to the default tailnet of
|
||||
the authenticated user making the API call. For example:
|
||||
```
|
||||
GET /api/v2/tailnet/-/...
|
||||
curl https://api.tailscale.com/api/v2/tailnet/-/...
|
||||
```
|
||||
|
||||
Tailnets are a top-level resource. ACL is an example of a resource that is tied to a top-level tailnet.
|
||||
|
||||
For more information on Tailscale networks/tailnets, click [here](https://tailscale.com/kb/1064/invite-team-members).
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
package atomicfile // import "tailscale.com/atomicfile"
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
@@ -18,7 +17,7 @@ import (
|
||||
// WriteFile writes data to filename+some suffix, then renames it
|
||||
// into filename. The perm argument is ignored on Windows.
|
||||
func WriteFile(filename string, data []byte, perm os.FileMode) (err error) {
|
||||
f, err := ioutil.TempFile(filepath.Dir(filename), filepath.Base(filename)+".tmp")
|
||||
f, err := os.CreateTemp(filepath.Dir(filename), filepath.Base(filename)+".tmp")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -24,11 +24,17 @@ func New(socket string) (*BIRDClient, error) {
|
||||
return newWithTimeout(socket, responseTimeout)
|
||||
}
|
||||
|
||||
func newWithTimeout(socket string, timeout time.Duration) (*BIRDClient, error) {
|
||||
func newWithTimeout(socket string, timeout time.Duration) (_ *BIRDClient, err error) {
|
||||
conn, err := net.Dial("unix", socket)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to connect to BIRD: %w", err)
|
||||
}
|
||||
defer func() {
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
b := &BIRDClient{
|
||||
socket: socket,
|
||||
conn: conn,
|
||||
|
||||
@@ -106,10 +106,10 @@ func TestChirp(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := c.EnableProtocol("rando"); err == nil {
|
||||
t.Fatalf("enabling %q succeded", "rando")
|
||||
t.Fatalf("enabling %q succeeded", "rando")
|
||||
}
|
||||
if err := c.DisableProtocol("rando"); err == nil {
|
||||
t.Fatalf("disabling %q succeded", "rando")
|
||||
t.Fatalf("disabling %q succeeded", "rando")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -459,7 +459,7 @@ func (c *Client) ValidateACLJSON(ctx context.Context, source, dest string) (test
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("control api responsed with %d status code", resp.StatusCode)
|
||||
return nil, fmt.Errorf("control api responded with %d status code", resp.StatusCode)
|
||||
}
|
||||
|
||||
// The test ran without fail
|
||||
|
||||
@@ -15,7 +15,6 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptrace"
|
||||
@@ -137,7 +136,7 @@ func (lc *LocalClient) doLocalRequestNiceError(req *http.Request) (*http.Respons
|
||||
onVersionMismatch(ipn.IPCVersion(), server)
|
||||
}
|
||||
if res.StatusCode == 403 {
|
||||
all, _ := ioutil.ReadAll(res.Body)
|
||||
all, _ := io.ReadAll(res.Body)
|
||||
return nil, &AccessDeniedError{errors.New(errorMessageFromBody(all))}
|
||||
}
|
||||
return res, nil
|
||||
@@ -207,7 +206,7 @@ func (lc *LocalClient) send(ctx context.Context, method, path string, wantStatus
|
||||
return nil, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
slurp, err := ioutil.ReadAll(res.Body)
|
||||
slurp, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -268,15 +267,47 @@ func (lc *LocalClient) Profile(ctx context.Context, pprofType string, sec int) (
|
||||
return lc.get200(ctx, fmt.Sprintf("/localapi/v0/profile?name=%s&seconds=%v", url.QueryEscape(pprofType), secArg))
|
||||
}
|
||||
|
||||
// BugReport logs and returns a log marker that can be shared by the user with support.
|
||||
func (lc *LocalClient) BugReport(ctx context.Context, note string) (string, error) {
|
||||
body, err := lc.send(ctx, "POST", "/localapi/v0/bugreport?note="+url.QueryEscape(note), 200, nil)
|
||||
// BugReportOpts contains options to pass to the Tailscale daemon when
|
||||
// generating a bug report.
|
||||
type BugReportOpts struct {
|
||||
// Note contains an optional user-provided note to add to the logs.
|
||||
Note string
|
||||
|
||||
// Diagnose specifies whether to print additional diagnostic information to
|
||||
// the logs when generating this bugreport.
|
||||
Diagnose bool
|
||||
}
|
||||
|
||||
// BugReportWithOpts logs and returns a log marker that can be shared by the
|
||||
// user with support.
|
||||
//
|
||||
// The opts type specifies options to pass to the Tailscale daemon when
|
||||
// generating this bug report.
|
||||
func (lc *LocalClient) BugReportWithOpts(ctx context.Context, opts BugReportOpts) (string, error) {
|
||||
var qparams url.Values
|
||||
if opts.Note != "" {
|
||||
qparams.Set("note", opts.Note)
|
||||
}
|
||||
if opts.Diagnose {
|
||||
qparams.Set("diagnose", "true")
|
||||
}
|
||||
|
||||
uri := fmt.Sprintf("/localapi/v0/bugreport?%s", qparams.Encode())
|
||||
body, err := lc.send(ctx, "POST", uri, 200, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return strings.TrimSpace(string(body)), nil
|
||||
}
|
||||
|
||||
// BugReport logs and returns a log marker that can be shared by the user with support.
|
||||
//
|
||||
// This is the same as calling BugReportWithOpts and only specifying the Note
|
||||
// field.
|
||||
func (lc *LocalClient) BugReport(ctx context.Context, note string) (string, error) {
|
||||
return lc.BugReportWithOpts(ctx, BugReportOpts{Note: note})
|
||||
}
|
||||
|
||||
// DebugAction invokes a debug action, such as "rebind" or "restun".
|
||||
// These are development tools and subject to change or removal over time.
|
||||
func (lc *LocalClient) DebugAction(ctx context.Context, action string) error {
|
||||
@@ -287,6 +318,28 @@ func (lc *LocalClient) DebugAction(ctx context.Context, action string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetComponentDebugLogging sets component's debug logging enabled for
|
||||
// the provided duration. If the duration is in the past, the debug logging
|
||||
// is disabled.
|
||||
func (lc *LocalClient) SetComponentDebugLogging(ctx context.Context, component string, d time.Duration) error {
|
||||
body, err := lc.send(ctx, "POST",
|
||||
fmt.Sprintf("/localapi/v0/component-debug-logging?component=%s&secs=%d",
|
||||
url.QueryEscape(component), int64(d.Seconds())), 200, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error %w: %s", err, body)
|
||||
}
|
||||
var res struct {
|
||||
Error string
|
||||
}
|
||||
if err := json.Unmarshal(body, &res); err != nil {
|
||||
return err
|
||||
}
|
||||
if res.Error != "" {
|
||||
return errors.New(res.Error)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Status returns the Tailscale daemon's status.
|
||||
func Status(ctx context.Context) (*ipnstate.Status, error) {
|
||||
return defaultLocalClient.Status(ctx)
|
||||
@@ -365,7 +418,7 @@ func (lc *LocalClient) GetWaitingFile(ctx context.Context, baseName string) (rc
|
||||
return nil, 0, fmt.Errorf("unexpected chunking")
|
||||
}
|
||||
if res.StatusCode != 200 {
|
||||
body, _ := ioutil.ReadAll(res.Body)
|
||||
body, _ := io.ReadAll(res.Body)
|
||||
res.Body.Close()
|
||||
return nil, 0, fmt.Errorf("HTTP %s: %s", res.Status, body)
|
||||
}
|
||||
@@ -643,14 +696,14 @@ func (lc *LocalClient) GetCertificate(hi *tls.ClientHelloInfo) (*tls.Certificate
|
||||
return &cert, nil
|
||||
}
|
||||
|
||||
// ExpandSNIName expands bare label name into the the most likely actual TLS cert name.
|
||||
// ExpandSNIName expands bare label name into the most likely actual TLS cert name.
|
||||
//
|
||||
// Deprecated: use LocalClient.ExpandSNIName.
|
||||
func ExpandSNIName(ctx context.Context, name string) (fqdn string, ok bool) {
|
||||
return defaultLocalClient.ExpandSNIName(ctx, name)
|
||||
}
|
||||
|
||||
// ExpandSNIName expands bare label name into the the most likely actual TLS cert name.
|
||||
// ExpandSNIName expands bare label name into the most likely actual TLS cert name.
|
||||
func (lc *LocalClient) ExpandSNIName(ctx context.Context, name string) (fqdn string, ok bool) {
|
||||
st, err := lc.StatusWithoutPeers(ctx)
|
||||
if err != nil {
|
||||
@@ -717,6 +770,30 @@ func (lc *LocalClient) NetworkLockInit(ctx context.Context, keys []tka.Key) (*ip
|
||||
return pr, nil
|
||||
}
|
||||
|
||||
// NetworkLockModify adds and/or removes key(s) to the tailnet key authority.
|
||||
func (lc *LocalClient) NetworkLockModify(ctx context.Context, addKeys, removeKeys []tka.Key) (*ipnstate.NetworkLockStatus, error) {
|
||||
var b bytes.Buffer
|
||||
type modifyRequest struct {
|
||||
AddKeys []tka.Key
|
||||
RemoveKeys []tka.Key
|
||||
}
|
||||
|
||||
if err := json.NewEncoder(&b).Encode(modifyRequest{AddKeys: addKeys, RemoveKeys: removeKeys}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
body, err := lc.send(ctx, "POST", "/localapi/v0/tka/modify", 200, &b)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error: %w", err)
|
||||
}
|
||||
|
||||
pr := new(ipnstate.NetworkLockStatus)
|
||||
if err := json.Unmarshal(body, pr); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return pr, nil
|
||||
}
|
||||
|
||||
// tailscaledConnectHint gives a little thing about why tailscaled (or
|
||||
// platform equivalent) is not answering localapi connections.
|
||||
//
|
||||
|
||||
@@ -17,7 +17,6 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
@@ -116,7 +115,7 @@ func (c *Client) Do(req *http.Request) (*http.Response, error) {
|
||||
return c.httpClient().Do(req)
|
||||
}
|
||||
|
||||
// sendRequest add the authenication key to the request and sends it. It
|
||||
// sendRequest add the authentication key to the request and sends it. It
|
||||
// receives the response and reads up to 10MB of it.
|
||||
func (c *Client) sendRequest(req *http.Request) ([]byte, *http.Response, error) {
|
||||
if !I_Acknowledge_This_API_Is_Unstable {
|
||||
@@ -131,7 +130,7 @@ func (c *Client) sendRequest(req *http.Request) ([]byte, *http.Response, error)
|
||||
|
||||
// Read response. Limit the response to 10MB.
|
||||
body := io.LimitReader(resp.Body, maxReadSize+1)
|
||||
b, err := ioutil.ReadAll(body)
|
||||
b, err := io.ReadAll(body)
|
||||
if len(b) > maxReadSize {
|
||||
err = errors.New("API response too large")
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
|
||||
tailscale.com/safesocket from tailscale.com/client/tailscale
|
||||
tailscale.com/syncs from tailscale.com/cmd/derper+
|
||||
tailscale.com/tailcfg from tailscale.com/client/tailscale+
|
||||
tailscale.com/tka from tailscale.com/client/tailscale
|
||||
tailscale.com/tka from tailscale.com/client/tailscale+
|
||||
W tailscale.com/tsconst from tailscale.com/net/interfaces
|
||||
💣 tailscale.com/tstime/mono from tailscale.com/tstime/rate
|
||||
tailscale.com/tstime/rate from tailscale.com/wgengine/filter
|
||||
@@ -107,6 +107,8 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
|
||||
LD golang.org/x/sys/unix from github.com/jsimonetti/rtnetlink/internal/unix+
|
||||
W golang.org/x/sys/windows from golang.org/x/sys/windows/registry+
|
||||
W golang.org/x/sys/windows/registry from golang.zx2c4.com/wireguard/windows/tunnel/winipcfg+
|
||||
W golang.org/x/sys/windows/svc from golang.org/x/sys/windows/svc/mgr+
|
||||
W golang.org/x/sys/windows/svc/mgr from tailscale.com/util/winutil
|
||||
golang.org/x/text/secure/bidirule from golang.org/x/net/idna
|
||||
golang.org/x/text/transform from golang.org/x/text/secure/bidirule+
|
||||
golang.org/x/text/unicode/bidi from golang.org/x/net/idna+
|
||||
|
||||
@@ -14,7 +14,6 @@ import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"math"
|
||||
"net"
|
||||
@@ -99,7 +98,7 @@ func loadConfig() config {
|
||||
}
|
||||
log.Printf("no config path specified; using %s", *configPath)
|
||||
}
|
||||
b, err := ioutil.ReadFile(*configPath)
|
||||
b, err := os.ReadFile(*configPath)
|
||||
switch {
|
||||
case errors.Is(err, os.ErrNotExist):
|
||||
return writeNewConfig()
|
||||
@@ -155,7 +154,7 @@ func main() {
|
||||
s.SetVerifyClient(*verifyClients)
|
||||
|
||||
if *meshPSKFile != "" {
|
||||
b, err := ioutil.ReadFile(*meshPSKFile)
|
||||
b, err := os.ReadFile(*meshPSKFile)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ func addWebSocketSupport(s *derp.Server, base http.Handler) http.Handler {
|
||||
up := strings.ToLower(r.Header.Get("Upgrade"))
|
||||
|
||||
// Very early versions of Tailscale set "Upgrade: WebSocket" but didn't actually
|
||||
// speak WebSockets (they still assumed DERP's binary framining). So to distinguish
|
||||
// speak WebSockets (they still assumed DERP's binary framing). So to distinguish
|
||||
// clients that actually want WebSockets, look for an explicit "derp" subprotocol.
|
||||
if up != "websocket" || !strings.Contains(r.Header.Get("Sec-Websocket-Protocol"), "derp") {
|
||||
base.ServeHTTP(w, r)
|
||||
@@ -33,6 +33,12 @@ func addWebSocketSupport(s *derp.Server, base http.Handler) http.Handler {
|
||||
c, err := websocket.Accept(w, r, &websocket.AcceptOptions{
|
||||
Subprotocols: []string{"derp"},
|
||||
OriginPatterns: []string{"*"},
|
||||
// Disable compression because we transmit WireGuard messages that
|
||||
// are not compressible.
|
||||
// Additionally, Safari has a broken implementation of compression
|
||||
// (see https://github.com/nhooyr/websocket/issues/218) that makes
|
||||
// enabling it actively harmful.
|
||||
CompressionMode: websocket.CompressionDisabled,
|
||||
})
|
||||
if err != nil {
|
||||
log.Printf("websocket.Accept: %v", err)
|
||||
|
||||
@@ -13,7 +13,6 @@ import (
|
||||
"errors"
|
||||
"flag"
|
||||
"html/template"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
@@ -106,7 +105,7 @@ func devMode() bool { return *httpsAddr == "" && *httpAddr != "" }
|
||||
|
||||
func getTmpl() (*template.Template, error) {
|
||||
if devMode() {
|
||||
tmplData, err := ioutil.ReadFile("hello.tmpl.html")
|
||||
tmplData, err := os.ReadFile("hello.tmpl.html")
|
||||
if os.IsNotExist(err) {
|
||||
log.Printf("using baked-in template in dev mode; can't find hello.tmpl.html in current directory")
|
||||
return tmpl, nil
|
||||
|
||||
42
cmd/pgproxy/README.md
Normal file
42
cmd/pgproxy/README.md
Normal file
@@ -0,0 +1,42 @@
|
||||
# pgproxy
|
||||
|
||||
The pgproxy server is a proxy for the Postgres wire protocol. [Read
|
||||
more in our blog
|
||||
post](https://tailscale.com/blog/introducing-pgproxy/) about it!
|
||||
|
||||
The proxy runs an in-process Tailscale instance, accepts postgres
|
||||
client connections over Tailscale only, and proxies them to the
|
||||
configured upstream postgres server.
|
||||
|
||||
This proxy exists because postgres clients default to very insecure
|
||||
connection settings: either they "prefer" but do not require TLS; or
|
||||
they set sslmode=require, which merely requires that a TLS handshake
|
||||
took place, but don't verify the server's TLS certificate or the
|
||||
presented TLS hostname. In other words, sslmode=require enforces that
|
||||
a TLS session is created, but that session can trivially be
|
||||
machine-in-the-middled to steal credentials, data, inject malicious
|
||||
queries, and so forth.
|
||||
|
||||
Because this flaw is in the client's validation of the TLS session,
|
||||
you have no way of reliably detecting the misconfiguration
|
||||
server-side. You could fix the configuration of all the clients you
|
||||
know of, but the default makes it very easy to accidentally regress.
|
||||
|
||||
Instead of trying to verify client configuration over time, this proxy
|
||||
removes the need for postgres clients to be configured correctly: the
|
||||
upstream database is configured to only accept connections from the
|
||||
proxy, and the proxy is only available to clients over Tailscale.
|
||||
|
||||
Therefore, clients must use the proxy to connect to the database. The
|
||||
client<>proxy connection is secured end-to-end by Tailscale, which the
|
||||
proxy enforces by verifying that the connecting client is a known
|
||||
current Tailscale peer. The proxy<>server connection is established by
|
||||
the proxy itself, using strict TLS verification settings, and the
|
||||
client is only allowed to communicate with the server once we've
|
||||
established that the upstream connection is safe to use.
|
||||
|
||||
A couple side benefits: because clients can only connect via
|
||||
Tailscale, you can use Tailscale ACLs as an extra layer of defense on
|
||||
top of the postgres user/password authentication. And, the proxy can
|
||||
maintain an audit log of who connected to the database, complete with
|
||||
the strongly authenticated Tailscale identity of the client.
|
||||
366
cmd/pgproxy/pgproxy.go
Normal file
366
cmd/pgproxy/pgproxy.go
Normal file
@@ -0,0 +1,366 @@
|
||||
// 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.
|
||||
|
||||
// The pgproxy server is a proxy for the Postgres wire protocol.
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
crand "crypto/rand"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"expvar"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"math/big"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"tailscale.com/client/tailscale"
|
||||
"tailscale.com/metrics"
|
||||
"tailscale.com/tsnet"
|
||||
"tailscale.com/tsweb"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
var (
|
||||
hostname = flag.String("hostname", "", "Tailscale hostname to serve on")
|
||||
port = flag.Int("port", 5432, "Listening port for client connections")
|
||||
debugPort = flag.Int("debug-port", 80, "Listening port for debug/metrics endpoint")
|
||||
upstreamAddr = flag.String("upstream-addr", "", "Address of the upstream Postgres server, in host:port format")
|
||||
upstreamCA = flag.String("upstream-ca-file", "", "File containing the PEM-encoded CA certificate for the upstream server")
|
||||
tailscaleDir = flag.String("state-dir", "", "Directory in which to store the Tailscale auth state")
|
||||
)
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
if *hostname == "" {
|
||||
log.Fatal("missing --hostname")
|
||||
}
|
||||
if *upstreamAddr == "" {
|
||||
log.Fatal("missing --upstream-addr")
|
||||
}
|
||||
if *upstreamCA == "" {
|
||||
log.Fatal("missing --upstream-ca-file")
|
||||
}
|
||||
if *tailscaleDir == "" {
|
||||
log.Fatal("missing --state-dir")
|
||||
}
|
||||
|
||||
ts := &tsnet.Server{
|
||||
Dir: *tailscaleDir,
|
||||
Hostname: *hostname,
|
||||
// Make the stdout logs a clean audit log of connections.
|
||||
Logf: logger.Discard,
|
||||
}
|
||||
|
||||
if os.Getenv("TS_AUTHKEY") == "" {
|
||||
log.Print("Note: you need to run this with TS_AUTHKEY=... the first time, to join your tailnet of choice.")
|
||||
}
|
||||
|
||||
tsclient, err := ts.LocalClient()
|
||||
if err != nil {
|
||||
log.Fatalf("getting tsnet API client: %v", err)
|
||||
}
|
||||
|
||||
p, err := newProxy(*upstreamAddr, *upstreamCA, tsclient)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
expvar.Publish("pgproxy", p.Expvar())
|
||||
|
||||
if *debugPort != 0 {
|
||||
mux := http.NewServeMux()
|
||||
tsweb.Debugger(mux)
|
||||
srv := &http.Server{
|
||||
Handler: mux,
|
||||
}
|
||||
dln, err := ts.Listen("tcp", fmt.Sprintf(":%d", *debugPort))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
go func() {
|
||||
log.Fatal(srv.Serve(dln))
|
||||
}()
|
||||
}
|
||||
|
||||
ln, err := ts.Listen("tcp", fmt.Sprintf(":%d", *port))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
log.Printf("serving access to %s on port %d", *upstreamAddr, *port)
|
||||
log.Fatal(p.Serve(ln))
|
||||
}
|
||||
|
||||
// proxy is a postgres wire protocol proxy, which strictly enforces
|
||||
// the security of the TLS connection to its upstream regardless of
|
||||
// what the client's TLS configuration is.
|
||||
type proxy struct {
|
||||
upstreamAddr string // "my.database.com:5432"
|
||||
upstreamHost string // "my.database.com"
|
||||
upstreamCertPool *x509.CertPool
|
||||
downstreamCert []tls.Certificate
|
||||
client *tailscale.LocalClient
|
||||
|
||||
activeSessions expvar.Int
|
||||
startedSessions expvar.Int
|
||||
errors metrics.LabelMap
|
||||
}
|
||||
|
||||
// newProxy returns a proxy that forwards connections to
|
||||
// upstreamAddr. The upstream's TLS session is verified using the CA
|
||||
// cert(s) in upstreamCAPath.
|
||||
func newProxy(upstreamAddr, upstreamCAPath string, client *tailscale.LocalClient) (*proxy, error) {
|
||||
bs, err := os.ReadFile(upstreamCAPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
upstreamCertPool := x509.NewCertPool()
|
||||
if !upstreamCertPool.AppendCertsFromPEM(bs) {
|
||||
return nil, fmt.Errorf("invalid CA cert in %q", upstreamCAPath)
|
||||
}
|
||||
|
||||
h, _, err := net.SplitHostPort(upstreamAddr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
downstreamCert, err := mkSelfSigned(h)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &proxy{
|
||||
upstreamAddr: upstreamAddr,
|
||||
upstreamHost: h,
|
||||
upstreamCertPool: upstreamCertPool,
|
||||
downstreamCert: []tls.Certificate{downstreamCert},
|
||||
client: client,
|
||||
errors: metrics.LabelMap{Label: "kind"},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Expvar returns p's monitoring metrics.
|
||||
func (p *proxy) Expvar() expvar.Var {
|
||||
ret := &metrics.Set{}
|
||||
ret.Set("sessions_active", &p.activeSessions)
|
||||
ret.Set("sessions_started", &p.startedSessions)
|
||||
ret.Set("session_errors", &p.errors)
|
||||
return ret
|
||||
}
|
||||
|
||||
// Serve accepts postgres client connections on ln and proxies them to
|
||||
// the configured upstream. ln can be any net.Listener, but all client
|
||||
// connections must originate from tailscale IPs that can be verified
|
||||
// with WhoIs.
|
||||
func (p *proxy) Serve(ln net.Listener) error {
|
||||
var lastSessionID int64
|
||||
for {
|
||||
c, err := ln.Accept()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
id := time.Now().UnixNano()
|
||||
if id == lastSessionID {
|
||||
// Bluntly enforce SID uniqueness, even if collisions are
|
||||
// fantastically unlikely (but OSes vary in how much timer
|
||||
// precision they expose to the OS, so id might be rounded
|
||||
// e.g. to the same millisecond)
|
||||
id++
|
||||
}
|
||||
lastSessionID = id
|
||||
go func(sessionID int64) {
|
||||
if err := p.serve(sessionID, c); err != nil {
|
||||
log.Printf("%d: session ended with error: %v", sessionID, err)
|
||||
}
|
||||
}(id)
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
// sslStart is the magic bytes that postgres clients use to indicate
|
||||
// that they want to do a TLS handshake. Servers should respond with
|
||||
// the single byte "S" before starting a normal TLS handshake.
|
||||
sslStart = [8]byte{0, 0, 0, 8, 0x04, 0xd2, 0x16, 0x2f}
|
||||
// plaintextStart is the magic bytes that postgres clients use to
|
||||
// indicate that they're starting a plaintext authentication
|
||||
// handshake.
|
||||
plaintextStart = [8]byte{0, 0, 0, 86, 0, 3, 0, 0}
|
||||
)
|
||||
|
||||
// serve proxies the postgres client on c to the proxy's upstream,
|
||||
// enforcing strict TLS to the upstream.
|
||||
func (p *proxy) serve(sessionID int64, c net.Conn) error {
|
||||
defer c.Close()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
whois, err := p.client.WhoIs(ctx, c.RemoteAddr().String())
|
||||
if err != nil {
|
||||
p.errors.Add("whois-failed", 1)
|
||||
return fmt.Errorf("getting client identity: %v", err)
|
||||
}
|
||||
|
||||
// Before anything else, log the connection attempt.
|
||||
user, machine := "", ""
|
||||
if whois.Node != nil {
|
||||
if whois.Node.Hostinfo.ShareeNode() {
|
||||
machine = "external-device"
|
||||
} else {
|
||||
machine = strings.TrimSuffix(whois.Node.Name, ".")
|
||||
}
|
||||
}
|
||||
if whois.UserProfile != nil {
|
||||
user = whois.UserProfile.LoginName
|
||||
if user == "tagged-devices" && whois.Node != nil {
|
||||
user = strings.Join(whois.Node.Tags, ",")
|
||||
}
|
||||
}
|
||||
if user == "" || machine == "" {
|
||||
p.errors.Add("no-ts-identity", 1)
|
||||
return fmt.Errorf("couldn't identify source user and machine (user %q, machine %q)", user, machine)
|
||||
}
|
||||
log.Printf("%d: session start, from %s (machine %s, user %s)", sessionID, c.RemoteAddr(), machine, user)
|
||||
start := time.Now()
|
||||
defer func() {
|
||||
elapsed := time.Since(start)
|
||||
log.Printf("%d: session end, from %s (machine %s, user %s), lasted %s", sessionID, c.RemoteAddr(), machine, user, elapsed.Round(time.Millisecond))
|
||||
}()
|
||||
|
||||
// Read the client's opening message, to figure out if it's trying
|
||||
// to TLS or not.
|
||||
var buf [8]byte
|
||||
if _, err := io.ReadFull(c, buf[:len(sslStart)]); err != nil {
|
||||
p.errors.Add("network-error", 1)
|
||||
return fmt.Errorf("initial magic read: %v", err)
|
||||
}
|
||||
var clientIsTLS bool
|
||||
switch {
|
||||
case buf == sslStart:
|
||||
clientIsTLS = true
|
||||
case buf == plaintextStart:
|
||||
clientIsTLS = false
|
||||
default:
|
||||
p.errors.Add("client-bad-protocol", 1)
|
||||
return fmt.Errorf("unrecognized initial packet = % 02x", buf)
|
||||
}
|
||||
|
||||
// Dial & verify upstream connection.
|
||||
var d net.Dialer
|
||||
d.Timeout = 10 * time.Second
|
||||
upc, err := d.Dial("tcp", p.upstreamAddr)
|
||||
if err != nil {
|
||||
p.errors.Add("network-error", 1)
|
||||
return fmt.Errorf("upstream dial: %v", err)
|
||||
}
|
||||
defer upc.Close()
|
||||
if _, err := upc.Write(sslStart[:]); err != nil {
|
||||
p.errors.Add("network-error", 1)
|
||||
return fmt.Errorf("upstream write of start-ssl magic: %v", err)
|
||||
}
|
||||
if _, err := io.ReadFull(upc, buf[:1]); err != nil {
|
||||
p.errors.Add("network-error", 1)
|
||||
return fmt.Errorf("reading upstream start-ssl response: %v", err)
|
||||
}
|
||||
if buf[0] != 'S' {
|
||||
p.errors.Add("upstream-bad-protocol", 1)
|
||||
return fmt.Errorf("upstream didn't acknowldge start-ssl, said %q", buf[0])
|
||||
}
|
||||
tlsConf := &tls.Config{
|
||||
ServerName: p.upstreamHost,
|
||||
RootCAs: p.upstreamCertPool,
|
||||
MinVersion: tls.VersionTLS12,
|
||||
}
|
||||
uptc := tls.Client(upc, tlsConf)
|
||||
if err = uptc.HandshakeContext(ctx); err != nil {
|
||||
p.errors.Add("upstream-tls", 1)
|
||||
return fmt.Errorf("upstream TLS handshake: %v", err)
|
||||
}
|
||||
|
||||
// Accept the client conn and set it up the way the client wants.
|
||||
var clientConn net.Conn
|
||||
if clientIsTLS {
|
||||
io.WriteString(c, "S") // yeah, we're good to speak TLS
|
||||
s := tls.Server(c, &tls.Config{
|
||||
ServerName: p.upstreamHost,
|
||||
Certificates: p.downstreamCert,
|
||||
MinVersion: tls.VersionTLS12,
|
||||
})
|
||||
if err = uptc.HandshakeContext(ctx); err != nil {
|
||||
p.errors.Add("client-tls", 1)
|
||||
return fmt.Errorf("client TLS handshake: %v", err)
|
||||
}
|
||||
clientConn = s
|
||||
} else {
|
||||
// Repeat the header we read earlier up to the server.
|
||||
if _, err := uptc.Write(plaintextStart[:]); err != nil {
|
||||
p.errors.Add("network-error", 1)
|
||||
return fmt.Errorf("sending initial client bytes to upstream: %v", err)
|
||||
}
|
||||
clientConn = c
|
||||
}
|
||||
|
||||
// Finally, proxy the client to the upstream.
|
||||
errc := make(chan error, 1)
|
||||
go func() {
|
||||
_, err := io.Copy(uptc, clientConn)
|
||||
errc <- err
|
||||
}()
|
||||
go func() {
|
||||
_, err := io.Copy(clientConn, uptc)
|
||||
errc <- err
|
||||
}()
|
||||
if err := <-errc; err != nil {
|
||||
// Don't increment error counts here, because the most common
|
||||
// cause of termination is client or server closing the
|
||||
// connection normally, and it'll obscure "interesting"
|
||||
// handshake errors.
|
||||
return fmt.Errorf("session terminated with error: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// mkSelfSigned creates and returns a self-signed TLS certificate for
|
||||
// hostname.
|
||||
func mkSelfSigned(hostname string) (tls.Certificate, error) {
|
||||
priv, err := ecdsa.GenerateKey(elliptic.P256(), crand.Reader)
|
||||
if err != nil {
|
||||
return tls.Certificate{}, err
|
||||
}
|
||||
pub := priv.Public()
|
||||
template := x509.Certificate{
|
||||
SerialNumber: big.NewInt(1),
|
||||
Subject: pkix.Name{
|
||||
Organization: []string{"pgproxy"},
|
||||
},
|
||||
DNSNames: []string{hostname},
|
||||
NotBefore: time.Now(),
|
||||
NotAfter: time.Now().Add(10 * 365 * 24 * time.Hour),
|
||||
KeyUsage: x509.KeyUsageDigitalSignature,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
||||
BasicConstraintsValid: true,
|
||||
}
|
||||
derBytes, err := x509.CreateCertificate(crand.Reader, &template, &template, pub, priv)
|
||||
if err != nil {
|
||||
return tls.Certificate{}, err
|
||||
}
|
||||
cert, err := x509.ParseCertificate(derBytes)
|
||||
if err != nil {
|
||||
return tls.Certificate{}, err
|
||||
}
|
||||
|
||||
return tls.Certificate{
|
||||
Certificate: [][]byte{derBytes},
|
||||
PrivateKey: priv,
|
||||
Leaf: cert,
|
||||
}, nil
|
||||
}
|
||||
@@ -110,11 +110,12 @@ func runSpeedtest(ctx context.Context, args []string) error {
|
||||
w := tabwriter.NewWriter(os.Stdout, 12, 0, 0, ' ', tabwriter.TabIndent)
|
||||
fmt.Println("Results:")
|
||||
fmt.Fprintln(w, "Interval\t\tTransfer\t\tBandwidth\t\t")
|
||||
startTime := results[0].IntervalStart
|
||||
for _, r := range results {
|
||||
if r.Total {
|
||||
fmt.Fprintln(w, "-------------------------------------------------------------------------")
|
||||
}
|
||||
fmt.Fprintf(w, "%.2f-%.2f\tsec\t%.4f\tMBits\t%.4f\tMbits/sec\t\n", r.IntervalStart.Seconds(), r.IntervalEnd.Seconds(), r.MegaBits(), r.MBitsPerSecond())
|
||||
fmt.Fprintf(w, "%.2f-%.2f\tsec\t%.4f\tMBits\t%.4f\tMbits/sec\t\n", r.IntervalStart.Sub(startTime).Seconds(), r.IntervalEnd.Sub(startTime).Seconds(), r.MegaBits(), r.MBitsPerSecond())
|
||||
}
|
||||
w.Flush()
|
||||
return nil
|
||||
|
||||
189
cmd/ssh-auth-none-demo/ssh-auth-none-demo.go
Normal file
189
cmd/ssh-auth-none-demo/ssh-auth-none-demo.go
Normal file
@@ -0,0 +1,189 @@
|
||||
// Copyright (c) 2022 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.
|
||||
|
||||
// ssh-auth-none-demo is a demo SSH server that's meant to run on the
|
||||
// public internet (at 188.166.70.128 port 2222) and
|
||||
// highlight the unique parts of the Tailscale SSH server so SSH
|
||||
// client authors can hit it easily and fix their SSH clients without
|
||||
// needing to set up Tailscale and Tailscale SSH.
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"crypto/ed25519"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
gossh "github.com/tailscale/golang-x-crypto/ssh"
|
||||
"tailscale.com/tempfork/gliderlabs/ssh"
|
||||
)
|
||||
|
||||
// keyTypes are the SSH key types that we either try to read from the
|
||||
// system's OpenSSH keys.
|
||||
var keyTypes = []string{"rsa", "ecdsa", "ed25519"}
|
||||
|
||||
var (
|
||||
addr = flag.String("addr", ":2222", "address to listen on")
|
||||
)
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
cacheDir, err := os.UserCacheDir()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
dir := filepath.Join(cacheDir, "ssh-auth-none-demo")
|
||||
if err := os.MkdirAll(dir, 0700); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
keys, err := getHostKeys(dir)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if len(keys) == 0 {
|
||||
log.Fatal("no host keys")
|
||||
}
|
||||
|
||||
srv := &ssh.Server{
|
||||
Addr: *addr,
|
||||
Version: "Tailscale",
|
||||
Handler: handleSessionPostSSHAuth,
|
||||
ServerConfigCallback: func(ctx ssh.Context) *gossh.ServerConfig {
|
||||
start := time.Now()
|
||||
return &gossh.ServerConfig{
|
||||
NextAuthMethodCallback: func(conn gossh.ConnMetadata, prevErrors []error) []string {
|
||||
return []string{"tailscale"}
|
||||
},
|
||||
NoClientAuth: true, // required for the NoClientAuthCallback to run
|
||||
NoClientAuthCallback: func(cm gossh.ConnMetadata) (*gossh.Permissions, error) {
|
||||
cm.SendAuthBanner(fmt.Sprintf("# Banner: doing none auth at %v\r\n", time.Since(start)))
|
||||
|
||||
totalBanners := 2
|
||||
if cm.User() == "banners" {
|
||||
totalBanners = 5
|
||||
}
|
||||
for banner := 2; banner <= totalBanners; banner++ {
|
||||
time.Sleep(time.Second)
|
||||
if banner == totalBanners {
|
||||
cm.SendAuthBanner(fmt.Sprintf("# Banner%d: access granted at %v\r\n", banner, time.Since(start)))
|
||||
} else {
|
||||
cm.SendAuthBanner(fmt.Sprintf("# Banner%d at %v\r\n", banner, time.Since(start)))
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
},
|
||||
BannerCallback: func(cm gossh.ConnMetadata) string {
|
||||
log.Printf("Got connection from user %q, %q from %v", cm.User(), cm.ClientVersion(), cm.RemoteAddr())
|
||||
return fmt.Sprintf("# Banner for user %q, %q\n", cm.User(), cm.ClientVersion())
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
for _, signer := range keys {
|
||||
srv.AddHostKey(signer)
|
||||
}
|
||||
|
||||
log.Printf("Running on %s ...", srv.Addr)
|
||||
if err := srv.ListenAndServe(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
log.Printf("done")
|
||||
}
|
||||
|
||||
func handleSessionPostSSHAuth(s ssh.Session) {
|
||||
log.Printf("Started session from user %q", s.User())
|
||||
fmt.Fprintf(s, "Hello user %q, it worked.\n", s.User())
|
||||
|
||||
// Abort the session on Control-C or Control-D.
|
||||
go func() {
|
||||
buf := make([]byte, 1024)
|
||||
for {
|
||||
n, err := s.Read(buf)
|
||||
for _, b := range buf[:n] {
|
||||
if b <= 4 { // abort on Control-C (3) or Control-D (4)
|
||||
io.WriteString(s, "bye\n")
|
||||
s.Exit(1)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
for i := 10; i > 0; i-- {
|
||||
fmt.Fprintf(s, "%v ...\n", i)
|
||||
time.Sleep(time.Second)
|
||||
}
|
||||
s.Exit(0)
|
||||
}
|
||||
|
||||
func getHostKeys(dir string) (ret []ssh.Signer, err error) {
|
||||
for _, typ := range keyTypes {
|
||||
hostKey, err := hostKeyFileOrCreate(dir, typ)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
signer, err := gossh.ParsePrivateKey(hostKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ret = append(ret, signer)
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func hostKeyFileOrCreate(keyDir, typ string) ([]byte, error) {
|
||||
path := filepath.Join(keyDir, "ssh_host_"+typ+"_key")
|
||||
v, err := ioutil.ReadFile(path)
|
||||
if err == nil {
|
||||
return v, nil
|
||||
}
|
||||
if !os.IsNotExist(err) {
|
||||
return nil, err
|
||||
}
|
||||
var priv any
|
||||
switch typ {
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported key type %q", typ)
|
||||
case "ed25519":
|
||||
_, priv, err = ed25519.GenerateKey(rand.Reader)
|
||||
case "ecdsa":
|
||||
// curve is arbitrary. We pick whatever will at
|
||||
// least pacify clients as the actual encryption
|
||||
// doesn't matter: it's all over WireGuard anyway.
|
||||
curve := elliptic.P256()
|
||||
priv, err = ecdsa.GenerateKey(curve, rand.Reader)
|
||||
case "rsa":
|
||||
// keySize is arbitrary. We pick whatever will at
|
||||
// least pacify clients as the actual encryption
|
||||
// doesn't matter: it's all over WireGuard anyway.
|
||||
const keySize = 2048
|
||||
priv, err = rsa.GenerateKey(rand.Reader, keySize)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mk, err := x509.MarshalPKCS8PrivateKey(priv)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pemGen := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: mk})
|
||||
err = os.WriteFile(path, pemGen, 0700)
|
||||
return pemGen, err
|
||||
}
|
||||
@@ -7,8 +7,11 @@ package cli
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
"tailscale.com/client/tailscale"
|
||||
)
|
||||
|
||||
var bugReportCmd = &ffcli.Command{
|
||||
@@ -16,6 +19,17 @@ var bugReportCmd = &ffcli.Command{
|
||||
Exec: runBugReport,
|
||||
ShortHelp: "Print a shareable identifier to help diagnose issues",
|
||||
ShortUsage: "bugreport [note]",
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
fs := newFlagSet("bugreport")
|
||||
fs.BoolVar(&bugReportArgs.diagnose, "diagnose", false, "run additional in-depth checks")
|
||||
fs.BoolVar(&bugReportArgs.record, "record", false, "if true, pause and then write another bugreport")
|
||||
return fs
|
||||
})(),
|
||||
}
|
||||
|
||||
var bugReportArgs struct {
|
||||
diagnose bool
|
||||
record bool
|
||||
}
|
||||
|
||||
func runBugReport(ctx context.Context, args []string) error {
|
||||
@@ -25,12 +39,31 @@ func runBugReport(ctx context.Context, args []string) error {
|
||||
case 1:
|
||||
note = args[0]
|
||||
default:
|
||||
return errors.New("unknown argumets")
|
||||
return errors.New("unknown arguments")
|
||||
}
|
||||
logMarker, err := localClient.BugReport(ctx, note)
|
||||
logMarker, err := localClient.BugReportWithOpts(ctx, tailscale.BugReportOpts{
|
||||
Note: note,
|
||||
Diagnose: bugReportArgs.diagnose,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if bugReportArgs.record {
|
||||
outln("The initial bugreport is below; please reproduce your issue and then press Enter...")
|
||||
}
|
||||
|
||||
outln(logMarker)
|
||||
|
||||
if bugReportArgs.record {
|
||||
fmt.Scanln()
|
||||
|
||||
logMarker, err := localClient.BugReportWithOpts(ctx, tailscale.BugReportOpts{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
outln(logMarker)
|
||||
outln("Please provide both bugreport markers above to the support team or GitHub issue.")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -410,7 +410,7 @@ func TestCheckForAccidentalSettingReverts(t *testing.T) {
|
||||
want: accidentalUpPrefix + " --hostname=foo --exit-node=100.64.5.7",
|
||||
},
|
||||
{
|
||||
name: "error_exit_node_and_allow_lan_omit_with_id_pref", // Isue 3480
|
||||
name: "error_exit_node_and_allow_lan_omit_with_id_pref", // Issue 3480
|
||||
flags: []string{"--hostname=foo"},
|
||||
curExitNodeIP: netip.MustParseAddr("100.2.3.4"),
|
||||
curPrefs: &ipn.Prefs{
|
||||
@@ -448,7 +448,7 @@ func TestCheckForAccidentalSettingReverts(t *testing.T) {
|
||||
},
|
||||
{
|
||||
// Issue 3176: on Synology, don't require --accept-routes=false because user
|
||||
// migth've had old an install, and we don't support --accept-routes anyway.
|
||||
// might've had an old install, and we don't support --accept-routes anyway.
|
||||
name: "synology_permit_omit_accept_routes",
|
||||
flags: []string{"--hostname=foo"},
|
||||
curPrefs: &ipn.Prefs{
|
||||
@@ -789,6 +789,10 @@ func TestUpdatePrefs(t *testing.T) {
|
||||
curPrefs *ipn.Prefs
|
||||
env upCheckEnv // empty goos means "linux"
|
||||
|
||||
// sshOverTailscale specifies if the cmd being run over SSH over Tailscale.
|
||||
// It is used to test the --accept-risks flag.
|
||||
sshOverTailscale bool
|
||||
|
||||
// checkUpdatePrefsMutations, if non-nil, is run with the new prefs after
|
||||
// updatePrefs might've mutated them (from applyImplicitPrefs).
|
||||
checkUpdatePrefsMutations func(t *testing.T, newPrefs *ipn.Prefs)
|
||||
@@ -916,15 +920,159 @@ func TestUpdatePrefs(t *testing.T) {
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "enable_ssh",
|
||||
flags: []string{"--ssh"},
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: "https://login.tailscale.com",
|
||||
Persist: &persist.Persist{LoginName: "crawshaw.github"},
|
||||
AllowSingleHosts: true,
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
},
|
||||
wantJustEditMP: &ipn.MaskedPrefs{
|
||||
RunSSHSet: true,
|
||||
WantRunningSet: true,
|
||||
},
|
||||
checkUpdatePrefsMutations: func(t *testing.T, newPrefs *ipn.Prefs) {
|
||||
if !newPrefs.RunSSH {
|
||||
t.Errorf("RunSSH not set to true")
|
||||
}
|
||||
},
|
||||
env: upCheckEnv{backendState: "Running"},
|
||||
},
|
||||
{
|
||||
name: "disable_ssh",
|
||||
flags: []string{"--ssh=false"},
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: "https://login.tailscale.com",
|
||||
Persist: &persist.Persist{LoginName: "crawshaw.github"},
|
||||
AllowSingleHosts: true,
|
||||
CorpDNS: true,
|
||||
RunSSH: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
},
|
||||
wantJustEditMP: &ipn.MaskedPrefs{
|
||||
RunSSHSet: true,
|
||||
WantRunningSet: true,
|
||||
},
|
||||
checkUpdatePrefsMutations: func(t *testing.T, newPrefs *ipn.Prefs) {
|
||||
if newPrefs.RunSSH {
|
||||
t.Errorf("RunSSH not set to false")
|
||||
}
|
||||
},
|
||||
env: upCheckEnv{backendState: "Running", upArgs: upArgsT{
|
||||
runSSH: true,
|
||||
}},
|
||||
},
|
||||
{
|
||||
name: "disable_ssh_over_ssh_no_risk",
|
||||
flags: []string{"--ssh=false"},
|
||||
sshOverTailscale: true,
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: "https://login.tailscale.com",
|
||||
Persist: &persist.Persist{LoginName: "crawshaw.github"},
|
||||
AllowSingleHosts: true,
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
RunSSH: true,
|
||||
},
|
||||
wantJustEditMP: &ipn.MaskedPrefs{
|
||||
RunSSHSet: true,
|
||||
WantRunningSet: true,
|
||||
},
|
||||
checkUpdatePrefsMutations: func(t *testing.T, newPrefs *ipn.Prefs) {
|
||||
if !newPrefs.RunSSH {
|
||||
t.Errorf("RunSSH not set to true")
|
||||
}
|
||||
},
|
||||
env: upCheckEnv{backendState: "Running"},
|
||||
wantErrSubtr: "aborted, no changes made",
|
||||
},
|
||||
{
|
||||
name: "enable_ssh_over_ssh_no_risk",
|
||||
flags: []string{"--ssh=true"},
|
||||
sshOverTailscale: true,
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: "https://login.tailscale.com",
|
||||
Persist: &persist.Persist{LoginName: "crawshaw.github"},
|
||||
AllowSingleHosts: true,
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
},
|
||||
wantJustEditMP: &ipn.MaskedPrefs{
|
||||
RunSSHSet: true,
|
||||
WantRunningSet: true,
|
||||
},
|
||||
checkUpdatePrefsMutations: func(t *testing.T, newPrefs *ipn.Prefs) {
|
||||
if !newPrefs.RunSSH {
|
||||
t.Errorf("RunSSH not set to true")
|
||||
}
|
||||
},
|
||||
env: upCheckEnv{backendState: "Running"},
|
||||
wantErrSubtr: "aborted, no changes made",
|
||||
},
|
||||
{
|
||||
name: "enable_ssh_over_ssh",
|
||||
flags: []string{"--ssh=true", "--accept-risk=lose-ssh"},
|
||||
sshOverTailscale: true,
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: "https://login.tailscale.com",
|
||||
Persist: &persist.Persist{LoginName: "crawshaw.github"},
|
||||
AllowSingleHosts: true,
|
||||
CorpDNS: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
},
|
||||
wantJustEditMP: &ipn.MaskedPrefs{
|
||||
RunSSHSet: true,
|
||||
WantRunningSet: true,
|
||||
},
|
||||
checkUpdatePrefsMutations: func(t *testing.T, newPrefs *ipn.Prefs) {
|
||||
if !newPrefs.RunSSH {
|
||||
t.Errorf("RunSSH not set to true")
|
||||
}
|
||||
},
|
||||
env: upCheckEnv{backendState: "Running"},
|
||||
},
|
||||
{
|
||||
name: "disable_ssh_over_ssh",
|
||||
flags: []string{"--ssh=false", "--accept-risk=lose-ssh"},
|
||||
sshOverTailscale: true,
|
||||
curPrefs: &ipn.Prefs{
|
||||
ControlURL: "https://login.tailscale.com",
|
||||
Persist: &persist.Persist{LoginName: "crawshaw.github"},
|
||||
AllowSingleHosts: true,
|
||||
CorpDNS: true,
|
||||
RunSSH: true,
|
||||
NetfilterMode: preftype.NetfilterOn,
|
||||
},
|
||||
wantJustEditMP: &ipn.MaskedPrefs{
|
||||
RunSSHSet: true,
|
||||
WantRunningSet: true,
|
||||
},
|
||||
checkUpdatePrefsMutations: func(t *testing.T, newPrefs *ipn.Prefs) {
|
||||
if newPrefs.RunSSH {
|
||||
t.Errorf("RunSSH not set to false")
|
||||
}
|
||||
},
|
||||
env: upCheckEnv{backendState: "Running"},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if tt.sshOverTailscale {
|
||||
old := getSSHClientEnvVar
|
||||
getSSHClientEnvVar = func() string { return "100.100.100.100 1 1" }
|
||||
t.Cleanup(func() { getSSHClientEnvVar = old })
|
||||
}
|
||||
if tt.env.goos == "" {
|
||||
tt.env.goos = "linux"
|
||||
}
|
||||
tt.env.flagSet = newUpFlagSet(tt.env.goos, &tt.env.upArgs)
|
||||
flags := CleanUpArgs(tt.flags)
|
||||
tt.env.flagSet.Parse(flags)
|
||||
if err := tt.env.flagSet.Parse(flags); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
newPrefs, err := prefsFromUpArgs(tt.env.upArgs, t.Logf, new(ipnstate.Status), tt.env.goos)
|
||||
if err != nil {
|
||||
@@ -939,6 +1087,8 @@ func TestUpdatePrefs(t *testing.T) {
|
||||
return
|
||||
}
|
||||
t.Fatal(err)
|
||||
} else if tt.wantErrSubtr != "" {
|
||||
t.Fatalf("want error %q, got nil", tt.wantErrSubtr)
|
||||
}
|
||||
if tt.checkUpdatePrefsMutations != nil {
|
||||
tt.checkUpdatePrefsMutations(t, newPrefs)
|
||||
@@ -952,13 +1102,18 @@ func TestUpdatePrefs(t *testing.T) {
|
||||
justEditMP.Prefs = ipn.Prefs{} // uninteresting
|
||||
}
|
||||
if !reflect.DeepEqual(justEditMP, tt.wantJustEditMP) {
|
||||
t.Logf("justEditMP != wantJustEditMP; following diff omits the Prefs field, which was %+v", oldEditPrefs)
|
||||
t.Logf("justEditMP != wantJustEditMP; following diff omits the Prefs field, which was \n%v", asJSON(oldEditPrefs))
|
||||
t.Fatalf("justEditMP: %v\n\n: ", cmp.Diff(justEditMP, tt.wantJustEditMP, cmpIP))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func asJSON(v any) string {
|
||||
b, _ := json.MarshalIndent(v, "", "\t")
|
||||
return string(b)
|
||||
}
|
||||
|
||||
var cmpIP = cmp.Comparer(func(a, b netip.Addr) bool {
|
||||
return a == b
|
||||
})
|
||||
|
||||
@@ -48,11 +48,11 @@ func runConfigureHost(ctx context.Context, args []string) error {
|
||||
if uid := os.Getuid(); uid != 0 {
|
||||
return fmt.Errorf("must be run as root, not %q (%v)", os.Getenv("USER"), uid)
|
||||
}
|
||||
osVer := hostinfo.GetOSVersion()
|
||||
isDSM6 := strings.HasPrefix(osVer, "Synology 6")
|
||||
isDSM7 := strings.HasPrefix(osVer, "Synology 7")
|
||||
hi := hostinfo.New()
|
||||
isDSM6 := strings.HasPrefix(hi.DistroVersion, "6.")
|
||||
isDSM7 := strings.HasPrefix(hi.DistroVersion, "7.")
|
||||
if !isDSM6 && !isDSM7 {
|
||||
return fmt.Errorf("unsupported DSM version %q", osVer)
|
||||
return fmt.Errorf("unsupported DSM version %q", hi.DistroVersion)
|
||||
}
|
||||
if _, err := os.Stat("/dev/net/tun"); os.IsNotExist(err) {
|
||||
if err := os.MkdirAll("/dev/net", 0755); err != nil {
|
||||
|
||||
@@ -53,6 +53,16 @@ var debugCmd = &ffcli.Command{
|
||||
Exec: runDERPMap,
|
||||
ShortHelp: "print DERP map",
|
||||
},
|
||||
{
|
||||
Name: "component-logs",
|
||||
Exec: runDebugComponentLogs,
|
||||
ShortHelp: "enable/disable debug logs for a component",
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
fs := newFlagSet("component-logs")
|
||||
fs.DurationVar(&debugComponentLogsArgs.forDur, "for", time.Hour, "how long to enable debug logs for; zero or negative means to disable")
|
||||
return fs
|
||||
})(),
|
||||
},
|
||||
{
|
||||
Name: "daemon-goroutines",
|
||||
Exec: runDaemonGoroutines,
|
||||
@@ -489,7 +499,15 @@ func runTS2021(ctx context.Context, args []string) error {
|
||||
return c, err
|
||||
}
|
||||
|
||||
conn, err := controlhttp.Dial(ctx, ts2021Args.host, "80", "443", machinePrivate, keys.PublicKey, uint16(ts2021Args.version), dialFunc)
|
||||
conn, err := (&controlhttp.Dialer{
|
||||
Hostname: ts2021Args.host,
|
||||
HTTPPort: "80",
|
||||
HTTPSPort: "443",
|
||||
MachineKey: machinePrivate,
|
||||
ControlKey: keys.PublicKey,
|
||||
ProtocolVersion: uint16(ts2021Args.version),
|
||||
Dialer: dialFunc,
|
||||
}).Dial(ctx)
|
||||
log.Printf("controlhttp.Dial = %p, %v", conn, err)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -505,3 +523,26 @@ func runTS2021(ctx context.Context, args []string) error {
|
||||
log.Printf("final underlying conn: %v / %v", conn.LocalAddr(), conn.RemoteAddr())
|
||||
return nil
|
||||
}
|
||||
|
||||
var debugComponentLogsArgs struct {
|
||||
forDur time.Duration
|
||||
}
|
||||
|
||||
func runDebugComponentLogs(ctx context.Context, args []string) error {
|
||||
if len(args) != 1 {
|
||||
return errors.New("usage: debug component-logs <component>")
|
||||
}
|
||||
component := args[0]
|
||||
dur := debugComponentLogsArgs.forDur
|
||||
|
||||
err := localClient.SetComponentDebugLogging(ctx, component, dur)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if debugComponentLogsArgs.forDur <= 0 {
|
||||
fmt.Printf("Disabled debug logs for component %q\n", component)
|
||||
} else {
|
||||
fmt.Printf("Enabled debug logs for component %q for %v\n", component, dur)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -22,9 +22,13 @@ var downCmd = &ffcli.Command{
|
||||
FlagSet: newDownFlagSet(),
|
||||
}
|
||||
|
||||
var downArgs struct {
|
||||
acceptedRisks string
|
||||
}
|
||||
|
||||
func newDownFlagSet() *flag.FlagSet {
|
||||
downf := newFlagSet("down")
|
||||
registerAcceptRiskFlag(downf)
|
||||
registerAcceptRiskFlag(downf, &downArgs.acceptedRisks)
|
||||
return downf
|
||||
}
|
||||
|
||||
@@ -34,7 +38,7 @@ func runDown(ctx context.Context, args []string) error {
|
||||
}
|
||||
|
||||
if isSSHOverTailscale() {
|
||||
if err := presentRiskToUser(riskLoseSSH, `You are connected over Tailscale; this action will disable Tailscale and result in your session disconnecting.`); err != nil {
|
||||
if err := presentRiskToUser(riskLoseSSH, `You are connected over Tailscale; this action will disable Tailscale and result in your session disconnecting.`, downArgs.acceptedRisks); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"sort"
|
||||
@@ -134,6 +133,9 @@ func printReport(dm *tailcfg.DERPMap, report *netcheck.Report) error {
|
||||
printf("\t* MappingVariesByDestIP: %v\n", report.MappingVariesByDestIP)
|
||||
printf("\t* HairPinning: %v\n", report.HairPinning)
|
||||
printf("\t* PortMapping: %v\n", portMapping(report))
|
||||
if report.CaptivePortal != "" {
|
||||
printf("\t* CaptivePortal: %v\n", report.CaptivePortal)
|
||||
}
|
||||
|
||||
// When DERP latency checking failed,
|
||||
// magicsock will try to pick the DERP server that
|
||||
@@ -202,7 +204,7 @@ func prodDERPMap(ctx context.Context, httpc *http.Client) (*tailcfg.DERPMap, err
|
||||
return nil, fmt.Errorf("fetch prodDERPMap failed: %w", err)
|
||||
}
|
||||
defer res.Body.Close()
|
||||
b, err := ioutil.ReadAll(io.LimitReader(res.Body, 1<<20))
|
||||
b, err := io.ReadAll(io.LimitReader(res.Body, 1<<20))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("fetch prodDERPMap failed: %w", err)
|
||||
}
|
||||
|
||||
@@ -17,11 +17,16 @@ import (
|
||||
)
|
||||
|
||||
var netlockCmd = &ffcli.Command{
|
||||
Name: "lock",
|
||||
ShortUsage: "lock <sub-command> <arguments>",
|
||||
ShortHelp: "Manipulate the tailnet key authority",
|
||||
Subcommands: []*ffcli.Command{nlInitCmd, nlStatusCmd},
|
||||
Exec: runNetworkLockStatus,
|
||||
Name: "lock",
|
||||
ShortUsage: "lock <sub-command> <arguments>",
|
||||
ShortHelp: "Manipulate the tailnet key authority",
|
||||
Subcommands: []*ffcli.Command{
|
||||
nlInitCmd,
|
||||
nlStatusCmd,
|
||||
nlAddCmd,
|
||||
nlRemoveCmd,
|
||||
},
|
||||
Exec: runNetworkLockStatus,
|
||||
}
|
||||
|
||||
var nlInitCmd = &ffcli.Command{
|
||||
@@ -41,29 +46,9 @@ func runNetworkLockInit(ctx context.Context, args []string) error {
|
||||
}
|
||||
|
||||
// Parse the set of initially-trusted keys.
|
||||
// Keys are specified using their key.NLPublic.MarshalText representation,
|
||||
// with an optional '?<votes>' suffix.
|
||||
var keys []tka.Key
|
||||
for i, a := range args {
|
||||
var key key.NLPublic
|
||||
spl := strings.SplitN(a, "?", 2)
|
||||
if err := key.UnmarshalText([]byte(spl[0])); err != nil {
|
||||
return fmt.Errorf("parsing key %d: %v", i+1, err)
|
||||
}
|
||||
|
||||
k := tka.Key{
|
||||
Kind: tka.Key25519,
|
||||
Public: key.Verifier(),
|
||||
Votes: 1,
|
||||
}
|
||||
if len(spl) > 1 {
|
||||
votes, err := strconv.Atoi(spl[1])
|
||||
if err != nil {
|
||||
return fmt.Errorf("parsing key %d votes: %v", i+1, err)
|
||||
}
|
||||
k.Votes = uint(votes)
|
||||
}
|
||||
keys = append(keys, k)
|
||||
keys, err := parseNLKeyArgs(args)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
status, err := localClient.NetworkLockInit(ctx, keys)
|
||||
@@ -99,3 +84,78 @@ func runNetworkLockStatus(ctx context.Context, args []string) error {
|
||||
fmt.Printf("our public-key: %s\n", p)
|
||||
return nil
|
||||
}
|
||||
|
||||
var nlAddCmd = &ffcli.Command{
|
||||
Name: "add",
|
||||
ShortUsage: "add <public-key>...",
|
||||
ShortHelp: "Adds one or more signing keys to the tailnet key authority",
|
||||
Exec: func(ctx context.Context, args []string) error {
|
||||
return runNetworkLockModify(ctx, args, nil)
|
||||
},
|
||||
}
|
||||
|
||||
var nlRemoveCmd = &ffcli.Command{
|
||||
Name: "remove",
|
||||
ShortUsage: "remove <public-key>...",
|
||||
ShortHelp: "Removes one or more signing keys to the tailnet key authority",
|
||||
Exec: func(ctx context.Context, args []string) error {
|
||||
return runNetworkLockModify(ctx, nil, args)
|
||||
},
|
||||
}
|
||||
|
||||
// parseNLKeyArgs converts a slice of strings into a slice of tka.Key. The keys
|
||||
// should be specified using their key.NLPublic.MarshalText representation with
|
||||
// an optional '?<votes>' suffix. If any of the keys encounters an error, a nil
|
||||
// slice is returned along with an appropriate error.
|
||||
func parseNLKeyArgs(args []string) ([]tka.Key, error) {
|
||||
var keys []tka.Key
|
||||
for i, a := range args {
|
||||
var nlpk key.NLPublic
|
||||
spl := strings.SplitN(a, "?", 2)
|
||||
if err := nlpk.UnmarshalText([]byte(spl[0])); err != nil {
|
||||
return nil, fmt.Errorf("parsing key %d: %v", i+1, err)
|
||||
}
|
||||
|
||||
k := tka.Key{
|
||||
Kind: tka.Key25519,
|
||||
Public: nlpk.Verifier(),
|
||||
Votes: 1,
|
||||
}
|
||||
if len(spl) > 1 {
|
||||
votes, err := strconv.Atoi(spl[1])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parsing key %d votes: %v", i+1, err)
|
||||
}
|
||||
k.Votes = uint(votes)
|
||||
}
|
||||
keys = append(keys, k)
|
||||
}
|
||||
return keys, nil
|
||||
}
|
||||
|
||||
func runNetworkLockModify(ctx context.Context, addArgs, removeArgs []string) error {
|
||||
st, err := localClient.NetworkLockStatus(ctx)
|
||||
if err != nil {
|
||||
return fixTailscaledConnectError(err)
|
||||
}
|
||||
if st.Enabled {
|
||||
return errors.New("network-lock is already enabled")
|
||||
}
|
||||
|
||||
addKeys, err := parseNLKeyArgs(addArgs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
removeKeys, err := parseNLKeyArgs(removeArgs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
status, err := localClient.NetworkLockModify(ctx, addKeys, removeKeys)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf("Status: %+v\n\n", status)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -16,9 +16,8 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
riskTypes []string
|
||||
acceptedRisks string
|
||||
riskLoseSSH = registerRiskType("lose-ssh")
|
||||
riskTypes []string
|
||||
riskLoseSSH = registerRiskType("lose-ssh")
|
||||
)
|
||||
|
||||
func registerRiskType(riskType string) string {
|
||||
@@ -28,12 +27,13 @@ func registerRiskType(riskType string) string {
|
||||
|
||||
// registerAcceptRiskFlag registers the --accept-risk flag. Accepted risks are accounted for
|
||||
// in presentRiskToUser.
|
||||
func registerAcceptRiskFlag(f *flag.FlagSet) {
|
||||
f.StringVar(&acceptedRisks, "accept-risk", "", "accept risk and skip confirmation for risk types: "+strings.Join(riskTypes, ","))
|
||||
func registerAcceptRiskFlag(f *flag.FlagSet, acceptedRisks *string) {
|
||||
f.StringVar(acceptedRisks, "accept-risk", "", "accept risk and skip confirmation for risk types: "+strings.Join(riskTypes, ","))
|
||||
}
|
||||
|
||||
// riskAccepted reports whether riskType is in acceptedRisks.
|
||||
func riskAccepted(riskType string) bool {
|
||||
// isRiskAccepted reports whether riskType is in the comma-separated list of
|
||||
// risks in acceptedRisks.
|
||||
func isRiskAccepted(riskType, acceptedRisks string) bool {
|
||||
for _, r := range strings.Split(acceptedRisks, ",") {
|
||||
if r == riskType {
|
||||
return true
|
||||
@@ -49,12 +49,16 @@ var errAborted = errors.New("aborted, no changes made")
|
||||
// It is used by the presentRiskToUser function below.
|
||||
const riskAbortTimeSeconds = 5
|
||||
|
||||
// presentRiskToUser displays the risk message and waits for the user to
|
||||
// cancel. It returns errorAborted if the user aborts.
|
||||
func presentRiskToUser(riskType, riskMessage string) error {
|
||||
if riskAccepted(riskType) {
|
||||
// presentRiskToUser displays the risk message and waits for the user to cancel.
|
||||
// It returns errorAborted if the user aborts. In tests it returns errAborted
|
||||
// immediately unless the risk has been explicitly accepted.
|
||||
func presentRiskToUser(riskType, riskMessage, acceptedRisks string) error {
|
||||
if isRiskAccepted(riskType, acceptedRisks) {
|
||||
return nil
|
||||
}
|
||||
if inTest() {
|
||||
return errAborted
|
||||
}
|
||||
outln(riskMessage)
|
||||
printf("To skip this warning, use --accept-risk=%s\n", riskType)
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
|
||||
func findSSH() (string, error) {
|
||||
// use C:\Windows\System32\OpenSSH\ssh.exe since unexpected behavior
|
||||
// occured with ssh.exe provided by msys2/cygwin and other environments.
|
||||
// occurred with ssh.exe provided by msys2/cygwin and other environments.
|
||||
if systemRoot := os.Getenv("SystemRoot"); systemRoot != "" {
|
||||
exe := filepath.Join(systemRoot, "System32", "OpenSSH", "ssh.exe")
|
||||
if st, err := os.Stat(exe); err == nil && !st.IsDir() {
|
||||
|
||||
@@ -116,7 +116,7 @@ func newUpFlagSet(goos string, upArgs *upArgsT) *flag.FlagSet {
|
||||
upf.BoolVar(&upArgs.forceDaemon, "unattended", false, "run in \"Unattended Mode\" where Tailscale keeps running even after the current GUI user logs out (Windows-only)")
|
||||
}
|
||||
upf.DurationVar(&upArgs.timeout, "timeout", 0, "maximum amount of time to wait for tailscaled to enter a Running state; default (0s) blocks forever")
|
||||
registerAcceptRiskFlag(upf)
|
||||
registerAcceptRiskFlag(upf, &upArgs.acceptedRisks)
|
||||
return upf
|
||||
}
|
||||
|
||||
@@ -150,6 +150,7 @@ type upArgsT struct {
|
||||
opUser string
|
||||
json bool
|
||||
timeout time.Duration
|
||||
acceptedRisks string
|
||||
}
|
||||
|
||||
func (a upArgsT) getAuthKey() (string, error) {
|
||||
@@ -376,6 +377,20 @@ func updatePrefs(prefs, curPrefs *ipn.Prefs, env upCheckEnv) (simpleUp bool, jus
|
||||
return false, nil, fmt.Errorf("can't change --login-server without --force-reauth")
|
||||
}
|
||||
|
||||
// Do this after validations to avoid the 5s delay if we're going to error
|
||||
// out anyway.
|
||||
wantSSH, haveSSH := env.upArgs.runSSH, curPrefs.RunSSH
|
||||
if wantSSH != haveSSH && isSSHOverTailscale() {
|
||||
if wantSSH {
|
||||
err = presentRiskToUser(riskLoseSSH, `You are connected over Tailscale; this action will reroute SSH traffic to Tailscale SSH and will result in your session disconnecting.`, env.upArgs.acceptedRisks)
|
||||
} else {
|
||||
err = presentRiskToUser(riskLoseSSH, `You are connected using Tailscale SSH; this action will result in your session disconnecting.`, env.upArgs.acceptedRisks)
|
||||
}
|
||||
if err != nil {
|
||||
return false, nil, err
|
||||
}
|
||||
}
|
||||
|
||||
tagsChanged := !reflect.DeepEqual(curPrefs.AdvertiseTags, prefs.AdvertiseTags)
|
||||
|
||||
simpleUp = env.flagSet.NFlag() == 0 &&
|
||||
@@ -475,17 +490,6 @@ func runUp(ctx context.Context, args []string) (retErr error) {
|
||||
curExitNodeIP: exitNodeIP(curPrefs, st),
|
||||
}
|
||||
|
||||
if upArgs.runSSH != curPrefs.RunSSH && isSSHOverTailscale() {
|
||||
if upArgs.runSSH {
|
||||
err = presentRiskToUser(riskLoseSSH, `You are connected over Tailscale; this action will reroute SSH traffic to Tailscale SSH and will result in your session disconnecting.`)
|
||||
} else {
|
||||
err = presentRiskToUser(riskLoseSSH, `You are connected using Tailscale SSH; this action will result in your session disconnecting.`)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if retErr == nil {
|
||||
checkSSHUpWarnings(ctx)
|
||||
|
||||
@@ -15,7 +15,6 @@ import (
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
@@ -254,7 +253,7 @@ func qnapAuthnFinish(user, url string) (string, *qnapAuthResponse, error) {
|
||||
return "", nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
out, err := ioutil.ReadAll(resp.Body)
|
||||
out, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
@@ -100,6 +100,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
tailscale.com/util/groupmember from tailscale.com/cmd/tailscale/cli
|
||||
tailscale.com/util/lineread from tailscale.com/net/interfaces+
|
||||
tailscale.com/util/mak from tailscale.com/net/netcheck
|
||||
tailscale.com/util/multierr from tailscale.com/control/controlhttp
|
||||
tailscale.com/util/singleflight from tailscale.com/net/dnscache
|
||||
L tailscale.com/util/strs from tailscale.com/hostinfo
|
||||
W 💣 tailscale.com/util/winutil from tailscale.com/hostinfo+
|
||||
@@ -134,6 +135,8 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
LD golang.org/x/sys/unix from tailscale.com/net/netns+
|
||||
W golang.org/x/sys/windows from golang.org/x/sys/windows/registry+
|
||||
W golang.org/x/sys/windows/registry from golang.zx2c4.com/wireguard/windows/tunnel/winipcfg+
|
||||
W golang.org/x/sys/windows/svc from golang.org/x/sys/windows/svc/mgr+
|
||||
W golang.org/x/sys/windows/svc/mgr from tailscale.com/util/winutil
|
||||
golang.org/x/text/secure/bidirule from golang.org/x/net/idna
|
||||
golang.org/x/text/transform from golang.org/x/text/secure/bidirule+
|
||||
golang.org/x/text/unicode/bidi from golang.org/x/net/idna+
|
||||
|
||||
@@ -15,7 +15,6 @@ import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
@@ -89,6 +88,8 @@ func runMonitor(ctx context.Context, loop bool) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer mon.Close()
|
||||
|
||||
mon.RegisterChangeCallback(func(changed bool, st *interfaces.State) {
|
||||
if !changed {
|
||||
log.Printf("Link monitor fired; no change")
|
||||
@@ -163,7 +164,7 @@ func getURL(ctx context.Context, urlStr string) error {
|
||||
return res.Write(os.Stdout)
|
||||
}
|
||||
|
||||
func checkDerp(ctx context.Context, derpRegion string) error {
|
||||
func checkDerp(ctx context.Context, derpRegion string) (err error) {
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", ipn.DefaultControlURL+"/derpmap/default", nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("create derp map request: %w", err)
|
||||
@@ -173,7 +174,7 @@ func checkDerp(ctx context.Context, derpRegion string) error {
|
||||
return fmt.Errorf("fetch derp map failed: %w", err)
|
||||
}
|
||||
defer res.Body.Close()
|
||||
b, err := ioutil.ReadAll(io.LimitReader(res.Body, 1<<20))
|
||||
b, err := io.ReadAll(io.LimitReader(res.Body, 1<<20))
|
||||
if err != nil {
|
||||
return fmt.Errorf("fetch derp map failed: %w", err)
|
||||
}
|
||||
@@ -202,6 +203,12 @@ func checkDerp(ctx context.Context, derpRegion string) error {
|
||||
|
||||
c1 := derphttp.NewRegionClient(priv1, log.Printf, getRegion)
|
||||
c2 := derphttp.NewRegionClient(priv2, log.Printf, getRegion)
|
||||
defer func() {
|
||||
if err != nil {
|
||||
c1.Close()
|
||||
c2.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
c2.NotePreferred(true) // just to open it
|
||||
|
||||
|
||||
@@ -109,7 +109,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
github.com/tailscale/goupnp/scpd from github.com/tailscale/goupnp
|
||||
github.com/tailscale/goupnp/soap from github.com/tailscale/goupnp+
|
||||
github.com/tailscale/goupnp/ssdp from github.com/tailscale/goupnp
|
||||
L 💣 github.com/tailscale/netlink from tailscale.com/wgengine/router
|
||||
L 💣 github.com/tailscale/netlink from tailscale.com/wgengine/router+
|
||||
github.com/tcnksm/go-httpstat from tailscale.com/net/netcheck
|
||||
LD github.com/u-root/u-root/pkg/termios from tailscale.com/ssh/tailssh
|
||||
L github.com/u-root/uio/rand from github.com/insomniacslk/dhcp/dhcpv4
|
||||
@@ -190,6 +190,8 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/derp from tailscale.com/derp/derphttp+
|
||||
tailscale.com/derp/derphttp from tailscale.com/net/netcheck+
|
||||
tailscale.com/disco from tailscale.com/derp+
|
||||
tailscale.com/doctor from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/doctor/routetable from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/envknob from tailscale.com/control/controlclient+
|
||||
tailscale.com/health from tailscale.com/control/controlclient+
|
||||
tailscale.com/hostinfo from tailscale.com/control/controlclient+
|
||||
@@ -230,6 +232,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/net/ping from tailscale.com/net/netcheck
|
||||
tailscale.com/net/portmapper from tailscale.com/net/netcheck+
|
||||
tailscale.com/net/proxymux from tailscale.com/cmd/tailscaled
|
||||
tailscale.com/net/routetable from tailscale.com/doctor/routetable
|
||||
tailscale.com/net/socks5 from tailscale.com/cmd/tailscaled
|
||||
tailscale.com/net/stun from tailscale.com/net/netcheck+
|
||||
tailscale.com/net/tlsdial from tailscale.com/control/controlclient+
|
||||
@@ -237,6 +240,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/net/tsdial from tailscale.com/control/controlclient+
|
||||
💣 tailscale.com/net/tshttpproxy from tailscale.com/control/controlclient+
|
||||
tailscale.com/net/tstun from tailscale.com/net/dns+
|
||||
tailscale.com/net/tunstats from tailscale.com/net/tstun+
|
||||
tailscale.com/paths from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/portlist from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/safesocket from tailscale.com/client/tailscale+
|
||||
@@ -272,6 +276,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
💣 tailscale.com/util/deephash from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/util/dnsname from tailscale.com/hostinfo+
|
||||
LW tailscale.com/util/endian from tailscale.com/net/dns+
|
||||
tailscale.com/util/goroutines from tailscale.com/control/controlclient+
|
||||
tailscale.com/util/groupmember from tailscale.com/ipn/ipnserver
|
||||
💣 tailscale.com/util/hashx from tailscale.com/util/deephash
|
||||
tailscale.com/util/lineread from tailscale.com/hostinfo+
|
||||
@@ -292,6 +297,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
tailscale.com/wgengine/filter from tailscale.com/control/controlclient+
|
||||
💣 tailscale.com/wgengine/magicsock from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/wgengine/monitor from tailscale.com/control/controlclient+
|
||||
tailscale.com/wgengine/netlog from tailscale.com/wgengine
|
||||
tailscale.com/wgengine/netstack from tailscale.com/cmd/tailscaled+
|
||||
tailscale.com/wgengine/router from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/wgengine/wgcfg from tailscale.com/ipn/ipnlocal+
|
||||
@@ -317,6 +323,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
golang.org/x/crypto/salsa20/salsa from golang.org/x/crypto/nacl/box+
|
||||
LD golang.org/x/crypto/ssh from tailscale.com/ssh/tailssh+
|
||||
golang.org/x/exp/constraints from golang.org/x/exp/slices
|
||||
golang.org/x/exp/maps from tailscale.com/wgengine
|
||||
golang.org/x/exp/slices from tailscale.com/ipn/ipnlocal+
|
||||
golang.org/x/net/bpf from github.com/mdlayher/genetlink+
|
||||
golang.org/x/net/dns/dnsmessage from net+
|
||||
@@ -338,7 +345,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
|
||||
W golang.org/x/sys/windows/registry from golang.org/x/sys/windows/svc/eventlog+
|
||||
W golang.org/x/sys/windows/svc from golang.org/x/sys/windows/svc/mgr+
|
||||
W golang.org/x/sys/windows/svc/eventlog from tailscale.com/cmd/tailscaled
|
||||
W golang.org/x/sys/windows/svc/mgr from tailscale.com/cmd/tailscaled
|
||||
W golang.org/x/sys/windows/svc/mgr from tailscale.com/cmd/tailscaled+
|
||||
golang.org/x/term from tailscale.com/logpolicy
|
||||
golang.org/x/text/secure/bidirule from golang.org/x/net/idna
|
||||
golang.org/x/text/transform from golang.org/x/text/secure/bidirule+
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"io/fs"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
@@ -84,6 +84,13 @@ func uninstallSystemDaemonDarwin(args []string) (ret error) {
|
||||
ret = err
|
||||
}
|
||||
}
|
||||
|
||||
// Do not delete targetBin if it's a symlink, which happens if it was installed via
|
||||
// Homebrew.
|
||||
if isSymlink(targetBin) {
|
||||
return ret
|
||||
}
|
||||
|
||||
if err := os.Remove(targetBin); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
err = nil
|
||||
@@ -108,41 +115,25 @@ func installSystemDaemonDarwin(args []string) (err error) {
|
||||
// Best effort:
|
||||
uninstallSystemDaemonDarwin(nil)
|
||||
|
||||
// Copy ourselves to /usr/local/bin/tailscaled.
|
||||
if err := os.MkdirAll(filepath.Dir(targetBin), 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
exe, err := os.Executable()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to find our own executable path: %w", err)
|
||||
}
|
||||
tmpBin := targetBin + ".tmp"
|
||||
f, err := os.Create(tmpBin)
|
||||
|
||||
same, err := sameFile(exe, targetBin)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
self, err := os.Open(exe)
|
||||
if err != nil {
|
||||
f.Close()
|
||||
return err
|
||||
}
|
||||
_, err = io.Copy(f, self)
|
||||
self.Close()
|
||||
if err != nil {
|
||||
f.Close()
|
||||
return err
|
||||
}
|
||||
if err := f.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.Chmod(tmpBin, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.Rename(tmpBin, targetBin); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := ioutil.WriteFile(sysPlist, []byte(darwinLaunchdPlist), 0700); err != nil {
|
||||
// Do not overwrite targetBin with the binary file if it it's already
|
||||
// pointing to it. This is primarily to handle Homebrew that writes
|
||||
// /usr/local/bin/tailscaled is a symlink to the actual binary.
|
||||
if !same {
|
||||
if err := copyBinary(exe, targetBin); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := os.WriteFile(sysPlist, []byte(darwinLaunchdPlist), 0700); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -156,3 +147,55 @@ func installSystemDaemonDarwin(args []string) (err error) {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// copyBinary copies binary file `src` into `dst`.
|
||||
func copyBinary(src, dst string) error {
|
||||
if err := os.MkdirAll(filepath.Dir(dst), 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
tmpBin := dst + ".tmp"
|
||||
f, err := os.Create(tmpBin)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
srcf, err := os.Open(src)
|
||||
if err != nil {
|
||||
f.Close()
|
||||
return err
|
||||
}
|
||||
_, err = io.Copy(f, srcf)
|
||||
srcf.Close()
|
||||
if err != nil {
|
||||
f.Close()
|
||||
return err
|
||||
}
|
||||
if err := f.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.Chmod(tmpBin, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.Rename(tmpBin, dst); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func isSymlink(path string) bool {
|
||||
fi, err := os.Lstat(path)
|
||||
return err == nil && (fi.Mode()&os.ModeSymlink == os.ModeSymlink)
|
||||
}
|
||||
|
||||
// sameFile returns true if both file paths exist and resolve to the same file.
|
||||
func sameFile(path1, path2 string) (bool, error) {
|
||||
dst1, err := filepath.EvalSymlinks(path1)
|
||||
if err != nil && !errors.Is(err, fs.ErrNotExist) {
|
||||
return false, fmt.Errorf("EvalSymlinks(%s): %w", path1, err)
|
||||
}
|
||||
dst2, err := filepath.EvalSymlinks(path2)
|
||||
if err != nil && !errors.Is(err, fs.ErrNotExist) {
|
||||
return false, fmt.Errorf("EvalSymlinks(%s): %w", path2, err)
|
||||
}
|
||||
return dst1 == dst2, nil
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ import (
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
@@ -87,7 +88,7 @@ func defaultTunName() string {
|
||||
// see https://github.com/tailscale/tailscale/issues/391
|
||||
//
|
||||
// But Gokrazy does have the tun module built-in, so users
|
||||
// can stil run --tun=tailscale0 if they wish, if they
|
||||
// can still run --tun=tailscale0 if they wish, if they
|
||||
// arrange for iptables to be present or run in "tailscale
|
||||
// up --netfilter-mode=off" mode, perhaps. Untested.
|
||||
return "userspace-networking"
|
||||
@@ -97,6 +98,20 @@ func defaultTunName() string {
|
||||
return "tailscale0"
|
||||
}
|
||||
|
||||
// defaultPort returns the default UDP port to listen on for disco+wireguard.
|
||||
// By default it returns 0, to pick one randomly from the kernel.
|
||||
// If the environment variable PORT is set, that's used instead.
|
||||
// The PORT environment variable is chosen to match what the Linux systemd
|
||||
// unit uses, to make documentation more consistent.
|
||||
func defaultPort() uint16 {
|
||||
if s := envknob.String("PORT"); s != "" {
|
||||
if p, err := strconv.ParseUint(s, 10, 16); err == nil {
|
||||
return uint16(p)
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
var args struct {
|
||||
// tunname is a /dev/net/tun tunnel name ("tailscale0"), the
|
||||
// string "userspace-networking", "tap:TAPNAME[:BRIDGENAME]"
|
||||
@@ -132,6 +147,9 @@ var subCommands = map[string]*func([]string) error{
|
||||
var beCLI func() // non-nil if CLI is linked in
|
||||
|
||||
func main() {
|
||||
envknob.PanicIfAnyEnvCheckedInInit()
|
||||
envknob.ApplyDiskConfig()
|
||||
|
||||
printVersion := false
|
||||
flag.IntVar(&args.verbose, "verbose", 0, "log verbosity level; 0 is default, 1 or higher are increasingly verbose")
|
||||
flag.BoolVar(&args.cleanup, "cleanup", false, "clean up system state and exit")
|
||||
@@ -139,8 +157,8 @@ func main() {
|
||||
flag.StringVar(&args.socksAddr, "socks5-server", "", `optional [ip]:port to run a SOCK5 server (e.g. "localhost:1080")`)
|
||||
flag.StringVar(&args.httpProxyAddr, "outbound-http-proxy-listen", "", `optional [ip]:port to run an outbound HTTP proxy (e.g. "localhost:8080")`)
|
||||
flag.StringVar(&args.tunname, "tun", defaultTunName(), `tunnel interface name; use "userspace-networking" (beta) to not use TUN`)
|
||||
flag.Var(flagtype.PortValue(&args.port, 0), "port", "UDP port to listen on for WireGuard and peer-to-peer traffic; 0 means automatically select")
|
||||
flag.StringVar(&args.statepath, "state", "", "absolute path of state file; use 'kube:<secret-name>' to use Kubernetes secrets or 'arn:aws:ssm:...' to store in AWS SSM; use 'mem:' to not store state and register as an emphemeral node. If empty and --statedir is provided, the default is <statedir>/tailscaled.state. Default: "+paths.DefaultTailscaledStateFile())
|
||||
flag.Var(flagtype.PortValue(&args.port, defaultPort()), "port", "UDP port to listen on for WireGuard and peer-to-peer traffic; 0 means automatically select")
|
||||
flag.StringVar(&args.statepath, "state", "", "absolute path of state file; use 'kube:<secret-name>' to use Kubernetes secrets or 'arn:aws:ssm:...' to store in AWS SSM; use 'mem:' to not store state and register as an ephemeral node. If empty and --statedir is provided, the default is <statedir>/tailscaled.state. Default: "+paths.DefaultTailscaledStateFile())
|
||||
flag.StringVar(&args.statedir, "statedir", "", "path to directory for storage of config state, TLS certs, temporary incoming Taildrop files, etc. If empty, it's derived from --state when possible.")
|
||||
flag.StringVar(&args.socketpath, "socket", paths.DefaultTailscaledSocket(), "path of the service unix socket")
|
||||
flag.StringVar(&args.birdSocketPath, "bird-socket", "", "path of the bird unix socket")
|
||||
@@ -308,6 +326,10 @@ func run() error {
|
||||
pol.Shutdown(ctx)
|
||||
}()
|
||||
|
||||
if err := envknob.ApplyDiskConfigError(); err != nil {
|
||||
log.Printf("Error reading environment config: %v", err)
|
||||
}
|
||||
|
||||
if isWindowsService() {
|
||||
// Run the IPN server from the Windows service manager.
|
||||
log.Printf("Running service...")
|
||||
@@ -353,8 +375,7 @@ func run() error {
|
||||
|
||||
socksListener, httpProxyListener := mustStartProxyListeners(args.socksAddr, args.httpProxyAddr)
|
||||
|
||||
dialer := new(tsdial.Dialer) // mutated below (before used)
|
||||
dialer.Logf = logf
|
||||
dialer := &tsdial.Dialer{Logf: logf} // mutated below (before used)
|
||||
e, useNetstack, err := createEngine(logf, linkMon, dialer)
|
||||
if err != nil {
|
||||
return fmt.Errorf("createEngine: %w", err)
|
||||
@@ -376,7 +397,7 @@ func run() error {
|
||||
return fmt.Errorf("newNetstack: %w", err)
|
||||
}
|
||||
ns.ProcessLocalIPs = useNetstack
|
||||
ns.ProcessSubnets = useNetstack || wrapNetstack
|
||||
ns.ProcessSubnets = useNetstack || shouldWrapNetstack()
|
||||
|
||||
if useNetstack {
|
||||
dialer.UseNetstackForIP = func(ip netip.Addr) bool {
|
||||
@@ -477,8 +498,6 @@ func createEngine(logf logger.Logf, linkMon *monitor.Mon, dialer *tsdial.Dialer)
|
||||
return nil, false, multierr.New(errs...)
|
||||
}
|
||||
|
||||
var wrapNetstack = shouldWrapNetstack()
|
||||
|
||||
func shouldWrapNetstack() bool {
|
||||
if v, ok := envknob.LookupBool("TS_DEBUG_WRAP_NETSTACK"); ok {
|
||||
return v
|
||||
@@ -545,11 +564,13 @@ func tryEngine(logf logger.Logf, linkMon *monitor.Mon, dialer *tsdial.Dialer, na
|
||||
}
|
||||
d, err := dns.NewOSConfigurator(logf, devName)
|
||||
if err != nil {
|
||||
dev.Close()
|
||||
r.Close()
|
||||
return nil, false, fmt.Errorf("dns.NewOSConfigurator: %w", err)
|
||||
}
|
||||
conf.DNS = d
|
||||
conf.Router = r
|
||||
if wrapNetstack {
|
||||
if shouldWrapNetstack() {
|
||||
conf.Router = netstack.NewSubnetRouterWrapper(conf.Router)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ After=network-pre.target NetworkManager.service systemd-resolved.service
|
||||
[Service]
|
||||
EnvironmentFile=/etc/default/tailscaled
|
||||
ExecStartPre=/usr/sbin/tailscaled --cleanup
|
||||
ExecStart=/usr/sbin/tailscaled --state=/var/lib/tailscale/tailscaled.state --socket=/run/tailscale/tailscaled.sock --port $PORT $FLAGS
|
||||
ExecStart=/usr/sbin/tailscaled --state=/var/lib/tailscale/tailscaled.state --socket=/run/tailscale/tailscaled.sock --port=${PORT} $FLAGS
|
||||
ExecStopPost=/usr/sbin/tailscaled --cleanup
|
||||
|
||||
Restart=on-failure
|
||||
|
||||
@@ -23,6 +23,7 @@ package main // import "tailscale.com/cmd/tailscaled"
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/netip"
|
||||
@@ -192,11 +193,14 @@ func beWindowsSubprocess() bool {
|
||||
}
|
||||
logid := os.Args[2]
|
||||
|
||||
// Remove the date/time prefix; the logtail + file logggers add it.
|
||||
// Remove the date/time prefix; the logtail + file loggers add it.
|
||||
log.SetFlags(0)
|
||||
|
||||
log.Printf("Program starting: v%v: %#v", version.Long, os.Args)
|
||||
log.Printf("subproc mode: logid=%v", logid)
|
||||
if err := envknob.ApplyDiskConfigError(); err != nil {
|
||||
log.Printf("Error reading environment config: %v", err)
|
||||
}
|
||||
|
||||
go func() {
|
||||
b := make([]byte, 16)
|
||||
@@ -262,11 +266,17 @@ func startIPNServer(ctx context.Context, logid string) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("monitor: %w", err)
|
||||
}
|
||||
dialer := new(tsdial.Dialer)
|
||||
dialer := &tsdial.Dialer{Logf: logf}
|
||||
|
||||
getEngineRaw := func() (wgengine.Engine, *netstack.Impl, error) {
|
||||
dev, devName, err := tstun.New(logf, "Tailscale")
|
||||
if err != nil {
|
||||
if errors.Is(err, windows.ERROR_DEVICE_NOT_AVAILABLE) {
|
||||
// Wintun is not installing correctly. Dump the state of NetSetupSvc
|
||||
// (which is a user-mode service that must be active for network devices
|
||||
// to install) and its dependencies to the log.
|
||||
winutil.LogSvcState(logf, "NetSetupSvc")
|
||||
}
|
||||
return nil, nil, fmt.Errorf("TUN: %w", err)
|
||||
}
|
||||
r, err := router.New(logf, dev, nil)
|
||||
@@ -274,7 +284,7 @@ func startIPNServer(ctx context.Context, logid string) error {
|
||||
dev.Close()
|
||||
return nil, nil, fmt.Errorf("router: %w", err)
|
||||
}
|
||||
if wrapNetstack {
|
||||
if shouldWrapNetstack() {
|
||||
r = netstack.NewSubnetRouterWrapper(r)
|
||||
}
|
||||
d, err := dns.NewOSConfigurator(logf, devName)
|
||||
@@ -301,7 +311,7 @@ func startIPNServer(ctx context.Context, logid string) error {
|
||||
return nil, nil, fmt.Errorf("newNetstack: %w", err)
|
||||
}
|
||||
ns.ProcessLocalIPs = false
|
||||
ns.ProcessSubnets = wrapNetstack
|
||||
ns.ProcessSubnets = shouldWrapNetstack()
|
||||
if err := ns.Start(); err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to start netstack: %w", err)
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ package main
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
@@ -47,7 +46,7 @@ func runBuild() {
|
||||
if err != nil {
|
||||
log.Fatalf("Cannot fix esbuild metadata paths: %v", err)
|
||||
}
|
||||
if err := ioutil.WriteFile(path.Join(*distDir, "/esbuild-metadata.json"), metadataBytes, 0666); err != nil {
|
||||
if err := os.WriteFile(path.Join(*distDir, "/esbuild-metadata.json"), metadataBytes, 0666); err != nil {
|
||||
log.Fatalf("Cannot write metadata: %v", err)
|
||||
}
|
||||
|
||||
@@ -58,7 +57,7 @@ func runBuild() {
|
||||
|
||||
// fixEsbuildMetadataPaths re-keys the esbuild metadata file to use paths
|
||||
// relative to the dist directory (it normally uses paths relative to the cwd,
|
||||
// which are akward if we're running with a different cwd at serving time).
|
||||
// which are awkward if we're running with a different cwd at serving time).
|
||||
func fixEsbuildMetadataPaths(metadataStr string) ([]byte, error) {
|
||||
var metadata EsbuildMetadata
|
||||
if err := json.Unmarshal([]byte(metadataStr), &metadata); err != nil {
|
||||
|
||||
@@ -6,7 +6,6 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
@@ -183,12 +182,17 @@ func setupEsbuildWasm(build esbuild.PluginBuild, dev bool) {
|
||||
|
||||
func buildWasm(dev bool) ([]byte, error) {
|
||||
start := time.Now()
|
||||
outputFile, err := ioutil.TempFile("", "main.*.wasm")
|
||||
outputFile, err := os.CreateTemp("", "main.*.wasm")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Cannot create main.wasm output file: %w", err)
|
||||
}
|
||||
outputPath := outputFile.Name()
|
||||
|
||||
defer os.Remove(outputPath)
|
||||
// Running defer (*os.File).Close() in defer order before os.Remove
|
||||
// because on some systems like Windows, it is possible for os.Remove
|
||||
// to fail for unclosed files.
|
||||
defer outputFile.Close()
|
||||
|
||||
args := []string{"build", "-tags", "tailscale_go,osusergo,netgo,nethttpomithttp2,omitidna,omitpemdecrypt"}
|
||||
if !dev {
|
||||
|
||||
@@ -10,9 +10,9 @@
|
||||
"qrcode": "^1.5.0",
|
||||
"tailwindcss": "^3.1.6",
|
||||
"typescript": "^4.7.4",
|
||||
"xterm": "5.0.0-beta.58",
|
||||
"xterm-addon-fit": "^0.5.0",
|
||||
"xterm-addon-web-links": "0.7.0-beta.6"
|
||||
"xterm": "^5.0.0",
|
||||
"xterm-addon-fit": "^0.6.0",
|
||||
"xterm-addon-web-links": "^0.7.0"
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "tsc --noEmit",
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"author": "Tailscale Inc.",
|
||||
"description": "Tailscale Connect SDK",
|
||||
"license": "BSD-3-Clause",
|
||||
"name": "tailscale-connect",
|
||||
"name": "@tailscale/connect",
|
||||
"type": "module",
|
||||
"main": "./pkg.js",
|
||||
"types": "./pkg.d.ts",
|
||||
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
@@ -75,7 +74,7 @@ func generateServeIndex(distFS fs.FS) ([]byte, error) {
|
||||
return nil, fmt.Errorf("Could not open esbuild-metadata.json: %w", err)
|
||||
}
|
||||
defer esbuildMetadataFile.Close()
|
||||
esbuildMetadataBytes, err := ioutil.ReadAll(esbuildMetadataFile)
|
||||
esbuildMetadataBytes, err := io.ReadAll(esbuildMetadataFile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Could not read esbuild-metadata.json: %w", err)
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ function SSHSession({
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
useEffect(() => {
|
||||
if (ref.current) {
|
||||
runSSHSession(ref.current, def, ipn, onDone)
|
||||
runSSHSession(ref.current, def, ipn, onDone, (err) => console.error(err))
|
||||
}
|
||||
}, [ref])
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@ import { WebLinksAddon } from "xterm-addon-web-links"
|
||||
export type SSHSessionDef = {
|
||||
username: string
|
||||
hostname: string
|
||||
/** Defaults to 5 seconds */
|
||||
timeoutSeconds?: number
|
||||
}
|
||||
|
||||
export function runSSHSession(
|
||||
@@ -12,6 +14,7 @@ export function runSSHSession(
|
||||
def: SSHSessionDef,
|
||||
ipn: IPN,
|
||||
onDone: () => void,
|
||||
onError?: (err: string) => void,
|
||||
terminalOptions?: ITerminalOptions
|
||||
) {
|
||||
const parentWindow = termContainerNode.ownerDocument.defaultView ?? window
|
||||
@@ -39,14 +42,14 @@ export function runSSHSession(
|
||||
term.focus()
|
||||
|
||||
let resizeObserver: ResizeObserver | undefined
|
||||
let handleBeforeUnload: ((e: BeforeUnloadEvent) => void) | undefined
|
||||
let handleUnload: ((e: Event) => void) | undefined
|
||||
|
||||
const sshSession = ipn.ssh(def.hostname, def.username, {
|
||||
writeFn(input) {
|
||||
term.write(input)
|
||||
},
|
||||
writeErrorFn(err) {
|
||||
console.error(err)
|
||||
onError?.(err)
|
||||
term.write(err)
|
||||
},
|
||||
setReadFn(hook) {
|
||||
@@ -57,11 +60,12 @@ export function runSSHSession(
|
||||
onDone() {
|
||||
resizeObserver?.disconnect()
|
||||
term.dispose()
|
||||
if (handleBeforeUnload) {
|
||||
parentWindow.removeEventListener("beforeunload", handleBeforeUnload)
|
||||
if (handleUnload) {
|
||||
parentWindow.removeEventListener("unload", handleUnload)
|
||||
}
|
||||
onDone()
|
||||
},
|
||||
timeoutSeconds: def.timeoutSeconds,
|
||||
})
|
||||
|
||||
// Make terminal and SSH session track the size of the containing DOM node.
|
||||
@@ -71,6 +75,6 @@ export function runSSHSession(
|
||||
|
||||
// Close the session if the user closes the window without an explicit
|
||||
// exit.
|
||||
handleBeforeUnload = () => sshSession.close()
|
||||
parentWindow.addEventListener("beforeunload", handleBeforeUnload)
|
||||
handleUnload = () => sshSession.close()
|
||||
parentWindow.addEventListener("unload", handleUnload)
|
||||
}
|
||||
|
||||
@@ -15,12 +15,12 @@ import wasmURL from "./main.wasm"
|
||||
* needed for the package to function.
|
||||
*/
|
||||
type IPNPackageConfig = IPNConfig & {
|
||||
// Auth key used to intitialize the Tailscale client (required)
|
||||
// Auth key used to initialize the Tailscale client (required)
|
||||
authKey: string
|
||||
// URL of the main.wasm file that is included in the page, if it is not
|
||||
// accessible via a relative URL.
|
||||
wasmURL?: string
|
||||
// Funtion invoked if the Go process panics or unexpectedly exits.
|
||||
// Function invoked if the Go process panics or unexpectedly exits.
|
||||
panicHandler: (err: string) => void
|
||||
}
|
||||
|
||||
|
||||
2
cmd/tsconnect/src/types/wasm_js.d.ts
vendored
2
cmd/tsconnect/src/types/wasm_js.d.ts
vendored
@@ -23,6 +23,8 @@ declare global {
|
||||
setReadFn: (readFn: (data: string) => void) => void
|
||||
rows: number
|
||||
cols: number
|
||||
/** Defaults to 5 seconds */
|
||||
timeoutSeconds?: number
|
||||
onDone: () => void
|
||||
}
|
||||
): IPNSSHSession
|
||||
|
||||
@@ -96,7 +96,7 @@ func newIPN(jsConfig js.Value) map[string]any {
|
||||
logtail := logtail.NewLogger(c, log.Printf)
|
||||
logf := logtail.Logf
|
||||
|
||||
dialer := new(tsdial.Dialer)
|
||||
dialer := &tsdial.Dialer{Logf: logf}
|
||||
eng, err := wgengine.NewUserspaceEngine(logf, wgengine.Config{
|
||||
Dialer: dialer,
|
||||
})
|
||||
@@ -360,6 +360,10 @@ func (s *jsSSHSession) Run() {
|
||||
setReadFn := s.termConfig.Get("setReadFn")
|
||||
rows := s.termConfig.Get("rows").Int()
|
||||
cols := s.termConfig.Get("cols").Int()
|
||||
timeoutSeconds := 5.0
|
||||
if jsTimeoutSeconds := s.termConfig.Get("timeoutSeconds"); jsTimeoutSeconds.Type() == js.TypeNumber {
|
||||
timeoutSeconds = jsTimeoutSeconds.Float()
|
||||
}
|
||||
onDone := s.termConfig.Get("onDone")
|
||||
defer onDone.Invoke()
|
||||
|
||||
@@ -367,7 +371,7 @@ func (s *jsSSHSession) Run() {
|
||||
writeErrorFn.Invoke(fmt.Sprintf("%s Error: %v\r\n", label, err))
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(timeoutSeconds*float64(time.Second)))
|
||||
defer cancel()
|
||||
c, err := s.jsIPN.dialer.UserDial(ctx, "tcp", net.JoinHostPort(s.host, "22"))
|
||||
if err != nil {
|
||||
|
||||
@@ -639,20 +639,20 @@ xtend@^4.0.2:
|
||||
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
|
||||
integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==
|
||||
|
||||
xterm-addon-fit@^0.5.0:
|
||||
version "0.5.0"
|
||||
resolved "https://registry.yarnpkg.com/xterm-addon-fit/-/xterm-addon-fit-0.5.0.tgz#2d51b983b786a97dcd6cde805e700c7f913bc596"
|
||||
integrity sha512-DsS9fqhXHacEmsPxBJZvfj2la30Iz9xk+UKjhQgnYNkrUIN5CYLbw7WEfz117c7+S86S/tpHPfvNxJsF5/G8wQ==
|
||||
xterm-addon-fit@^0.6.0:
|
||||
version "0.6.0"
|
||||
resolved "https://registry.yarnpkg.com/xterm-addon-fit/-/xterm-addon-fit-0.6.0.tgz#142e1ce181da48763668332593fc440349c88c34"
|
||||
integrity sha512-9/7A+1KEjkFam0yxTaHfuk9LEvvTSBi0PZmEkzJqgafXPEXL9pCMAVV7rB09sX6ATRDXAdBpQhZkhKj7CGvYeg==
|
||||
|
||||
xterm@5.0.0-beta.58:
|
||||
version "5.0.0-beta.58"
|
||||
resolved "https://registry.yarnpkg.com/xterm/-/xterm-5.0.0-beta.58.tgz#e3e96ab9fd24d006ec16cc9351a060cc79e67e80"
|
||||
integrity sha512-gjg39oKdgUKful27+7I1hvSK51lu/LRhdimFhfZyMvdk0iATH0FAfzv1eAvBKWY2UBgYUfxhicTkanYioANdMw==
|
||||
xterm-addon-web-links@^0.7.0:
|
||||
version "0.7.0"
|
||||
resolved "https://registry.yarnpkg.com/xterm-addon-web-links/-/xterm-addon-web-links-0.7.0.tgz#dceac36170605f9db10a01d716bd83ee38f65c17"
|
||||
integrity sha512-6PqoqzzPwaeSq22skzbvyboDvSnYk5teUYEoKBwMYvhbkwOQkemZccjWHT5FnNA8o1aInTc4PRYAl4jjPucCKA==
|
||||
|
||||
xterm-addon-web-links@0.7.0-beta.6:
|
||||
version "0.7.0-beta.6"
|
||||
resolved "https://registry.yarnpkg.com/xterm-addon-web-links/-/xterm-addon-web-links-0.7.0-beta.6.tgz#ec63b681b4f0f0135fa039f53664f65fe9d9f43a"
|
||||
integrity sha512-nD/r/GchGTN4c9gAIVLWVoxExTzAUV7E9xZnwsvhuwI4CEE6yqO15ns8g2hdcUrsPyCbNEw05mIrkF6W5Yj8qA==
|
||||
xterm@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/xterm/-/xterm-5.0.0.tgz#0af50509b33d0dc62fde7a4ec17750b8e453cc5c"
|
||||
integrity sha512-tmVsKzZovAYNDIaUinfz+VDclraQpPUnAME+JawosgWRMphInDded/PuY0xmU5dOhyeYZsI0nz5yd8dPYsdLTA==
|
||||
|
||||
y18n@^4.0.0:
|
||||
version "4.0.3"
|
||||
|
||||
@@ -388,7 +388,7 @@ func main() {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if runCloner {
|
||||
// When a new pacakge is added or when existing generated files have
|
||||
// When a new package is added or when existing generated files have
|
||||
// been deleted, we might run into a case where tailscale.com/cmd/cloner
|
||||
// has not run yet. We detect this by verifying that all the structs we
|
||||
// interacted with have had Clone method already generated. If they
|
||||
|
||||
@@ -88,11 +88,17 @@ func New(opts Options) (*Auto, error) {
|
||||
}
|
||||
|
||||
// NewNoStart creates a new Auto, but without calling Start on it.
|
||||
func NewNoStart(opts Options) (*Auto, error) {
|
||||
func NewNoStart(opts Options) (_ *Auto, err error) {
|
||||
direct, err := NewDirect(opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() {
|
||||
if err != nil {
|
||||
direct.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
if opts.Status == nil {
|
||||
return nil, errors.New("missing required Options.Status")
|
||||
}
|
||||
@@ -114,19 +120,11 @@ func NewNoStart(opts Options) (*Auto, error) {
|
||||
}
|
||||
c.authCtx, c.authCancel = context.WithCancel(context.Background())
|
||||
c.mapCtx, c.mapCancel = context.WithCancel(context.Background())
|
||||
c.unregisterHealthWatch = health.RegisterWatcher(c.onHealthChange)
|
||||
c.unregisterHealthWatch = health.RegisterWatcher(direct.ReportHealthChange)
|
||||
return c, nil
|
||||
|
||||
}
|
||||
|
||||
func (c *Auto) onHealthChange(sys health.Subsystem, err error) {
|
||||
if sys == health.SysOverall {
|
||||
return
|
||||
}
|
||||
c.logf("controlclient: restarting map request for %q health change to new state: %v", sys, err)
|
||||
c.cancelMapSafely()
|
||||
}
|
||||
|
||||
// SetPaused controls whether HTTP activity should be paused.
|
||||
//
|
||||
// The client can be paused and unpaused repeatedly, unlike Start and Shutdown, which can only be used once.
|
||||
|
||||
@@ -8,12 +8,11 @@ import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"tailscale.com/util/goroutines"
|
||||
)
|
||||
|
||||
func dumpGoroutinesToURL(c *http.Client, targetURL string) {
|
||||
@@ -22,7 +21,7 @@ func dumpGoroutinesToURL(c *http.Client, targetURL string) {
|
||||
|
||||
zbuf := new(bytes.Buffer)
|
||||
zw := gzip.NewWriter(zbuf)
|
||||
zw.Write(scrubbedGoroutineDump())
|
||||
zw.Write(goroutines.ScrubbedGoroutineDump())
|
||||
zw.Close()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "PUT", targetURL, zbuf)
|
||||
@@ -40,83 +39,3 @@ func dumpGoroutinesToURL(c *http.Client, targetURL string) {
|
||||
log.Printf("dumpGoroutinesToURL complete to %v (after %v)", targetURL, d)
|
||||
}
|
||||
}
|
||||
|
||||
// scrubbedGoroutineDump returns the list of all current goroutines, but with the actual
|
||||
// values of arguments scrubbed out, lest it contain some private key material.
|
||||
func scrubbedGoroutineDump() []byte {
|
||||
var buf []byte
|
||||
// Grab stacks multiple times into increasingly larger buffer sizes
|
||||
// to minimize the risk that we blow past our iOS memory limit.
|
||||
for size := 1 << 10; size <= 1<<20; size += 1 << 10 {
|
||||
buf = make([]byte, size)
|
||||
buf = buf[:runtime.Stack(buf, true)]
|
||||
if len(buf) < size {
|
||||
// It fit.
|
||||
break
|
||||
}
|
||||
}
|
||||
return scrubHex(buf)
|
||||
}
|
||||
|
||||
func scrubHex(buf []byte) []byte {
|
||||
saw := map[string][]byte{} // "0x123" => "v1%3" (unique value 1 and its value mod 8)
|
||||
|
||||
foreachHexAddress(buf, func(in []byte) {
|
||||
if string(in) == "0x0" {
|
||||
return
|
||||
}
|
||||
if v, ok := saw[string(in)]; ok {
|
||||
for i := range in {
|
||||
in[i] = '_'
|
||||
}
|
||||
copy(in, v)
|
||||
return
|
||||
}
|
||||
inStr := string(in)
|
||||
u64, err := strconv.ParseUint(string(in[2:]), 16, 64)
|
||||
for i := range in {
|
||||
in[i] = '_'
|
||||
}
|
||||
if err != nil {
|
||||
in[0] = '?'
|
||||
return
|
||||
}
|
||||
v := []byte(fmt.Sprintf("v%d%%%d", len(saw)+1, u64%8))
|
||||
saw[inStr] = v
|
||||
copy(in, v)
|
||||
})
|
||||
return buf
|
||||
}
|
||||
|
||||
var ohx = []byte("0x")
|
||||
|
||||
// foreachHexAddress calls f with each subslice of b that matches
|
||||
// regexp `0x[0-9a-f]*`.
|
||||
func foreachHexAddress(b []byte, f func([]byte)) {
|
||||
for len(b) > 0 {
|
||||
i := bytes.Index(b, ohx)
|
||||
if i == -1 {
|
||||
return
|
||||
}
|
||||
b = b[i:]
|
||||
hx := hexPrefix(b)
|
||||
f(hx)
|
||||
b = b[len(hx):]
|
||||
}
|
||||
}
|
||||
|
||||
func hexPrefix(b []byte) []byte {
|
||||
for i, c := range b {
|
||||
if i < 2 {
|
||||
continue
|
||||
}
|
||||
if !isHexByte(c) {
|
||||
return b[:i]
|
||||
}
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func isHexByte(b byte) bool {
|
||||
return '0' <= b && b <= '9' || 'a' <= b && b <= 'f' || 'A' <= b && b <= 'F'
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
@@ -77,6 +76,8 @@ type Direct struct {
|
||||
popBrowser func(url string) // or nil
|
||||
c2nHandler http.Handler // or nil
|
||||
|
||||
dialPlan ControlDialPlanner // can be nil
|
||||
|
||||
mu sync.Mutex // mutex guards the following fields
|
||||
serverKey key.MachinePublic // original ("legacy") nacl crypto_box-based public key
|
||||
serverNoiseKey key.MachinePublic
|
||||
@@ -107,6 +108,7 @@ type Options struct {
|
||||
KeepAlive bool
|
||||
Logf logger.Logf
|
||||
HTTPTestClient *http.Client // optional HTTP client to use (for tests only)
|
||||
NoiseTestClient *http.Client // optional HTTP client to use for noise RPCs (tests only)
|
||||
DebugFlags []string // debug settings to send to control
|
||||
LinkMonitor *monitor.Mon // optional link monitor
|
||||
PopBrowserURL func(url string) // optional func to open browser
|
||||
@@ -133,6 +135,34 @@ type Options struct {
|
||||
// MapResponse.PingRequest queries from the control plane.
|
||||
// If nil, PingRequest queries are not answered.
|
||||
Pinger Pinger
|
||||
|
||||
// DialPlan contains and stores a previous dial plan that we received
|
||||
// from the control server; if nil, we fall back to using DNS.
|
||||
//
|
||||
// If we receive a new DialPlan from the server, this value will be
|
||||
// updated.
|
||||
DialPlan ControlDialPlanner
|
||||
}
|
||||
|
||||
// ControlDialPlanner is the interface optionally supplied when creating a
|
||||
// control client to control exactly how TCP connections to the control plane
|
||||
// are dialed.
|
||||
//
|
||||
// It is usually implemented by an atomic.Pointer.
|
||||
type ControlDialPlanner interface {
|
||||
// Load returns the current plan for how to connect to control.
|
||||
//
|
||||
// The returned plan can be nil. If so, connections should be made by
|
||||
// resolving the control URL using DNS.
|
||||
Load() *tailcfg.ControlDialPlan
|
||||
|
||||
// Store updates the dial plan with new directions from the control
|
||||
// server.
|
||||
//
|
||||
// The dial plan can span multiple connections to the control server.
|
||||
// That is, a dial plan received when connected over Wi-Fi is still
|
||||
// valid for a subsequent connection over LTE after a network switch.
|
||||
Store(*tailcfg.ControlDialPlan)
|
||||
}
|
||||
|
||||
// Pinger is the LocalBackend.Ping method.
|
||||
@@ -216,6 +246,7 @@ func NewDirect(opts Options) (*Direct, error) {
|
||||
popBrowser: opts.PopBrowserURL,
|
||||
c2nHandler: opts.C2NHandler,
|
||||
dialer: opts.Dialer,
|
||||
dialPlan: opts.DialPlan,
|
||||
}
|
||||
if opts.Hostinfo == nil {
|
||||
c.SetHostinfo(hostinfo.New())
|
||||
@@ -227,6 +258,12 @@ func NewDirect(opts Options) (*Direct, error) {
|
||||
c.SetNetInfo(ni)
|
||||
}
|
||||
}
|
||||
if opts.NoiseTestClient != nil {
|
||||
c.noiseClient = &noiseClient{
|
||||
Client: opts.NoiseTestClient,
|
||||
}
|
||||
c.serverNoiseKey = key.NewMachine().Public() // prevent early error before hitting test client
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
@@ -490,7 +527,7 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
|
||||
c.logf("RegisterReq sign error: %v", err)
|
||||
}
|
||||
}
|
||||
if debugRegister {
|
||||
if debugRegister() {
|
||||
j, _ := json.MarshalIndent(request, "", "\t")
|
||||
c.logf("RegisterRequest: %s", j)
|
||||
}
|
||||
@@ -523,7 +560,7 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
|
||||
return regen, opt.URL, fmt.Errorf("register request: %w", err)
|
||||
}
|
||||
if res.StatusCode != 200 {
|
||||
msg, _ := ioutil.ReadAll(res.Body)
|
||||
msg, _ := io.ReadAll(res.Body)
|
||||
res.Body.Close()
|
||||
return regen, opt.URL, fmt.Errorf("register request: http %d: %.200s",
|
||||
res.StatusCode, strings.TrimSpace(string(msg)))
|
||||
@@ -533,7 +570,7 @@ func (c *Direct) doLogin(ctx context.Context, opt loginOpt) (mustRegen bool, new
|
||||
c.logf("error decoding RegisterResponse with server key %s and machine key %s: %v", serverKey, machinePrivKey.Public(), err)
|
||||
return regen, opt.URL, fmt.Errorf("register request: %v", err)
|
||||
}
|
||||
if debugRegister {
|
||||
if debugRegister() {
|
||||
j, _ := json.MarshalIndent(resp, "", "\t")
|
||||
c.logf("RegisterResponse: %s", j)
|
||||
}
|
||||
@@ -715,7 +752,7 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, readOnly bool
|
||||
c.logf("[v1] PollNetMap: stream=%v ep=%v", allowStream, epStrs)
|
||||
|
||||
vlogf := logger.Discard
|
||||
if DevKnob.DumpNetMaps {
|
||||
if DevKnob.DumpNetMaps() {
|
||||
// TODO(bradfitz): update this to use "[v2]" prefix perhaps? but we don't
|
||||
// want to upload it always.
|
||||
vlogf = c.logf
|
||||
@@ -739,7 +776,7 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, readOnly bool
|
||||
// with useful results. The first POST just gets us the DERP map which we
|
||||
// need to do the STUN queries to discover our endpoints.
|
||||
// TODO(bradfitz): we skip this optimization in tests, though,
|
||||
// because the e2e tests are currently hyperspecific about the
|
||||
// because the e2e tests are currently hyper-specific about the
|
||||
// ordering of things. The e2e tests need love.
|
||||
ReadOnly: readOnly || (len(epStrs) == 0 && !everEndpoints && !inTest()),
|
||||
}
|
||||
@@ -804,7 +841,7 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, readOnly bool
|
||||
}
|
||||
vlogf("netmap: Do = %v after %v", res.StatusCode, time.Since(t0).Round(time.Millisecond))
|
||||
if res.StatusCode != 200 {
|
||||
msg, _ := ioutil.ReadAll(res.Body)
|
||||
msg, _ := io.ReadAll(res.Body)
|
||||
res.Body.Close()
|
||||
return fmt.Errorf("initial fetch failed %d: %.200s",
|
||||
res.StatusCode, strings.TrimSpace(string(msg)))
|
||||
@@ -814,7 +851,7 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, readOnly bool
|
||||
health.NoteMapRequestHeard(request)
|
||||
|
||||
if cb == nil {
|
||||
io.Copy(ioutil.Discard, res.Body)
|
||||
io.Copy(io.Discard, res.Body)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -909,6 +946,14 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, readOnly bool
|
||||
} else {
|
||||
vlogf("netmap: got new map")
|
||||
}
|
||||
if resp.ControlDialPlan != nil {
|
||||
if c.dialPlan != nil {
|
||||
c.logf("netmap: got new dial plan from control")
|
||||
c.dialPlan.Store(resp.ControlDialPlan)
|
||||
} else {
|
||||
c.logf("netmap: [unexpected] new dial plan; nowhere to store it")
|
||||
}
|
||||
}
|
||||
|
||||
select {
|
||||
case timeoutReset <- struct{}{}:
|
||||
@@ -963,12 +1008,12 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, readOnly bool
|
||||
controlTrimWGConfig.Store(d.TrimWGConfig)
|
||||
}
|
||||
|
||||
if DevKnob.StripEndpoints {
|
||||
if DevKnob.StripEndpoints() {
|
||||
for _, p := range resp.Peers {
|
||||
p.Endpoints = nil
|
||||
}
|
||||
}
|
||||
if DevKnob.StripCaps {
|
||||
if DevKnob.StripCaps() {
|
||||
nm.SelfNode.Capabilities = nil
|
||||
}
|
||||
|
||||
@@ -998,7 +1043,7 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, readOnly bool
|
||||
// it uses the serverKey and mkey to decode the message from the NaCl-crypto-box.
|
||||
func decode(res *http.Response, v any, serverKey, serverNoiseKey key.MachinePublic, mkey key.MachinePrivate) error {
|
||||
defer res.Body.Close()
|
||||
msg, err := ioutil.ReadAll(io.LimitReader(res.Body, 1<<20))
|
||||
msg, err := io.ReadAll(io.LimitReader(res.Body, 1<<20))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1012,8 +1057,8 @@ func decode(res *http.Response, v any, serverKey, serverNoiseKey key.MachinePubl
|
||||
}
|
||||
|
||||
var (
|
||||
debugMap = envknob.Bool("TS_DEBUG_MAP")
|
||||
debugRegister = envknob.Bool("TS_DEBUG_REGISTER")
|
||||
debugMap = envknob.RegisterBool("TS_DEBUG_MAP")
|
||||
debugRegister = envknob.RegisterBool("TS_DEBUG_REGISTER")
|
||||
)
|
||||
|
||||
var jsonEscapedZero = []byte(`\u0000`)
|
||||
@@ -1051,7 +1096,7 @@ func (c *Direct) decodeMsg(msg []byte, v any, mkey key.MachinePrivate) error {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if debugMap {
|
||||
if debugMap() {
|
||||
var buf bytes.Buffer
|
||||
json.Indent(&buf, b, "", " ")
|
||||
log.Printf("MapResponse: %s", buf.Bytes())
|
||||
@@ -1088,7 +1133,7 @@ func encode(v any, serverKey, serverNoiseKey key.MachinePublic, mkey key.Machine
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if debugMap {
|
||||
if debugMap() {
|
||||
if _, ok := v.(*tailcfg.MapRequest); ok {
|
||||
log.Printf("MapRequest: %s", b)
|
||||
}
|
||||
@@ -1110,7 +1155,7 @@ func loadServerPubKeys(ctx context.Context, httpc *http.Client, serverURL string
|
||||
return nil, fmt.Errorf("fetch control key: %v", err)
|
||||
}
|
||||
defer res.Body.Close()
|
||||
b, err := ioutil.ReadAll(io.LimitReader(res.Body, 64<<10))
|
||||
b, err := io.ReadAll(io.LimitReader(res.Body, 64<<10))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("fetch control key response: %v", err)
|
||||
}
|
||||
@@ -1139,18 +1184,18 @@ func loadServerPubKeys(ctx context.Context, httpc *http.Client, serverURL string
|
||||
var DevKnob = initDevKnob()
|
||||
|
||||
type devKnobs struct {
|
||||
DumpNetMaps bool
|
||||
ForceProxyDNS bool
|
||||
StripEndpoints bool // strip endpoints from control (only use disco messages)
|
||||
StripCaps bool // strip all local node's control-provided capabilities
|
||||
DumpNetMaps func() bool
|
||||
ForceProxyDNS func() bool
|
||||
StripEndpoints func() bool // strip endpoints from control (only use disco messages)
|
||||
StripCaps func() bool // strip all local node's control-provided capabilities
|
||||
}
|
||||
|
||||
func initDevKnob() devKnobs {
|
||||
return devKnobs{
|
||||
DumpNetMaps: envknob.Bool("TS_DEBUG_NETMAP"),
|
||||
ForceProxyDNS: envknob.Bool("TS_DEBUG_PROXY_DNS"),
|
||||
StripEndpoints: envknob.Bool("TS_DEBUG_STRIP_ENDPOINTS"),
|
||||
StripCaps: envknob.Bool("TS_DEBUG_STRIP_CAPS"),
|
||||
DumpNetMaps: envknob.RegisterBool("TS_DEBUG_NETMAP"),
|
||||
ForceProxyDNS: envknob.RegisterBool("TS_DEBUG_PROXY_DNS"),
|
||||
StripEndpoints: envknob.RegisterBool("TS_DEBUG_STRIP_ENDPOINTS"),
|
||||
StripCaps: envknob.RegisterBool("TS_DEBUG_STRIP_CAPS"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1359,12 +1404,17 @@ func (c *Direct) getNoiseClient() (*noiseClient, error) {
|
||||
if nc != nil {
|
||||
return nc, nil
|
||||
}
|
||||
var dp func() *tailcfg.ControlDialPlan
|
||||
if c.dialPlan != nil {
|
||||
dp = c.dialPlan.Load
|
||||
}
|
||||
nc, err, _ := c.sfGroup.Do(struct{}{}, func() (*noiseClient, error) {
|
||||
k, err := c.getMachinePrivKey()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
nc, err := newNoiseClient(k, serverNoiseKey, c.serverURL, c.dialer)
|
||||
c.logf("creating new noise client")
|
||||
nc, err := newNoiseClient(k, serverNoiseKey, c.serverURL, c.dialer, dp)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -1384,21 +1434,17 @@ func (c *Direct) getNoiseClient() (*noiseClient, error) {
|
||||
func (c *Direct) setDNSNoise(ctx context.Context, req *tailcfg.SetDNSRequest) error {
|
||||
newReq := *req
|
||||
newReq.Version = tailcfg.CurrentCapabilityVersion
|
||||
np, err := c.getNoiseClient()
|
||||
nc, err := c.getNoiseClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
bodyData, err := json.Marshal(newReq)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
res, err := np.Post(fmt.Sprintf("https://%v/%v", np.host, "machine/set-dns"), "application/json", bytes.NewReader(bodyData))
|
||||
res, err := nc.post(ctx, "/machine/set-dns", &newReq)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != 200 {
|
||||
msg, _ := ioutil.ReadAll(res.Body)
|
||||
msg, _ := io.ReadAll(res.Body)
|
||||
return fmt.Errorf("set-dns response: %v, %.200s", res.Status, strings.TrimSpace(string(msg)))
|
||||
}
|
||||
var setDNSRes tailcfg.SetDNSResponse
|
||||
@@ -1464,7 +1510,7 @@ func (c *Direct) SetDNS(ctx context.Context, req *tailcfg.SetDNSRequest) (err er
|
||||
}
|
||||
defer res.Body.Close()
|
||||
if res.StatusCode != 200 {
|
||||
msg, _ := ioutil.ReadAll(res.Body)
|
||||
msg, _ := io.ReadAll(res.Body)
|
||||
return fmt.Errorf("set-dns response: %v, %.200s", res.Status, strings.TrimSpace(string(msg)))
|
||||
}
|
||||
var setDNSRes tailcfg.SetDNSResponse
|
||||
@@ -1540,6 +1586,38 @@ func postPingResult(start time.Time, logf logger.Logf, c *http.Client, pr *tailc
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReportHealthChange reports to the control plane a change to this node's
|
||||
// health.
|
||||
func (c *Direct) ReportHealthChange(sys health.Subsystem, sysErr error) {
|
||||
if sys == health.SysOverall {
|
||||
// We don't report these. These include things like the network is down
|
||||
// (in which case we can't report anyway) or the user wanted things
|
||||
// stopped, as opposed to the more unexpected failure types in the other
|
||||
// subsystems.
|
||||
return
|
||||
}
|
||||
np, err := c.getNoiseClient()
|
||||
if err != nil {
|
||||
// Don't report errors to control if the server doesn't support noise.
|
||||
return
|
||||
}
|
||||
req := &tailcfg.HealthChangeRequest{
|
||||
Subsys: string(sys),
|
||||
}
|
||||
if sysErr != nil {
|
||||
req.Error = sysErr.Error()
|
||||
}
|
||||
|
||||
// Best effort, no logging:
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
res, err := np.post(ctx, "/machine/update-health", req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
res.Body.Close()
|
||||
}
|
||||
|
||||
var (
|
||||
metricMapRequestsActive = clientmetric.NewGauge("controlclient_map_requests_active")
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ type mapSession struct {
|
||||
machinePubKey key.MachinePublic
|
||||
keepSharerAndUserSplit bool // see Options.KeepSharerAndUserSplit
|
||||
|
||||
// Fields storing state over the the coards of multiple MapResponses.
|
||||
// Fields storing state over the course of multiple MapResponses.
|
||||
lastNode *tailcfg.Node
|
||||
lastDNSConfig *tailcfg.DNSConfig
|
||||
lastDERPMap *tailcfg.DERPMap
|
||||
@@ -45,9 +45,11 @@ type mapSession struct {
|
||||
collectServices bool
|
||||
previousPeers []*tailcfg.Node // for delta-purposes
|
||||
lastDomain string
|
||||
lastDomainAuditLogID string
|
||||
lastHealth []string
|
||||
lastPopBrowserURL string
|
||||
stickyDebug tailcfg.Debug // accumulated opt.Bool values
|
||||
lastTKAInfo *tailcfg.TKAInfo
|
||||
|
||||
// netMapBuilding is non-nil during a netmapForResponse call,
|
||||
// containing the value to be returned, once fully populated.
|
||||
@@ -112,9 +114,15 @@ func (ms *mapSession) netmapForResponse(resp *tailcfg.MapResponse) *netmap.Netwo
|
||||
if resp.Domain != "" {
|
||||
ms.lastDomain = resp.Domain
|
||||
}
|
||||
if resp.DomainDataPlaneAuditLogID != "" {
|
||||
ms.lastDomainAuditLogID = resp.DomainDataPlaneAuditLogID
|
||||
}
|
||||
if resp.Health != nil {
|
||||
ms.lastHealth = resp.Health
|
||||
}
|
||||
if resp.TKAInfo != nil {
|
||||
ms.lastTKAInfo = resp.TKAInfo
|
||||
}
|
||||
|
||||
debug := resp.Debug
|
||||
if debug != nil {
|
||||
@@ -139,22 +147,31 @@ 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,
|
||||
DNS: *ms.lastDNSConfig,
|
||||
PacketFilter: ms.lastParsedPacketFilter,
|
||||
SSHPolicy: ms.lastSSHPolicy,
|
||||
CollectServices: ms.collectServices,
|
||||
DERPMap: ms.lastDERPMap,
|
||||
Debug: debug,
|
||||
ControlHealth: ms.lastHealth,
|
||||
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,
|
||||
}
|
||||
ms.netMapBuilding = nm
|
||||
|
||||
if ms.lastTKAInfo != nil && ms.lastTKAInfo.Head != "" {
|
||||
if err := nm.TKAHead.UnmarshalText([]byte(ms.lastTKAInfo.Head)); err != nil {
|
||||
ms.logf("error unmarshalling TKAHead: %v", err)
|
||||
nm.TKAEnabled = false
|
||||
}
|
||||
}
|
||||
|
||||
if resp.Node != nil {
|
||||
ms.lastNode = resp.Node
|
||||
}
|
||||
@@ -190,7 +207,7 @@ func (ms *mapSession) netmapForResponse(resp *tailcfg.MapResponse) *netmap.Netwo
|
||||
}
|
||||
ms.addUserProfile(peer.User)
|
||||
}
|
||||
if DevKnob.ForceProxyDNS {
|
||||
if DevKnob.ForceProxyDNS() {
|
||||
nm.DNS.Proxied = true
|
||||
}
|
||||
ms.netMapBuilding = nil
|
||||
@@ -356,13 +373,13 @@ func cloneNodes(v1 []*tailcfg.Node) []*tailcfg.Node {
|
||||
return v2
|
||||
}
|
||||
|
||||
var debugSelfIPv6Only = envknob.Bool("TS_DEBUG_SELF_V6_ONLY")
|
||||
var debugSelfIPv6Only = envknob.RegisterBool("TS_DEBUG_SELF_V6_ONLY")
|
||||
|
||||
func filterSelfAddresses(in []netip.Prefix) (ret []netip.Prefix) {
|
||||
switch {
|
||||
default:
|
||||
return in
|
||||
case debugSelfIPv6Only:
|
||||
case debugSelfIPv6Only():
|
||||
for _, a := range in {
|
||||
if a.Addr().Is6() {
|
||||
ret = append(ret, a)
|
||||
|
||||
@@ -466,7 +466,7 @@ func TestNetmapForResponse(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
// TestDeltaDebug tests that tailcfg.Debug values can be omitted in MapResposnes
|
||||
// TestDeltaDebug tests that tailcfg.Debug values can be omitted in MapResponses
|
||||
// entirely or have their opt.Bool values unspecified between MapResponses in a
|
||||
// session and that should mean no change. (as of capver 37). But two Debug
|
||||
// fields existed prior to capver 37 that weren't opt.Bool; we test that we both
|
||||
|
||||
@@ -5,8 +5,10 @@
|
||||
package controlclient
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"math"
|
||||
"net"
|
||||
"net/http"
|
||||
@@ -53,6 +55,11 @@ type noiseClient struct {
|
||||
httpPort string // the default port to call
|
||||
httpsPort string // the fallback Noise-over-https port
|
||||
|
||||
// dialPlan optionally returns a ControlDialPlan previously received
|
||||
// from the control server; either the function or the return value can
|
||||
// be nil.
|
||||
dialPlan func() *tailcfg.ControlDialPlan
|
||||
|
||||
// mu only protects the following variables.
|
||||
mu sync.Mutex
|
||||
nextID int
|
||||
@@ -61,7 +68,9 @@ type noiseClient struct {
|
||||
|
||||
// newNoiseClient returns a new noiseClient for the provided server and machine key.
|
||||
// serverURL is of the form https://<host>:<port> (no trailing slash).
|
||||
func newNoiseClient(priKey key.MachinePrivate, serverPubKey key.MachinePublic, serverURL string, dialer *tsdial.Dialer) (*noiseClient, error) {
|
||||
//
|
||||
// dialPlan may be nil
|
||||
func newNoiseClient(priKey key.MachinePrivate, serverPubKey key.MachinePublic, serverURL string, dialer *tsdial.Dialer, dialPlan func() *tailcfg.ControlDialPlan) (*noiseClient, error) {
|
||||
u, err := url.Parse(serverURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -89,6 +98,7 @@ func newNoiseClient(priKey key.MachinePrivate, serverPubKey key.MachinePublic, s
|
||||
httpPort: httpPort,
|
||||
httpsPort: httpsPort,
|
||||
dialer: dialer,
|
||||
dialPlan: dialPlan,
|
||||
}
|
||||
|
||||
// Create the HTTP/2 Transport using a net/http.Transport
|
||||
@@ -155,17 +165,61 @@ func (nc *noiseClient) dial(_, _ string, _ *tls.Config) (net.Conn, error) {
|
||||
nc.nextID++
|
||||
nc.mu.Unlock()
|
||||
|
||||
// Timeout is a little arbitrary, but plenty long enough for even the
|
||||
// highest latency links.
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if tailcfg.CurrentCapabilityVersion > math.MaxUint16 {
|
||||
// Panic, because a test should have started failing several
|
||||
// thousand version numbers before getting to this point.
|
||||
panic("capability version is too high to fit in the wire protocol")
|
||||
}
|
||||
conn, err := controlhttp.Dial(ctx, nc.host, nc.httpPort, nc.httpsPort, nc.privKey, nc.serverPubKey, uint16(tailcfg.CurrentCapabilityVersion), nc.dialer.SystemDial)
|
||||
|
||||
var dialPlan *tailcfg.ControlDialPlan
|
||||
if nc.dialPlan != nil {
|
||||
dialPlan = nc.dialPlan()
|
||||
}
|
||||
|
||||
// If we have a dial plan, then set our timeout as slightly longer than
|
||||
// the maximum amount of time contained therein; we assume that
|
||||
// explicit instructions on timeouts are more useful than a single
|
||||
// hard-coded timeout.
|
||||
//
|
||||
// The default value of 5 is chosen so that, when there's no dial plan,
|
||||
// we retain the previous behaviour of 10 seconds end-to-end timeout.
|
||||
timeoutSec := 5.0
|
||||
if dialPlan != nil {
|
||||
for _, c := range dialPlan.Candidates {
|
||||
if v := c.DialStartDelaySec + c.DialTimeoutSec; v > timeoutSec {
|
||||
timeoutSec = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// After we establish a connection, we need some time to actually
|
||||
// upgrade it into a Noise connection. With a ballpark worst-case RTT
|
||||
// of 1000ms, give ourselves an extra 5 seconds to complete the
|
||||
// handshake.
|
||||
timeoutSec += 5
|
||||
|
||||
// Be extremely defensive and ensure that the timeout is in the range
|
||||
// [5, 60] seconds (e.g. if we accidentally get a negative number).
|
||||
if timeoutSec > 60 {
|
||||
timeoutSec = 60
|
||||
} else if timeoutSec < 5 {
|
||||
timeoutSec = 5
|
||||
}
|
||||
|
||||
timeout := time.Duration(timeoutSec * float64(time.Second))
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
|
||||
conn, err := (&controlhttp.Dialer{
|
||||
Hostname: nc.host,
|
||||
HTTPPort: nc.httpPort,
|
||||
HTTPSPort: nc.httpsPort,
|
||||
MachineKey: nc.privKey,
|
||||
ControlKey: nc.serverPubKey,
|
||||
ProtocolVersion: uint16(tailcfg.CurrentCapabilityVersion),
|
||||
Dialer: nc.dialer.SystemDial,
|
||||
DialPlan: dialPlan,
|
||||
}).Dial(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -176,3 +230,16 @@ func (nc *noiseClient) dial(_, _ string, _ *tls.Config) (net.Conn, error) {
|
||||
mak.Set(&nc.connPool, ncc.id, ncc)
|
||||
return ncc, nil
|
||||
}
|
||||
|
||||
func (nc *noiseClient) post(ctx context.Context, path string, body any) (*http.Response, error) {
|
||||
jbody, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", "https://"+nc.host+path, bytes.NewReader(jbody))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
return nc.Do(req)
|
||||
}
|
||||
|
||||
@@ -28,69 +28,231 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptrace"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"sort"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"tailscale.com/control/controlbase"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/net/dnscache"
|
||||
"tailscale.com/net/dnsfallback"
|
||||
"tailscale.com/net/netutil"
|
||||
"tailscale.com/net/tlsdial"
|
||||
"tailscale.com/net/tshttpproxy"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/util/multierr"
|
||||
)
|
||||
|
||||
// Dial connects to the HTTP server at host:httpPort, requests to switch to the
|
||||
// Tailscale control protocol, and returns an established control
|
||||
var stdDialer net.Dialer
|
||||
|
||||
// Dial connects to the HTTP server at this Dialer's Host:HTTPPort, requests to
|
||||
// switch to the Tailscale control protocol, and returns an established control
|
||||
// protocol connection.
|
||||
//
|
||||
// If Dial fails to connect using addr, it also tries to tunnel over
|
||||
// TLS to host:httpsPort as a compatibility fallback.
|
||||
// If Dial fails to connect using HTTP, it also tries to tunnel over TLS to the
|
||||
// Dialer's Host:HTTPSPort as a compatibility fallback.
|
||||
//
|
||||
// The provided ctx is only used for the initial connection, until
|
||||
// Dial returns. It does not affect the connection once established.
|
||||
func Dial(ctx context.Context, host string, httpPort string, httpsPort string, machineKey key.MachinePrivate, controlKey key.MachinePublic, protocolVersion uint16, dialer dnscache.DialContextFunc) (*controlbase.Conn, error) {
|
||||
a := &dialParams{
|
||||
host: host,
|
||||
httpPort: httpPort,
|
||||
httpsPort: httpsPort,
|
||||
machineKey: machineKey,
|
||||
controlKey: controlKey,
|
||||
version: protocolVersion,
|
||||
proxyFunc: tshttpproxy.ProxyFromEnvironment,
|
||||
dialer: dialer,
|
||||
func (a *Dialer) Dial(ctx context.Context) (*controlbase.Conn, error) {
|
||||
if a.Hostname == "" {
|
||||
return nil, errors.New("required Dialer.Hostname empty")
|
||||
}
|
||||
return a.dial(ctx)
|
||||
}
|
||||
|
||||
type dialParams struct {
|
||||
host string
|
||||
httpPort string
|
||||
httpsPort string
|
||||
machineKey key.MachinePrivate
|
||||
controlKey key.MachinePublic
|
||||
version uint16
|
||||
proxyFunc func(*http.Request) (*url.URL, error) // or nil
|
||||
dialer dnscache.DialContextFunc
|
||||
|
||||
// For tests only
|
||||
insecureTLS bool
|
||||
testFallbackDelay time.Duration
|
||||
func (a *Dialer) logf(format string, args ...any) {
|
||||
if a.Logf != nil {
|
||||
a.Logf(format, args...)
|
||||
}
|
||||
}
|
||||
|
||||
// httpsFallbackDelay is how long we'll wait for a.httpPort to work before
|
||||
// starting to try a.httpsPort.
|
||||
func (a *dialParams) httpsFallbackDelay() time.Duration {
|
||||
func (a *Dialer) getProxyFunc() func(*http.Request) (*url.URL, error) {
|
||||
if a.proxyFunc != nil {
|
||||
return a.proxyFunc
|
||||
}
|
||||
return tshttpproxy.ProxyFromEnvironment
|
||||
}
|
||||
|
||||
// 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 v := a.testFallbackDelay; v != 0 {
|
||||
return v
|
||||
}
|
||||
return 500 * time.Millisecond
|
||||
}
|
||||
|
||||
func (a *dialParams) dial(ctx context.Context) (*controlbase.Conn, error) {
|
||||
var _ = envknob.RegisterBool("TS_USE_CONTROL_DIAL_PLAN") // to record at init time whether it's in use
|
||||
|
||||
func (a *Dialer) dial(ctx context.Context) (*controlbase.Conn, error) {
|
||||
// If we don't have a dial plan, just fall back to dialing the single
|
||||
// host we know about.
|
||||
useDialPlan := envknob.BoolDefaultTrue("TS_USE_CONTROL_DIAL_PLAN")
|
||||
if !useDialPlan || a.DialPlan == nil || len(a.DialPlan.Candidates) == 0 {
|
||||
return a.dialHost(ctx, netip.Addr{})
|
||||
}
|
||||
candidates := a.DialPlan.Candidates
|
||||
|
||||
// Otherwise, we try dialing per the plan. Store the highest priority
|
||||
// in the list, so that if we get a connection to one of those
|
||||
// candidates we can return quickly.
|
||||
var highestPriority int = math.MinInt
|
||||
for _, c := range candidates {
|
||||
if c.Priority > highestPriority {
|
||||
highestPriority = c.Priority
|
||||
}
|
||||
}
|
||||
|
||||
// This context allows us to cancel in-flight connections if we get a
|
||||
// highest-priority connection before we're all done.
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
|
||||
// Now, for each candidate, kick off a dial in parallel.
|
||||
type dialResult struct {
|
||||
conn *controlbase.Conn
|
||||
err error
|
||||
addr netip.Addr
|
||||
priority int
|
||||
}
|
||||
resultsCh := make(chan dialResult, len(candidates))
|
||||
|
||||
var pending atomic.Int32
|
||||
pending.Store(int32(len(candidates)))
|
||||
for _, c := range candidates {
|
||||
go func(ctx context.Context, c tailcfg.ControlIPCandidate) {
|
||||
var (
|
||||
conn *controlbase.Conn
|
||||
err error
|
||||
)
|
||||
|
||||
// Always send results back to our channel.
|
||||
defer func() {
|
||||
resultsCh <- dialResult{conn, err, c.IP, c.Priority}
|
||||
if pending.Add(-1) == 0 {
|
||||
close(resultsCh)
|
||||
}
|
||||
}()
|
||||
|
||||
// If non-zero, wait the configured start timeout
|
||||
// before we do anything.
|
||||
if c.DialStartDelaySec > 0 {
|
||||
a.logf("[v2] controlhttp: waiting %.2f seconds before dialing %q @ %v", c.DialStartDelaySec, a.Hostname, c.IP)
|
||||
tmr := time.NewTimer(time.Duration(c.DialStartDelaySec * float64(time.Second)))
|
||||
defer tmr.Stop()
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
err = ctx.Err()
|
||||
return
|
||||
case <-tmr.C:
|
||||
}
|
||||
}
|
||||
|
||||
// Now, create a sub-context with the given timeout and
|
||||
// try dialing the provided host.
|
||||
ctx, cancel := context.WithTimeout(ctx, time.Duration(c.DialTimeoutSec*float64(time.Second)))
|
||||
defer cancel()
|
||||
|
||||
// This will dial, and the defer above sends it back to our parent.
|
||||
a.logf("[v2] controlhttp: trying to dial %q @ %v", a.Hostname, c.IP)
|
||||
conn, err = a.dialHost(ctx, c.IP)
|
||||
}(ctx, c)
|
||||
}
|
||||
|
||||
var results []dialResult
|
||||
for res := range resultsCh {
|
||||
// If we get a response that has the highest priority, we don't
|
||||
// need to wait for any of the other connections to finish; we
|
||||
// can just return this connection.
|
||||
//
|
||||
// TODO(andrew): we could make this better by keeping track of
|
||||
// the highest remaining priority dynamically, instead of just
|
||||
// checking for the highest total
|
||||
if res.priority == highestPriority && res.conn != nil {
|
||||
a.logf("[v1] controlhttp: high-priority success dialing %q @ %v from dial plan", a.Hostname, res.addr)
|
||||
|
||||
// Drain the channel and any existing connections in
|
||||
// the background.
|
||||
go func() {
|
||||
for _, res := range results {
|
||||
if res.conn != nil {
|
||||
res.conn.Close()
|
||||
}
|
||||
}
|
||||
for res := range resultsCh {
|
||||
if res.conn != nil {
|
||||
res.conn.Close()
|
||||
}
|
||||
}
|
||||
if a.drainFinished != nil {
|
||||
close(a.drainFinished)
|
||||
}
|
||||
}()
|
||||
return res.conn, nil
|
||||
}
|
||||
|
||||
// This isn't a highest-priority result, so just store it until
|
||||
// we're done.
|
||||
results = append(results, res)
|
||||
}
|
||||
|
||||
// After we finish this function, close any remaining open connections.
|
||||
defer func() {
|
||||
for _, result := range results {
|
||||
// Note: below, we nil out the returned connection (if
|
||||
// any) in the slice so we don't close it.
|
||||
if result.conn != nil {
|
||||
result.conn.Close()
|
||||
}
|
||||
}
|
||||
|
||||
// We don't drain asynchronously after this point, so notify our
|
||||
// channel when we return.
|
||||
if a.drainFinished != nil {
|
||||
close(a.drainFinished)
|
||||
}
|
||||
}()
|
||||
|
||||
// Sort by priority, then take the first non-error response.
|
||||
sort.Slice(results, func(i, j int) bool {
|
||||
// NOTE: intentionally inverted so that the highest priority
|
||||
// item comes first
|
||||
return results[i].priority > results[j].priority
|
||||
})
|
||||
|
||||
var (
|
||||
conn *controlbase.Conn
|
||||
errs []error
|
||||
)
|
||||
for i, result := range results {
|
||||
if result.err != nil {
|
||||
errs = append(errs, result.err)
|
||||
continue
|
||||
}
|
||||
|
||||
a.logf("[v1] controlhttp: succeeded dialing %q @ %v from dial plan", a.Hostname, result.addr)
|
||||
conn = result.conn
|
||||
results[i].conn = nil // so we don't close it in the defer
|
||||
return conn, nil
|
||||
}
|
||||
merr := multierr.New(errs...)
|
||||
|
||||
// If we get here, then we didn't get anywhere with our dial plan; fall back to just using DNS.
|
||||
a.logf("controlhttp: failed dialing using DialPlan, falling back to DNS; errs=%s", merr.Error())
|
||||
return a.dialHost(ctx, netip.Addr{})
|
||||
}
|
||||
|
||||
// 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.
|
||||
func (a *Dialer) dialHost(ctx context.Context, addr netip.Addr) (*controlbase.Conn, error) {
|
||||
// Create one shared context used by both port 80 and port 443 dials.
|
||||
// If port 80 is still in flight when 443 returns, this deferred cancel
|
||||
// will stop the port 80 dial.
|
||||
@@ -102,12 +264,12 @@ func (a *dialParams) dial(ctx context.Context) (*controlbase.Conn, error) {
|
||||
// we'll speak Noise.
|
||||
u80 := &url.URL{
|
||||
Scheme: "http",
|
||||
Host: net.JoinHostPort(a.host, a.httpPort),
|
||||
Host: net.JoinHostPort(a.Hostname, strDef(a.HTTPPort, "80")),
|
||||
Path: serverUpgradePath,
|
||||
}
|
||||
u443 := &url.URL{
|
||||
Scheme: "https",
|
||||
Host: net.JoinHostPort(a.host, a.httpsPort),
|
||||
Host: net.JoinHostPort(a.Hostname, strDef(a.HTTPSPort, "443")),
|
||||
Path: serverUpgradePath,
|
||||
}
|
||||
|
||||
@@ -118,7 +280,7 @@ func (a *dialParams) dial(ctx context.Context) (*controlbase.Conn, error) {
|
||||
}
|
||||
ch := make(chan tryURLRes) // must be unbuffered
|
||||
try := func(u *url.URL) {
|
||||
cbConn, err := a.dialURL(ctx, u)
|
||||
cbConn, err := a.dialURL(ctx, u, addr)
|
||||
select {
|
||||
case ch <- tryURLRes{u, cbConn, err}:
|
||||
case <-ctx.Done():
|
||||
@@ -169,12 +331,12 @@ func (a *dialParams) dial(ctx context.Context) (*controlbase.Conn, error) {
|
||||
}
|
||||
|
||||
// dialURL attempts to connect to the given URL.
|
||||
func (a *dialParams) dialURL(ctx context.Context, u *url.URL) (*controlbase.Conn, error) {
|
||||
init, cont, err := controlbase.ClientDeferred(a.machineKey, a.controlKey, a.version)
|
||||
func (a *Dialer) dialURL(ctx context.Context, u *url.URL, addr netip.Addr) (*controlbase.Conn, error) {
|
||||
init, cont, err := controlbase.ClientDeferred(a.MachineKey, a.ControlKey, a.ProtocolVersion)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
netConn, err := a.tryURLUpgrade(ctx, u, init)
|
||||
netConn, err := a.tryURLUpgrade(ctx, u, addr, init)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -186,29 +348,50 @@ func (a *dialParams) dialURL(ctx context.Context, u *url.URL) (*controlbase.Conn
|
||||
return cbConn, nil
|
||||
}
|
||||
|
||||
// tryURLUpgrade connects to u, and tries to upgrade it to a net.Conn.
|
||||
// tryURLUpgrade connects to u, and tries to upgrade it to a net.Conn. If addr
|
||||
// is valid, then no DNS is used and the connection will be made to the
|
||||
// provided address.
|
||||
//
|
||||
// Only the provided ctx is used, not a.ctx.
|
||||
func (a *dialParams) tryURLUpgrade(ctx context.Context, u *url.URL, init []byte) (net.Conn, error) {
|
||||
dns := &dnscache.Resolver{
|
||||
Forward: dnscache.Get().Forward,
|
||||
LookupIPFallback: dnsfallback.Lookup,
|
||||
UseLastGood: true,
|
||||
func (a *Dialer) tryURLUpgrade(ctx context.Context, u *url.URL, addr netip.Addr, init []byte) (net.Conn, error) {
|
||||
var dns *dnscache.Resolver
|
||||
|
||||
// If we were provided an address to dial, then create a resolver that just
|
||||
// returns that value; otherwise, fall back to DNS.
|
||||
if addr.IsValid() {
|
||||
dns = &dnscache.Resolver{
|
||||
SingleHostStaticResult: []netip.Addr{addr},
|
||||
SingleHost: u.Hostname(),
|
||||
}
|
||||
} else {
|
||||
dns = &dnscache.Resolver{
|
||||
Forward: dnscache.Get().Forward,
|
||||
LookupIPFallback: dnsfallback.Lookup,
|
||||
UseLastGood: true,
|
||||
}
|
||||
}
|
||||
|
||||
var dialer dnscache.DialContextFunc
|
||||
if a.Dialer != nil {
|
||||
dialer = a.Dialer
|
||||
} else {
|
||||
dialer = stdDialer.DialContext
|
||||
}
|
||||
|
||||
tr := http.DefaultTransport.(*http.Transport).Clone()
|
||||
defer tr.CloseIdleConnections()
|
||||
tr.Proxy = a.proxyFunc
|
||||
tr.Proxy = a.getProxyFunc()
|
||||
tshttpproxy.SetTransportGetProxyConnectHeader(tr)
|
||||
tr.DialContext = dnscache.Dialer(a.dialer, dns)
|
||||
tr.DialContext = dnscache.Dialer(dialer, dns)
|
||||
// Disable HTTP2, since h2 can't do protocol switching.
|
||||
tr.TLSClientConfig.NextProtos = []string{}
|
||||
tr.TLSNextProto = map[string]func(string, *tls.Conn) http.RoundTripper{}
|
||||
tr.TLSClientConfig = tlsdial.Config(a.host, tr.TLSClientConfig)
|
||||
tr.TLSClientConfig = tlsdial.Config(a.Hostname, tr.TLSClientConfig)
|
||||
if a.insecureTLS {
|
||||
tr.TLSClientConfig.InsecureSkipVerify = true
|
||||
tr.TLSClientConfig.VerifyConnection = nil
|
||||
}
|
||||
tr.DialTLSContext = dnscache.TLSDialer(a.dialer, dns, tr.TLSClientConfig)
|
||||
tr.DialTLSContext = dnscache.TLSDialer(dialer, dns, tr.TLSClientConfig)
|
||||
tr.DisableCompression = true
|
||||
|
||||
// (mis)use httptrace to extract the underlying net.Conn from the
|
||||
|
||||
@@ -7,27 +7,33 @@ package controlhttp
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"net"
|
||||
"net/url"
|
||||
|
||||
"nhooyr.io/websocket"
|
||||
"tailscale.com/control/controlbase"
|
||||
"tailscale.com/net/dnscache"
|
||||
"tailscale.com/types/key"
|
||||
)
|
||||
|
||||
// Variant of Dial that tunnels the request over WebSockets, since we cannot do
|
||||
// bi-directional communication over an HTTP connection when in JS.
|
||||
func Dial(ctx context.Context, host string, httpPort string, httpsPort string, machineKey key.MachinePrivate, controlKey key.MachinePublic, protocolVersion uint16, dialer dnscache.DialContextFunc) (*controlbase.Conn, error) {
|
||||
init, cont, err := controlbase.ClientDeferred(machineKey, controlKey, protocolVersion)
|
||||
func (d *Dialer) Dial(ctx context.Context) (*controlbase.Conn, error) {
|
||||
if d.Hostname == "" {
|
||||
return nil, errors.New("required Dialer.Hostname empty")
|
||||
}
|
||||
|
||||
init, cont, err := controlbase.ClientDeferred(d.MachineKey, d.ControlKey, d.ProtocolVersion)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
wsScheme := "wss"
|
||||
if host == "localhost" {
|
||||
host := d.Hostname
|
||||
// If using a custom control server (on a non-standard port), prefer that.
|
||||
// This mirrors the port selection in newNoiseClient from noise.go.
|
||||
if d.HTTPPort != "" && d.HTTPPort != "80" && d.HTTPSPort == "443" {
|
||||
wsScheme = "ws"
|
||||
host = net.JoinHostPort(host, httpPort)
|
||||
host = net.JoinHostPort(host, d.HTTPPort)
|
||||
}
|
||||
wsURL := &url.URL{
|
||||
Scheme: wsScheme,
|
||||
@@ -52,5 +58,4 @@ func Dial(ctx context.Context, host string, httpPort string, httpsPort string, m
|
||||
return nil, err
|
||||
}
|
||||
return cbConn, nil
|
||||
|
||||
}
|
||||
|
||||
@@ -4,6 +4,17 @@
|
||||
|
||||
package controlhttp
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"tailscale.com/net/dnscache"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
const (
|
||||
// upgradeHeader is the value of the Upgrade HTTP header used to
|
||||
// indicate the Tailscale control protocol.
|
||||
@@ -18,3 +29,64 @@ const (
|
||||
// to do the protocol switch is located.
|
||||
serverUpgradePath = "/ts2021"
|
||||
)
|
||||
|
||||
// Dialer contains configuration on how to dial the Tailscale control server.
|
||||
type Dialer struct {
|
||||
// Hostname is the hostname to connect to, with no port number.
|
||||
//
|
||||
// This field is required.
|
||||
Hostname string
|
||||
|
||||
// MachineKey contains the current machine's private key.
|
||||
//
|
||||
// This field is required.
|
||||
MachineKey key.MachinePrivate
|
||||
|
||||
// ControlKey contains the expected public key for the control server.
|
||||
//
|
||||
// This field is required.
|
||||
ControlKey key.MachinePublic
|
||||
|
||||
// ProtocolVersion is the expected protocol version to negotiate.
|
||||
//
|
||||
// This field is required.
|
||||
ProtocolVersion uint16
|
||||
|
||||
// HTTPPort is the port number to use when making a HTTP connection.
|
||||
//
|
||||
// If not specified, this defaults to port 80.
|
||||
HTTPPort string
|
||||
|
||||
// HTTPSPort is the port number to use when making a HTTPS connection.
|
||||
//
|
||||
// If not specified, this defaults to port 443.
|
||||
HTTPSPort string
|
||||
|
||||
// Dialer is the dialer used to make outbound connections.
|
||||
//
|
||||
// If not specified, this defaults to net.Dialer.DialContext.
|
||||
Dialer dnscache.DialContextFunc
|
||||
|
||||
// Logf, if set, is a logging function to use; if unset, logs are
|
||||
// dropped.
|
||||
Logf logger.Logf
|
||||
|
||||
// DialPlan, if set, contains instructions from the control server on
|
||||
// how to connect to it. If present, we will try the methods in this
|
||||
// plan before falling back to DNS.
|
||||
DialPlan *tailcfg.ControlDialPlan
|
||||
|
||||
proxyFunc func(*http.Request) (*url.URL, error) // or nil
|
||||
|
||||
// For tests only
|
||||
drainFinished chan struct{}
|
||||
insecureTLS bool
|
||||
testFallbackDelay time.Duration
|
||||
}
|
||||
|
||||
func strDef(v1, v2 string) string {
|
||||
if v1 != "" {
|
||||
return v1
|
||||
}
|
||||
return v2
|
||||
}
|
||||
|
||||
@@ -13,16 +13,21 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"tailscale.com/control/controlbase"
|
||||
"tailscale.com/net/dnscache"
|
||||
"tailscale.com/net/socks5"
|
||||
"tailscale.com/net/tsdial"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
type httpTestParam struct {
|
||||
@@ -170,15 +175,16 @@ func testControlHTTP(t *testing.T, param httpTestParam) {
|
||||
defer cancel()
|
||||
}
|
||||
|
||||
a := dialParams{
|
||||
host: "localhost",
|
||||
httpPort: strconv.Itoa(httpLn.Addr().(*net.TCPAddr).Port),
|
||||
httpsPort: strconv.Itoa(httpsLn.Addr().(*net.TCPAddr).Port),
|
||||
machineKey: client,
|
||||
controlKey: server.Public(),
|
||||
version: testProtocolVersion,
|
||||
a := &Dialer{
|
||||
Hostname: "localhost",
|
||||
HTTPPort: strconv.Itoa(httpLn.Addr().(*net.TCPAddr).Port),
|
||||
HTTPSPort: strconv.Itoa(httpsLn.Addr().(*net.TCPAddr).Port),
|
||||
MachineKey: client,
|
||||
ControlKey: server.Public(),
|
||||
ProtocolVersion: testProtocolVersion,
|
||||
Dialer: new(tsdial.Dialer).SystemDial,
|
||||
Logf: t.Logf,
|
||||
insecureTLS: true,
|
||||
dialer: new(tsdial.Dialer).SystemDial,
|
||||
testFallbackDelay: 50 * time.Millisecond,
|
||||
}
|
||||
|
||||
@@ -443,3 +449,263 @@ func brokenMITMHandler(w http.ResponseWriter, r *http.Request) {
|
||||
w.(http.Flusher).Flush()
|
||||
<-r.Context().Done()
|
||||
}
|
||||
|
||||
func TestDialPlan(t *testing.T) {
|
||||
if runtime.GOOS != "linux" {
|
||||
t.Skip("only works on Linux due to multiple localhost addresses")
|
||||
}
|
||||
|
||||
client, server := key.NewMachine(), key.NewMachine()
|
||||
|
||||
const (
|
||||
testProtocolVersion = 1
|
||||
|
||||
// We need consistent ports for each address; these are chosen
|
||||
// randomly and we hope that they won't conflict during this test.
|
||||
httpPort = "40080"
|
||||
httpsPort = "40443"
|
||||
)
|
||||
|
||||
makeHandler := func(t *testing.T, name string, host netip.Addr, wrap func(http.Handler) http.Handler) {
|
||||
done := make(chan struct{})
|
||||
t.Cleanup(func() {
|
||||
close(done)
|
||||
})
|
||||
var handler http.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
conn, err := AcceptHTTP(context.Background(), w, r, server)
|
||||
if err != nil {
|
||||
log.Print(err)
|
||||
} else {
|
||||
defer conn.Close()
|
||||
}
|
||||
w.Header().Set("X-Handler-Name", name)
|
||||
<-done
|
||||
})
|
||||
if wrap != nil {
|
||||
handler = wrap(handler)
|
||||
}
|
||||
|
||||
httpLn, err := net.Listen("tcp", host.String()+":"+httpPort)
|
||||
if err != nil {
|
||||
t.Fatalf("HTTP listen: %v", err)
|
||||
}
|
||||
httpsLn, err := net.Listen("tcp", host.String()+":"+httpsPort)
|
||||
if err != nil {
|
||||
t.Fatalf("HTTPS listen: %v", err)
|
||||
}
|
||||
|
||||
httpServer := &http.Server{Handler: handler}
|
||||
go httpServer.Serve(httpLn)
|
||||
t.Cleanup(func() {
|
||||
httpServer.Close()
|
||||
})
|
||||
|
||||
httpsServer := &http.Server{
|
||||
Handler: handler,
|
||||
TLSConfig: tlsConfig(t),
|
||||
ErrorLog: logger.StdLogger(logger.WithPrefix(t.Logf, "http.Server.ErrorLog: ")),
|
||||
}
|
||||
go httpsServer.ServeTLS(httpsLn, "", "")
|
||||
t.Cleanup(func() {
|
||||
httpsServer.Close()
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
fallbackAddr := netip.MustParseAddr("127.0.0.1")
|
||||
goodAddr := netip.MustParseAddr("127.0.0.2")
|
||||
otherAddr := netip.MustParseAddr("127.0.0.3")
|
||||
other2Addr := netip.MustParseAddr("127.0.0.4")
|
||||
brokenAddr := netip.MustParseAddr("127.0.0.10")
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
plan *tailcfg.ControlDialPlan
|
||||
wrap func(http.Handler) http.Handler
|
||||
want netip.Addr
|
||||
|
||||
allowFallback bool
|
||||
}{
|
||||
{
|
||||
name: "single",
|
||||
plan: &tailcfg.ControlDialPlan{Candidates: []tailcfg.ControlIPCandidate{
|
||||
{IP: goodAddr, Priority: 1, DialTimeoutSec: 10},
|
||||
}},
|
||||
want: goodAddr,
|
||||
},
|
||||
{
|
||||
name: "broken-then-good",
|
||||
plan: &tailcfg.ControlDialPlan{Candidates: []tailcfg.ControlIPCandidate{
|
||||
// Dials the broken one, which fails, and then
|
||||
// eventually dials the good one and succeeds
|
||||
{IP: brokenAddr, Priority: 2, DialTimeoutSec: 10},
|
||||
{IP: goodAddr, Priority: 1, DialTimeoutSec: 10, DialStartDelaySec: 1},
|
||||
}},
|
||||
want: goodAddr,
|
||||
},
|
||||
{
|
||||
name: "multiple-priority-fast-path",
|
||||
plan: &tailcfg.ControlDialPlan{Candidates: []tailcfg.ControlIPCandidate{
|
||||
// Dials some good IPs and our bad one (which
|
||||
// hangs forever), which then hits the fast
|
||||
// path where we bail without waiting.
|
||||
{IP: brokenAddr, Priority: 1, DialTimeoutSec: 10},
|
||||
{IP: goodAddr, Priority: 1, DialTimeoutSec: 10},
|
||||
{IP: other2Addr, Priority: 1, DialTimeoutSec: 10},
|
||||
{IP: otherAddr, Priority: 2, DialTimeoutSec: 10},
|
||||
}},
|
||||
want: otherAddr,
|
||||
},
|
||||
{
|
||||
name: "multiple-priority-slow-path",
|
||||
plan: &tailcfg.ControlDialPlan{Candidates: []tailcfg.ControlIPCandidate{
|
||||
// Our broken address is the highest priority,
|
||||
// so we don't hit our fast path.
|
||||
{IP: brokenAddr, Priority: 10, DialTimeoutSec: 10},
|
||||
{IP: otherAddr, Priority: 2, DialTimeoutSec: 10},
|
||||
{IP: goodAddr, Priority: 1, DialTimeoutSec: 10},
|
||||
}},
|
||||
want: otherAddr,
|
||||
},
|
||||
{
|
||||
name: "fallback",
|
||||
plan: &tailcfg.ControlDialPlan{Candidates: []tailcfg.ControlIPCandidate{
|
||||
{IP: brokenAddr, Priority: 1, DialTimeoutSec: 1},
|
||||
}},
|
||||
want: fallbackAddr,
|
||||
allowFallback: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range testCases {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
makeHandler(t, "fallback", fallbackAddr, nil)
|
||||
makeHandler(t, "good", goodAddr, nil)
|
||||
makeHandler(t, "other", otherAddr, nil)
|
||||
makeHandler(t, "other2", other2Addr, nil)
|
||||
makeHandler(t, "broken", brokenAddr, func(h http.Handler) http.Handler {
|
||||
return http.HandlerFunc(brokenMITMHandler)
|
||||
})
|
||||
|
||||
dialer := closeTrackDialer{
|
||||
t: t,
|
||||
inner: new(tsdial.Dialer).SystemDial,
|
||||
conns: make(map[*closeTrackConn]bool),
|
||||
}
|
||||
defer dialer.Done()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
// By default, we intentionally point to something that
|
||||
// we know won't connect, since we want a fallback to
|
||||
// DNS to be an error.
|
||||
host := "example.com"
|
||||
if tt.allowFallback {
|
||||
host = "localhost"
|
||||
}
|
||||
|
||||
drained := make(chan struct{})
|
||||
a := &Dialer{
|
||||
Hostname: host,
|
||||
HTTPPort: httpPort,
|
||||
HTTPSPort: httpsPort,
|
||||
MachineKey: client,
|
||||
ControlKey: server.Public(),
|
||||
ProtocolVersion: testProtocolVersion,
|
||||
Dialer: dialer.Dial,
|
||||
Logf: t.Logf,
|
||||
DialPlan: tt.plan,
|
||||
proxyFunc: func(*http.Request) (*url.URL, error) { return nil, nil },
|
||||
drainFinished: drained,
|
||||
insecureTLS: true,
|
||||
testFallbackDelay: 50 * time.Millisecond,
|
||||
}
|
||||
|
||||
conn, err := a.dial(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("dialing controlhttp: %v", err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
raddr := conn.RemoteAddr().(*net.TCPAddr)
|
||||
|
||||
got, ok := netip.AddrFromSlice(raddr.IP)
|
||||
if !ok {
|
||||
t.Errorf("invalid remote IP: %v", raddr.IP)
|
||||
} else if got != tt.want {
|
||||
t.Errorf("got connection from %q; want %q", got, tt.want)
|
||||
} else {
|
||||
t.Logf("successfully connected to %q", raddr.String())
|
||||
}
|
||||
|
||||
// Wait until our dialer drains so we can verify that
|
||||
// all connections are closed.
|
||||
<-drained
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type closeTrackDialer struct {
|
||||
t testing.TB
|
||||
inner dnscache.DialContextFunc
|
||||
mu sync.Mutex
|
||||
conns map[*closeTrackConn]bool
|
||||
}
|
||||
|
||||
func (d *closeTrackDialer) Dial(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
c, err := d.inner(ctx, network, addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ct := &closeTrackConn{Conn: c, d: d}
|
||||
|
||||
d.mu.Lock()
|
||||
d.conns[ct] = true
|
||||
d.mu.Unlock()
|
||||
return ct, nil
|
||||
}
|
||||
|
||||
func (d *closeTrackDialer) Done() {
|
||||
// Unfortunately, tsdial.Dialer.SystemDial closes connections
|
||||
// asynchronously in a goroutine, so we can't assume that everything is
|
||||
// closed by the time we get here.
|
||||
//
|
||||
// Sleep/wait a few times on the assumption that things will close
|
||||
// "eventually".
|
||||
const iters = 100
|
||||
for i := 0; i < iters; i++ {
|
||||
d.mu.Lock()
|
||||
if len(d.conns) == 0 {
|
||||
d.mu.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
// Only error on last iteration
|
||||
if i != iters-1 {
|
||||
d.mu.Unlock()
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
continue
|
||||
}
|
||||
|
||||
for conn := range d.conns {
|
||||
d.t.Errorf("expected close of conn %p; RemoteAddr=%q", conn, conn.RemoteAddr().String())
|
||||
}
|
||||
d.mu.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
func (d *closeTrackDialer) noteClose(c *closeTrackConn) {
|
||||
d.mu.Lock()
|
||||
delete(d.conns, c) // safe if already deleted
|
||||
d.mu.Unlock()
|
||||
}
|
||||
|
||||
type closeTrackConn struct {
|
||||
net.Conn
|
||||
d *closeTrackDialer
|
||||
}
|
||||
|
||||
func (c *closeTrackConn) Close() error {
|
||||
c.d.noteClose(c)
|
||||
return c.Conn.Close()
|
||||
}
|
||||
|
||||
@@ -82,6 +82,12 @@ func acceptWebsocket(ctx context.Context, w http.ResponseWriter, r *http.Request
|
||||
c, err := websocket.Accept(w, r, &websocket.AcceptOptions{
|
||||
Subprotocols: []string{upgradeHeaderValue},
|
||||
OriginPatterns: []string{"*"},
|
||||
// Disable compression because we transmit Noise messages that are not
|
||||
// compressible.
|
||||
// Additionally, Safari has a broken implementation of compression
|
||||
// (see https://github.com/nhooyr/websocket/issues/218) that makes
|
||||
// enabling it actively harmful.
|
||||
CompressionMode: websocket.CompressionDisabled,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Could not accept WebSocket connection %v", err)
|
||||
|
||||
@@ -13,20 +13,18 @@ import (
|
||||
)
|
||||
|
||||
// disableUPnP indicates whether to attempt UPnP mapping.
|
||||
var disableUPnP atomic.Bool
|
||||
var disableUPnPControl atomic.Bool
|
||||
|
||||
func init() {
|
||||
SetDisableUPnP(envknob.Bool("TS_DISABLE_UPNP"))
|
||||
}
|
||||
var disableUPnpEnv = envknob.RegisterBool("TS_DISABLE_UPNP")
|
||||
|
||||
// DisableUPnP reports the last reported value from control
|
||||
// whether UPnP portmapping should be disabled.
|
||||
func DisableUPnP() bool {
|
||||
return disableUPnP.Load()
|
||||
return disableUPnPControl.Load() || disableUPnpEnv()
|
||||
}
|
||||
|
||||
// SetDisableUPnP sets whether control says that UPnP should be
|
||||
// disabled.
|
||||
func SetDisableUPnP(v bool) {
|
||||
disableUPnP.Store(v)
|
||||
disableUPnPControl.Store(v)
|
||||
}
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package derp implements DERP, the Detour Encrypted Routing Protocol.
|
||||
// Package derp implements the Designated Encrypted Relay for Packets (DERP)
|
||||
// protocol.
|
||||
//
|
||||
// DERP routes packets to clients using curve25519 keys as addresses.
|
||||
//
|
||||
@@ -18,7 +19,6 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -194,7 +194,7 @@ func readFrame(br *bufio.Reader, maxSize uint32, b []byte) (t frameType, frameLe
|
||||
}
|
||||
remain := frameLen - uint32(n)
|
||||
if remain > 0 {
|
||||
if _, err := io.CopyN(ioutil.Discard, br, int64(remain)); err != nil {
|
||||
if _, err := io.CopyN(io.Discard, br, int64(remain)); err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
err = io.ErrShortBuffer
|
||||
|
||||
@@ -18,7 +18,6 @@ import (
|
||||
"expvar"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"math"
|
||||
"math/big"
|
||||
@@ -47,8 +46,6 @@ import (
|
||||
"tailscale.com/version"
|
||||
)
|
||||
|
||||
var debug = envknob.Bool("DERP_DEBUG_LOGS")
|
||||
|
||||
// verboseDropKeys is the set of destination public keys that should
|
||||
// verbosely log whenever DERP drops a packet.
|
||||
var verboseDropKeys = map[key.NodePublic]bool{}
|
||||
@@ -106,6 +103,7 @@ type Server struct {
|
||||
limitedLogf logger.Logf
|
||||
metaCert []byte // the encoded x509 cert to send after LetsEncrypt cert+intermediate
|
||||
dupPolicy dupPolicy
|
||||
debug bool
|
||||
|
||||
// Counters:
|
||||
packetsSent, bytesSent expvar.Int
|
||||
@@ -299,6 +297,7 @@ func NewServer(privateKey key.NodePrivate, logf logger.Logf) *Server {
|
||||
runtime.ReadMemStats(&ms)
|
||||
|
||||
s := &Server{
|
||||
debug: envknob.Bool("DERP_DEBUG_LOGS"),
|
||||
privateKey: privateKey,
|
||||
publicKey: privateKey.Public(),
|
||||
logf: logf,
|
||||
@@ -758,7 +757,7 @@ func (c *sclient) run(ctx context.Context) error {
|
||||
}
|
||||
|
||||
func (c *sclient) handleUnknownFrame(ft frameType, fl uint32) error {
|
||||
_, err := io.CopyN(ioutil.Discard, c.br, int64(fl))
|
||||
_, err := io.CopyN(io.Discard, c.br, int64(fl))
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -801,7 +800,7 @@ func (c *sclient) handleFramePing(ft frameType, fl uint32) error {
|
||||
return err
|
||||
}
|
||||
if extra := int64(fl) - int64(len(m)); extra > 0 {
|
||||
_, err = io.CopyN(ioutil.Discard, c.br, extra)
|
||||
_, err = io.CopyN(io.Discard, c.br, extra)
|
||||
}
|
||||
select {
|
||||
case c.sendPongCh <- [8]byte(m):
|
||||
@@ -980,7 +979,7 @@ func (s *Server) recordDrop(packetBytes []byte, srcKey, dstKey key.NodePublic, r
|
||||
msg := fmt.Sprintf("drop (%s) %s -> %s", srcKey.ShortString(), reason, dstKey.ShortString())
|
||||
s.limitedLogf(msg)
|
||||
}
|
||||
if debug {
|
||||
if s.debug {
|
||||
s.logf("dropping packet reason=%s dst=%s disco=%v", reason, dstKey, disco.LooksLikeDiscoWrapper(packetBytes))
|
||||
}
|
||||
}
|
||||
@@ -1828,7 +1827,7 @@ func (s *Server) ServeDebugTraffic(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
var bufioWriterPool = &sync.Pool{
|
||||
New: func() any {
|
||||
return bufio.NewWriterSize(ioutil.Discard, 2<<10)
|
||||
return bufio.NewWriterSize(io.Discard, 2<<10)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1861,7 +1860,7 @@ func (w *lazyBufioWriter) Flush() error {
|
||||
}
|
||||
err := w.lbw.Flush()
|
||||
|
||||
w.lbw.Reset(ioutil.Discard)
|
||||
w.lbw.Reset(io.Discard)
|
||||
bufioWriterPool.Put(w.lbw)
|
||||
w.lbw = nil
|
||||
|
||||
|
||||
@@ -15,9 +15,9 @@ import (
|
||||
"expvar"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"reflect"
|
||||
"sync"
|
||||
"testing"
|
||||
@@ -232,7 +232,7 @@ func TestSendFreeze(t *testing.T) {
|
||||
// alice --> bob
|
||||
// alice --> cathy
|
||||
//
|
||||
// Then cathy stops processing messsages.
|
||||
// Then cathy stops processing messages.
|
||||
// That should not interfere with alice talking to bob.
|
||||
|
||||
newClient := func(ctx context.Context, name string, k key.NodePrivate) (c *Client, clientConn nettest.Conn) {
|
||||
@@ -772,7 +772,7 @@ func TestForwarderRegistration(t *testing.T) {
|
||||
})
|
||||
|
||||
// Now pretend u1 was already connected locally (so clientsMesh[u1] is nil), and then we heard
|
||||
// that they're also connected to a peer of ours. That sholdn't transition the forwarder
|
||||
// that they're also connected to a peer of ours. That shouldn't transition the forwarder
|
||||
// from nil to the new one, not a multiForwarder.
|
||||
s.clients[u1] = singleClient{u1c}
|
||||
s.clientsMesh[u1] = nil
|
||||
@@ -1240,7 +1240,7 @@ func benchmarkSendRecvSize(b *testing.B, packetSize int) {
|
||||
}
|
||||
|
||||
func BenchmarkWriteUint32(b *testing.B) {
|
||||
w := bufio.NewWriter(ioutil.Discard)
|
||||
w := bufio.NewWriter(io.Discard)
|
||||
b.ReportAllocs()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
@@ -1279,9 +1279,9 @@ func waitConnect(t testing.TB, c *Client) {
|
||||
}
|
||||
|
||||
func TestParseSSOutput(t *testing.T) {
|
||||
contents, err := ioutil.ReadFile("testdata/example_ss.txt")
|
||||
contents, err := os.ReadFile("testdata/example_ss.txt")
|
||||
if err != nil {
|
||||
t.Errorf("ioutil.Readfile(example_ss.txt) failed: %v", err)
|
||||
t.Errorf("os.ReadFile(example_ss.txt) failed: %v", err)
|
||||
}
|
||||
seen := parseSSOutput(string(contents))
|
||||
if len(seen) == 0 {
|
||||
|
||||
@@ -19,7 +19,6 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
@@ -97,7 +96,7 @@ func NewRegionClient(privateKey key.NodePrivate, logf logger.Logf, getRegion fun
|
||||
return c
|
||||
}
|
||||
|
||||
// NewNetcheckClient returns a Client that's only able to have its DialRegion method called.
|
||||
// NewNetcheckClient returns a Client that's only able to have its DialRegionTLS method called.
|
||||
// It's used by the netcheck package.
|
||||
func NewNetcheckClient(logf logger.Logf) *Client {
|
||||
return &Client{logf: logf}
|
||||
@@ -200,7 +199,7 @@ func (c *Client) urlString(node *tailcfg.DERPNode) string {
|
||||
return fmt.Sprintf("https://%s/derp", node.HostName)
|
||||
}
|
||||
|
||||
// AddressFamilySelector decides whethers IPv6 is preferred for
|
||||
// AddressFamilySelector decides whether IPv6 is preferred for
|
||||
// outbound dials.
|
||||
type AddressFamilySelector interface {
|
||||
// PreferIPv6 reports whether IPv4 dials should be slightly
|
||||
@@ -432,7 +431,7 @@ func (c *Client) connect(ctx context.Context, caller string) (client *derp.Clien
|
||||
return nil, 0, err
|
||||
}
|
||||
if resp.StatusCode != http.StatusSwitchingProtocols {
|
||||
b, _ := ioutil.ReadAll(resp.Body)
|
||||
b, _ := io.ReadAll(resp.Body)
|
||||
resp.Body.Close()
|
||||
return nil, 0, fmt.Errorf("GET failed: %v: %s", err, b)
|
||||
}
|
||||
@@ -986,7 +985,9 @@ func (c *Client) isClosed() bool {
|
||||
// Close closes the client. It will not automatically reconnect after
|
||||
// being closed.
|
||||
func (c *Client) Close() error {
|
||||
c.cancelCtx() // not in lock, so it can cancel Connect, which holds mu
|
||||
if c.cancelCtx != nil {
|
||||
c.cancelCtx() // not in lock, so it can cancel Connect, which holds mu
|
||||
}
|
||||
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
@@ -15,9 +15,9 @@
|
||||
// The recipient then decrypts the bytes following (the nacl secretbox)
|
||||
// and then the inner payload structure is:
|
||||
//
|
||||
// messageType byte (the MessageType constants below)
|
||||
// messageVersion byte (0 for now; but always ignore bytes at the end)
|
||||
// message-paylod [...]byte
|
||||
// messageType byte (the MessageType constants below)
|
||||
// messageVersion byte (0 for now; but always ignore bytes at the end)
|
||||
// message-payload [...]byte
|
||||
package disco
|
||||
|
||||
import (
|
||||
|
||||
@@ -9,7 +9,7 @@ spec:
|
||||
serviceAccountName: "{{SA_NAME}}"
|
||||
initContainers:
|
||||
# In order to run as a proxy we need to enable IP Forwarding inside
|
||||
# the container. The `net.ipv4.ip_forward` sysctl is not whitelisted
|
||||
# the container. The `net.ipv4.ip_forward` sysctl is not allowlisted
|
||||
# in Kubelet by default.
|
||||
- name: sysctler
|
||||
image: busybox
|
||||
@@ -18,7 +18,7 @@ spec:
|
||||
command: ["/bin/sh"]
|
||||
args:
|
||||
- -c
|
||||
- sysctl -w net.ipv4.ip_forward=1 -w net.ipv6.conf.all.forwarding=1
|
||||
- sysctl -w net.ipv4.ip_forward=1 net.ipv6.conf.all.forwarding=1
|
||||
resources:
|
||||
requests:
|
||||
cpu: 1m
|
||||
@@ -37,7 +37,7 @@ spec:
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: tailscale-auth
|
||||
key: AUTH_KEY
|
||||
key: TS_AUTH_KEY
|
||||
optional: true
|
||||
- name: TS_DEST_IP
|
||||
value: "{{TS_DEST_IP}}"
|
||||
|
||||
@@ -17,10 +17,11 @@ TS_KUBE_SECRET="${TS_KUBE_SECRET:-tailscale}"
|
||||
TS_SOCKS5_SERVER="${TS_SOCKS5_SERVER:-}"
|
||||
TS_OUTBOUND_HTTP_PROXY_LISTEN="${TS_OUTBOUND_HTTP_PROXY_LISTEN:-}"
|
||||
TS_TAILSCALED_EXTRA_ARGS="${TS_TAILSCALED_EXTRA_ARGS:-}"
|
||||
TS_SOCKET="${TS_SOCKET:-/tmp/tailscaled.sock}"
|
||||
|
||||
set -e
|
||||
|
||||
TAILSCALED_ARGS="--socket=/tmp/tailscaled.sock"
|
||||
TAILSCALED_ARGS="--socket=${TS_SOCKET}"
|
||||
|
||||
if [[ ! -z "${KUBERNETES_SERVICE_HOST}" ]]; then
|
||||
TAILSCALED_ARGS="${TAILSCALED_ARGS} --state=kube:${TS_KUBE_SECRET} --statedir=${TS_STATE_DIR:-/tmp}"
|
||||
@@ -81,11 +82,11 @@ if [[ ! -z "${TS_EXTRA_ARGS}" ]]; then
|
||||
fi
|
||||
|
||||
echo "Running tailscale up"
|
||||
tailscale --socket=/tmp/tailscaled.sock up ${UP_ARGS}
|
||||
tailscale --socket="${TS_SOCKET}" up ${UP_ARGS}
|
||||
|
||||
if [[ ! -z "${TS_DEST_IP}" ]]; then
|
||||
echo "Adding iptables rule for DNAT"
|
||||
iptables -t nat -I PREROUTING -d "$(tailscale --socket=/tmp/tailscaled.sock ip -4)" -j DNAT --to-destination "${TS_DEST_IP}"
|
||||
iptables -t nat -I PREROUTING -d "$(tailscale --socket=${TS_SOCKET} ip -4)" -j DNAT --to-destination "${TS_DEST_IP}"
|
||||
fi
|
||||
|
||||
echo "Waiting for tailscaled to exit"
|
||||
|
||||
@@ -23,7 +23,7 @@ spec:
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: tailscale-auth
|
||||
key: AUTH_KEY
|
||||
key: TS_AUTH_KEY
|
||||
optional: true
|
||||
securityContext:
|
||||
capabilities:
|
||||
|
||||
@@ -23,7 +23,7 @@ spec:
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: tailscale-auth
|
||||
key: AUTH_KEY
|
||||
key: TS_AUTH_KEY
|
||||
optional: true
|
||||
- name: TS_ROUTES
|
||||
value: "{{TS_ROUTES}}"
|
||||
|
||||
@@ -26,5 +26,5 @@ spec:
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
name: tailscale-auth
|
||||
key: AUTH_KEY
|
||||
key: TS_AUTH_KEY
|
||||
optional: true
|
||||
|
||||
80
doctor/doctor.go
Normal file
80
doctor/doctor.go
Normal file
@@ -0,0 +1,80 @@
|
||||
// Copyright (c) 2022 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 doctor contains more in-depth healthchecks that can be run to aid in
|
||||
// diagnosing Tailscale issues.
|
||||
package doctor
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
// Check is the interface defining a singular check.
|
||||
//
|
||||
// A check should log information that it gathers using the provided log
|
||||
// function, and should attempt to make as much progress as possible in error
|
||||
// conditions.
|
||||
type Check interface {
|
||||
// Name should return a name describing this check, in lower-kebab-case
|
||||
// (i.e. "my-check", not "MyCheck" or "my_check").
|
||||
Name() string
|
||||
// Run executes the check, logging diagnostic information to the
|
||||
// provided logger function.
|
||||
Run(context.Context, logger.Logf) error
|
||||
}
|
||||
|
||||
// RunChecks runs a list of checks in parallel, and logs any returned errors
|
||||
// after all checks have returned.
|
||||
func RunChecks(ctx context.Context, log logger.Logf, checks ...Check) {
|
||||
if len(checks) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
type namedErr struct {
|
||||
name string
|
||||
err error
|
||||
}
|
||||
errs := make(chan namedErr, len(checks))
|
||||
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(len(checks))
|
||||
for _, check := range checks {
|
||||
go func(c Check) {
|
||||
defer wg.Done()
|
||||
|
||||
plog := logger.WithPrefix(log, c.Name()+": ")
|
||||
errs <- namedErr{
|
||||
name: c.Name(),
|
||||
err: c.Run(ctx, plog),
|
||||
}
|
||||
}(check)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
close(errs)
|
||||
|
||||
for n := range errs {
|
||||
if n.err == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
log("check %s: %v", n.name, n.err)
|
||||
}
|
||||
}
|
||||
|
||||
// CheckFunc creates a Check from a name and a function.
|
||||
func CheckFunc(name string, run func(context.Context, logger.Logf) error) Check {
|
||||
return checkFunc{name, run}
|
||||
}
|
||||
|
||||
type checkFunc struct {
|
||||
name string
|
||||
run func(context.Context, logger.Logf) error
|
||||
}
|
||||
|
||||
func (c checkFunc) Name() string { return c.name }
|
||||
func (c checkFunc) Run(ctx context.Context, log logger.Logf) error { return c.run(ctx, log) }
|
||||
50
doctor/doctor_test.go
Normal file
50
doctor/doctor_test.go
Normal file
@@ -0,0 +1,50 @@
|
||||
// Copyright (c) 2022 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 doctor
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
qt "github.com/frankban/quicktest"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
func TestRunChecks(t *testing.T) {
|
||||
c := qt.New(t)
|
||||
var (
|
||||
mu sync.Mutex
|
||||
lines []string
|
||||
)
|
||||
logf := func(format string, args ...any) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
lines = append(lines, fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
RunChecks(ctx, logf,
|
||||
testCheck1{},
|
||||
CheckFunc("testcheck2", func(_ context.Context, log logger.Logf) error {
|
||||
log("check 2")
|
||||
return nil
|
||||
}),
|
||||
)
|
||||
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
c.Assert(lines, qt.Contains, "testcheck1: check 1")
|
||||
c.Assert(lines, qt.Contains, "testcheck2: check 2")
|
||||
}
|
||||
|
||||
type testCheck1 struct{}
|
||||
|
||||
func (t testCheck1) Name() string { return "testcheck1" }
|
||||
func (t testCheck1) Run(_ context.Context, log logger.Logf) error {
|
||||
log("check 1")
|
||||
return nil
|
||||
}
|
||||
35
doctor/routetable/routetable.go
Normal file
35
doctor/routetable/routetable.go
Normal file
@@ -0,0 +1,35 @@
|
||||
// Copyright (c) 2022 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 routetable provides a doctor.Check that dumps the current system's
|
||||
// route table to the log.
|
||||
package routetable
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"tailscale.com/net/routetable"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
// MaxRoutes is the maximum number of routes that will be displayed.
|
||||
const MaxRoutes = 1000
|
||||
|
||||
// Check implements the doctor.Check interface.
|
||||
type Check struct{}
|
||||
|
||||
func (Check) Name() string {
|
||||
return "routetable"
|
||||
}
|
||||
|
||||
func (Check) Run(_ context.Context, logf logger.Logf) error {
|
||||
rs, err := routetable.Get(MaxRoutes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, r := range rs {
|
||||
logf("%s", r)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -17,30 +17,43 @@
|
||||
package envknob
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"tailscale.com/types/opt"
|
||||
"tailscale.com/version/distro"
|
||||
)
|
||||
|
||||
var (
|
||||
mu sync.Mutex
|
||||
set = map[string]string{}
|
||||
list []string
|
||||
mu sync.Mutex
|
||||
set = map[string]string{}
|
||||
regStr = map[string]*string{}
|
||||
regBool = map[string]*bool{}
|
||||
regOptBool = map[string]*opt.Bool{}
|
||||
)
|
||||
|
||||
func noteEnv(k, v string) {
|
||||
if v == "" {
|
||||
return
|
||||
}
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
if _, ok := set[k]; !ok {
|
||||
list = append(list, k)
|
||||
noteEnvLocked(k, v)
|
||||
}
|
||||
|
||||
func noteEnvLocked(k, v string) {
|
||||
if v != "" {
|
||||
set[k] = v
|
||||
} else {
|
||||
delete(set, k)
|
||||
}
|
||||
set[k] = v
|
||||
}
|
||||
|
||||
// logf is logger.Logf, but logger depends on envknob, so for circular
|
||||
@@ -52,11 +65,39 @@ type logf = func(format string, args ...any)
|
||||
func LogCurrent(logf logf) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
|
||||
list := make([]string, 0, len(set))
|
||||
for k := range set {
|
||||
list = append(list, k)
|
||||
}
|
||||
sort.Strings(list)
|
||||
for _, k := range list {
|
||||
logf("envknob: %s=%q", k, set[k])
|
||||
}
|
||||
}
|
||||
|
||||
// Setenv changes an environment variable.
|
||||
//
|
||||
// It is not safe for concurrent reading of environment variables via the
|
||||
// Register functions. All Setenv calls are meant to happen early in main before
|
||||
// any goroutines are started.
|
||||
func Setenv(envVar, val string) {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
os.Setenv(envVar, val)
|
||||
noteEnvLocked(envVar, val)
|
||||
|
||||
if p := regStr[envVar]; p != nil {
|
||||
*p = val
|
||||
}
|
||||
if p := regBool[envVar]; p != nil {
|
||||
setBoolLocked(p, envVar, val)
|
||||
}
|
||||
if p := regOptBool[envVar]; p != nil {
|
||||
setOptBoolLocked(p, envVar, val)
|
||||
}
|
||||
}
|
||||
|
||||
// String returns the named environment variable, using os.Getenv.
|
||||
//
|
||||
// If the variable is non-empty, it's also tracked & logged as being
|
||||
@@ -67,6 +108,82 @@ func String(envVar string) string {
|
||||
return v
|
||||
}
|
||||
|
||||
// RegisterString returns a func that gets the named environment variable,
|
||||
// without a map lookup per call. It assumes that mutations happen via
|
||||
// envknob.Setenv.
|
||||
func RegisterString(envVar string) func() string {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
p, ok := regStr[envVar]
|
||||
if !ok {
|
||||
val := os.Getenv(envVar)
|
||||
if val != "" {
|
||||
noteEnvLocked(envVar, val)
|
||||
}
|
||||
p = &val
|
||||
regStr[envVar] = p
|
||||
}
|
||||
return func() string { return *p }
|
||||
}
|
||||
|
||||
// RegisterBool returns a func that gets the named environment variable,
|
||||
// without a map lookup per call. It assumes that mutations happen via
|
||||
// envknob.Setenv.
|
||||
func RegisterBool(envVar string) func() bool {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
p, ok := regBool[envVar]
|
||||
if !ok {
|
||||
var b bool
|
||||
p = &b
|
||||
setBoolLocked(p, envVar, os.Getenv(envVar))
|
||||
regBool[envVar] = p
|
||||
}
|
||||
return func() bool { return *p }
|
||||
}
|
||||
|
||||
// RegisterOptBool returns a func that gets the named environment variable,
|
||||
// without a map lookup per call. It assumes that mutations happen via
|
||||
// envknob.Setenv.
|
||||
func RegisterOptBool(envVar string) func() opt.Bool {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
p, ok := regOptBool[envVar]
|
||||
if !ok {
|
||||
var b opt.Bool
|
||||
p = &b
|
||||
setOptBoolLocked(p, envVar, os.Getenv(envVar))
|
||||
regOptBool[envVar] = p
|
||||
}
|
||||
return func() opt.Bool { return *p }
|
||||
}
|
||||
|
||||
func setBoolLocked(p *bool, envVar, val string) {
|
||||
noteEnvLocked(envVar, val)
|
||||
if val == "" {
|
||||
*p = false
|
||||
return
|
||||
}
|
||||
var err error
|
||||
*p, err = strconv.ParseBool(val)
|
||||
if err != nil {
|
||||
log.Fatalf("invalid boolean environment variable %s value %q", envVar, val)
|
||||
}
|
||||
}
|
||||
|
||||
func setOptBoolLocked(p *opt.Bool, envVar, val string) {
|
||||
noteEnvLocked(envVar, val)
|
||||
if val == "" {
|
||||
*p = ""
|
||||
return
|
||||
}
|
||||
b, err := strconv.ParseBool(val)
|
||||
if err != nil {
|
||||
log.Fatalf("invalid boolean environment variable %s value %q", envVar, val)
|
||||
}
|
||||
p.Set(b)
|
||||
}
|
||||
|
||||
// Bool returns the boolean value of the named environment variable.
|
||||
// If the variable is not set, it returns false.
|
||||
// An invalid value exits the binary with a failure.
|
||||
@@ -81,6 +198,7 @@ func BoolDefaultTrue(envVar string) bool {
|
||||
}
|
||||
|
||||
func boolOr(envVar string, implicitValue bool) bool {
|
||||
assertNotInInit()
|
||||
val := os.Getenv(envVar)
|
||||
if val == "" {
|
||||
return implicitValue
|
||||
@@ -98,6 +216,7 @@ func boolOr(envVar string, implicitValue bool) bool {
|
||||
// The ok result is whether a value was set.
|
||||
// If the value isn't a valid int, it exits the program with a failure.
|
||||
func LookupBool(envVar string) (v bool, ok bool) {
|
||||
assertNotInInit()
|
||||
val := os.Getenv(envVar)
|
||||
if val == "" {
|
||||
return false, false
|
||||
@@ -113,6 +232,7 @@ func LookupBool(envVar string) (v bool, ok bool) {
|
||||
// OptBool is like Bool, but returns an opt.Bool, so the caller can
|
||||
// distinguish between implicitly and explicitly false.
|
||||
func OptBool(envVar string) opt.Bool {
|
||||
assertNotInInit()
|
||||
b, ok := LookupBool(envVar)
|
||||
if !ok {
|
||||
return ""
|
||||
@@ -126,6 +246,7 @@ func OptBool(envVar string) opt.Bool {
|
||||
// The ok result is whether a value was set.
|
||||
// If the value isn't a valid int, it exits the program with a failure.
|
||||
func LookupInt(envVar string) (v int, ok bool) {
|
||||
assertNotInInit()
|
||||
val := os.Getenv(envVar)
|
||||
if val == "" {
|
||||
return 0, false
|
||||
@@ -156,6 +277,11 @@ func SSHPolicyFile() string { return String("TS_DEBUG_SSH_POLICY_FILE") }
|
||||
// SSHIgnoreTailnetPolicy is whether to ignore the Tailnet SSH policy for development.
|
||||
func SSHIgnoreTailnetPolicy() bool { return Bool("TS_DEBUG_SSH_IGNORE_TAILNET_POLICY") }
|
||||
|
||||
|
||||
// TKASkipSignatureCheck is whether to skip node-key signature checking for development.
|
||||
func TKASkipSignatureCheck() bool { return Bool("TS_UNSAFE_SKIP_NKS_VERIFICATION") }
|
||||
|
||||
|
||||
// NoLogsNoSupport reports whether the client's opted out of log uploads and
|
||||
// technical support.
|
||||
func NoLogsNoSupport() bool {
|
||||
@@ -164,5 +290,142 @@ func NoLogsNoSupport() bool {
|
||||
|
||||
// SetNoLogsNoSupport enables no-logs-no-support mode.
|
||||
func SetNoLogsNoSupport() {
|
||||
os.Setenv("TS_NO_LOGS_NO_SUPPORT", "true")
|
||||
Setenv("TS_NO_LOGS_NO_SUPPORT", "true")
|
||||
}
|
||||
|
||||
// notInInit is set true the first time we've seen a non-init stack trace.
|
||||
var notInInit atomic.Bool
|
||||
|
||||
func assertNotInInit() {
|
||||
if notInInit.Load() {
|
||||
return
|
||||
}
|
||||
skip := 0
|
||||
for {
|
||||
pc, _, _, ok := runtime.Caller(skip)
|
||||
if !ok {
|
||||
notInInit.Store(true)
|
||||
return
|
||||
}
|
||||
fu := runtime.FuncForPC(pc)
|
||||
if fu == nil {
|
||||
return
|
||||
}
|
||||
name := fu.Name()
|
||||
name = strings.TrimRightFunc(name, func(r rune) bool { return r >= '0' && r <= '9' })
|
||||
if strings.HasSuffix(name, ".init") || strings.HasSuffix(name, ".init.") {
|
||||
stack := make([]byte, 1<<10)
|
||||
stack = stack[:runtime.Stack(stack, false)]
|
||||
envCheckedInInitStack = stack
|
||||
}
|
||||
skip++
|
||||
}
|
||||
}
|
||||
|
||||
var envCheckedInInitStack []byte
|
||||
|
||||
// PanicIfAnyEnvCheckedInInit panics if environment variables were read during
|
||||
// init.
|
||||
func PanicIfAnyEnvCheckedInInit() {
|
||||
if envCheckedInInitStack != nil {
|
||||
panic("envknob check of called from init function: " + string(envCheckedInInitStack))
|
||||
}
|
||||
}
|
||||
|
||||
var applyDiskConfigErr error
|
||||
|
||||
// ApplyDiskConfigError returns the most recent result of ApplyDiskConfig.
|
||||
func ApplyDiskConfigError() error { return applyDiskConfigErr }
|
||||
|
||||
// ApplyDiskConfig returns a platform-specific config file of environment keys/values and
|
||||
// applies them. On Linux and Unix operating systems, it's a no-op and always returns nil.
|
||||
// If no platform-specific config file is found, it also returns nil.
|
||||
//
|
||||
// It exists primarily for Windows to make it easy to apply environment variables to
|
||||
// a running service in a way similar to modifying /etc/default/tailscaled on Linux.
|
||||
// On Windows, you use %ProgramData%\Tailscale\tailscaled-env.txt instead.
|
||||
func ApplyDiskConfig() (err error) {
|
||||
var f *os.File
|
||||
defer func() {
|
||||
if err != nil {
|
||||
// Stash away our return error for the healthcheck package to use.
|
||||
applyDiskConfigErr = fmt.Errorf("error parsing %s: %w", f.Name(), err)
|
||||
}
|
||||
}()
|
||||
|
||||
// First try the explicitly-provided value for development testing. Not
|
||||
// useful for users to use on their own. (if they can set this, they can set
|
||||
// any environment variable anyway)
|
||||
if name := os.Getenv("TS_DEBUG_ENV_FILE"); name != "" {
|
||||
f, err = os.Open(name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error opening explicitly configured TS_DEBUG_ENV_FILE: %w", err)
|
||||
}
|
||||
defer f.Close()
|
||||
return applyKeyValueEnv(f)
|
||||
}
|
||||
|
||||
name := getPlatformEnvFile()
|
||||
if name == "" {
|
||||
return nil
|
||||
}
|
||||
f, err = os.Open(name)
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
return applyKeyValueEnv(f)
|
||||
}
|
||||
|
||||
// getPlatformEnvFile returns the current platform's path to an optional
|
||||
// tailscaled-env.txt file. It returns an empty string if none is defined
|
||||
// for the platform.
|
||||
func getPlatformEnvFile() string {
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
return filepath.Join(os.Getenv("ProgramData"), "Tailscale", "tailscaled-env.txt")
|
||||
case "linux":
|
||||
if distro.Get() == distro.Synology {
|
||||
return "/etc/tailscale/tailscaled-env.txt"
|
||||
}
|
||||
case "darwin":
|
||||
// TODO(bradfitz): figure this out. There are three ways to run
|
||||
// Tailscale on macOS (tailscaled, GUI App Store, GUI System Extension)
|
||||
// and we should deal with all three.
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// applyKeyValueEnv reads key=value lines r and calls Setenv for each.
|
||||
//
|
||||
// Empty lines and lines beginning with '#' are skipped.
|
||||
//
|
||||
// Values can be double quoted, in which case they're unquoted using
|
||||
// strconv.Unquote.
|
||||
func applyKeyValueEnv(r io.Reader) error {
|
||||
bs := bufio.NewScanner(r)
|
||||
for bs.Scan() {
|
||||
line := strings.TrimSpace(bs.Text())
|
||||
if line == "" || line[0] == '#' {
|
||||
continue
|
||||
}
|
||||
k, v, ok := strings.Cut(line, "=")
|
||||
k = strings.TrimSpace(k)
|
||||
if !ok || k == "" {
|
||||
continue
|
||||
}
|
||||
v = strings.TrimSpace(v)
|
||||
if strings.HasPrefix(v, `"`) {
|
||||
var err error
|
||||
v, err = strconv.Unquote(v)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid value in line %q: %v", line, err)
|
||||
}
|
||||
}
|
||||
Setenv(k, v)
|
||||
}
|
||||
return bs.Err()
|
||||
}
|
||||
|
||||
9
go.mod
9
go.mod
@@ -44,7 +44,7 @@ require (
|
||||
github.com/tailscale/certstore v0.1.1-0.20220316223106-78d6e1c49d8d
|
||||
github.com/tailscale/depaware v0.0.0-20210622194025-720c4b409502
|
||||
github.com/tailscale/goexpect v0.0.0-20210902213824-6e8c725cea41
|
||||
github.com/tailscale/golang-x-crypto v0.0.0-20220428210705-0b941c09a5e1
|
||||
github.com/tailscale/golang-x-crypto v0.0.0-20221011214003-2ffa11beee90
|
||||
github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05
|
||||
github.com/tailscale/hujson v0.0.0-20220630195928-54599719472f
|
||||
github.com/tailscale/mkctr v0.0.0-20220601142259-c0b937af2e89
|
||||
@@ -57,9 +57,9 @@ require (
|
||||
go4.org/netipx v0.0.0-20220725152314-7e7bdc8411bf
|
||||
golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f
|
||||
golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e
|
||||
golang.org/x/net v0.0.0-20220607020251-c690dde0001d
|
||||
golang.org/x/net v0.0.0-20221002022538-bcab6841153b
|
||||
golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8
|
||||
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211
|
||||
golang.org/x/time v0.0.0-20211116232009-f0f3c7e86c11
|
||||
golang.org/x/tools v0.1.11
|
||||
@@ -210,7 +210,6 @@ require (
|
||||
github.com/nishanths/exhaustive v0.7.11 // indirect
|
||||
github.com/nishanths/predeclared v0.2.1 // indirect
|
||||
github.com/olekukonko/tablewriter v0.0.5 // indirect
|
||||
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.0.3-0.20220114050600-8b9d41f48198 // indirect
|
||||
github.com/pelletier/go-toml v1.9.4 // indirect
|
||||
@@ -230,7 +229,7 @@ require (
|
||||
github.com/ryancurrah/gomodguard v1.2.3 // indirect
|
||||
github.com/ryanrolds/sqlclosecheck v0.3.0 // indirect
|
||||
github.com/sanposhiho/wastedassign/v2 v2.0.7 // indirect
|
||||
github.com/sassoftware/go-rpmutils v0.0.0-20190420191620-a8f1baeba37b // indirect
|
||||
github.com/sassoftware/go-rpmutils v0.1.0 // indirect
|
||||
github.com/securego/gosec/v2 v2.9.3 // indirect
|
||||
github.com/sergi/go-diff v1.2.0 // indirect
|
||||
github.com/shazow/go-diff v0.0.0-20160112020656-b6b7b6733b8c // indirect
|
||||
|
||||
16
go.sum
16
go.sum
@@ -876,7 +876,6 @@ github.com/onsi/gomega v1.10.2/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1y
|
||||
github.com/onsi/gomega v1.16.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
|
||||
github.com/onsi/gomega v1.17.0 h1:9Luw4uT5HTjHTN8+aNcSThgH1vdXnmdJ8xIfZ4wyTRE=
|
||||
github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
|
||||
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 h1:lDH9UUVJtmYCjyT0CI4q8xvlXPxeZ0gYCVvWbmPlp88=
|
||||
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
|
||||
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
|
||||
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
|
||||
@@ -990,8 +989,9 @@ github.com/sagikazarmark/crypt v0.1.0/go.mod h1:B/mN0msZuINBtQ1zZLEQcegFJJf9vnYI
|
||||
github.com/sanposhiho/wastedassign/v2 v2.0.6/go.mod h1:KyZ0MWTwxxBmfwn33zh3k1dmsbF2ud9pAAGfoLfjhtI=
|
||||
github.com/sanposhiho/wastedassign/v2 v2.0.7 h1:J+6nrY4VW+gC9xFzUc+XjPD3g3wF3je/NsJFwFK7Uxc=
|
||||
github.com/sanposhiho/wastedassign/v2 v2.0.7/go.mod h1:KyZ0MWTwxxBmfwn33zh3k1dmsbF2ud9pAAGfoLfjhtI=
|
||||
github.com/sassoftware/go-rpmutils v0.0.0-20190420191620-a8f1baeba37b h1:+gCnWOZV8Z/8jehJ2CdqB47Z3S+SREmQcuXkRFLNsiI=
|
||||
github.com/sassoftware/go-rpmutils v0.0.0-20190420191620-a8f1baeba37b/go.mod h1:am+Fp8Bt506lA3Rk3QCmSqmYmLMnPDhdDUcosQCAx+I=
|
||||
github.com/sassoftware/go-rpmutils v0.1.0 h1:VLrna+tV+77Tclr956QkY/pTyyKomQlq2Xw6PuE8tsc=
|
||||
github.com/sassoftware/go-rpmutils v0.1.0/go.mod h1:euhXULoBpvAxqrBHEyJS4Tsu3hHxUmQWNymxoJbzgUY=
|
||||
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
|
||||
github.com/securego/gosec/v2 v2.5.0/go.mod h1:L/CDXVntIff5ypVHIkqPXbtRpJiNCh6c6Amn68jXDjo=
|
||||
github.com/securego/gosec/v2 v2.9.1/go.mod h1:oDcDLcatOJxkCGaCaq8lua1jTnYf6Sou4wdiJ1n4iHc=
|
||||
@@ -1082,8 +1082,8 @@ github.com/tailscale/depaware v0.0.0-20210622194025-720c4b409502 h1:34icjjmqJ2HP
|
||||
github.com/tailscale/depaware v0.0.0-20210622194025-720c4b409502/go.mod h1:p9lPsd+cx33L3H9nNoecRRxPssFKUwwI50I3pZ0yT+8=
|
||||
github.com/tailscale/goexpect v0.0.0-20210902213824-6e8c725cea41 h1:/V2rCMMWcsjYaYO2MeovLw+ClP63OtXgCF2Y1eb8+Ns=
|
||||
github.com/tailscale/goexpect v0.0.0-20210902213824-6e8c725cea41/go.mod h1:/roCdA6gg6lQyw/Oz6gIIGu3ggJKYhF+WC/AQReE5XQ=
|
||||
github.com/tailscale/golang-x-crypto v0.0.0-20220428210705-0b941c09a5e1 h1:vsFV6BKSIgjRd8m8UfrGW4r+cc28fRF71K6IRo46rKs=
|
||||
github.com/tailscale/golang-x-crypto v0.0.0-20220428210705-0b941c09a5e1/go.mod h1:95n9fbUCixVSI4QXLEvdKJjnYK2eUlkTx9+QwLPXFKU=
|
||||
github.com/tailscale/golang-x-crypto v0.0.0-20221011214003-2ffa11beee90 h1:Vw3TVi00T2/J3yU22807VB0K0Fo8lNMUBEo2gL0L1bM=
|
||||
github.com/tailscale/golang-x-crypto v0.0.0-20221011214003-2ffa11beee90/go.mod h1:95n9fbUCixVSI4QXLEvdKJjnYK2eUlkTx9+QwLPXFKU=
|
||||
github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05 h1:4chzWmimtJPxRs2O36yuGRW3f9SYV+bMTTvMBI0EKio=
|
||||
github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05/go.mod h1:PdCqy9JzfWMJf1H5UJW2ip33/d4YkoKN0r67yKH1mG8=
|
||||
github.com/tailscale/hujson v0.0.0-20220630195928-54599719472f h1:n4r/sJ92cBSBHK8n9lR1XLFr0OiTVeGfN5TR+9LaN7E=
|
||||
@@ -1235,6 +1235,7 @@ golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8U
|
||||
golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200302210943-78000ba7a073/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20201208171446-5f87f3452ae9/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||
@@ -1354,8 +1355,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.0.0-20220607020251-c690dde0001d h1:4SFsTMi4UahlKoloni7L4eYzhFRifURQLw+yv0QDCx8=
|
||||
golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.0.0-20221002022538-bcab6841153b h1:6e93nYa3hNqAvLr0pD4PN1fFS+gKzp2zAXqrnTCstqU=
|
||||
golang.org/x/net v0.0.0-20221002022538-bcab6841153b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
|
||||
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=
|
||||
@@ -1490,8 +1491,9 @@ golang.org/x/sys v0.0.0-20211102192858-4dd72447c267/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20211105183446-c75c47738b0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8 h1:0A+M6Uqn+Eje4kHMK80dtF3JCXC4ykBgQG4Fe06QRhQ=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 h1:WIoqL4EROvwiPdUtaip4VcDdpZ4kha7wBWZrbVKCIZg=
|
||||
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/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 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
|
||||
|
||||
@@ -1 +1 @@
|
||||
b13188dd36c1ad2509796ce10b6a1231b200c36a
|
||||
3fd24dee31726924c1b61c8037a889b30b8aa0f6
|
||||
|
||||
@@ -325,7 +325,7 @@ func OverallError() error {
|
||||
return overallErrorLocked()
|
||||
}
|
||||
|
||||
var fakeErrForTesting = envknob.String("TS_DEBUG_FAKE_HEALTH_ERROR")
|
||||
var fakeErrForTesting = envknob.RegisterString("TS_DEBUG_FAKE_HEALTH_ERROR")
|
||||
|
||||
func overallErrorLocked() error {
|
||||
if !anyInterfaceUp {
|
||||
@@ -383,7 +383,10 @@ func overallErrorLocked() error {
|
||||
for _, s := range controlHealth {
|
||||
errs = append(errs, errors.New(s))
|
||||
}
|
||||
if e := fakeErrForTesting; len(errs) == 0 && e != "" {
|
||||
if err := envknob.ApplyDiskConfigError(); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
if e := fakeErrForTesting(); len(errs) == 0 && e != "" {
|
||||
return errors.New(e)
|
||||
}
|
||||
sort.Slice(errs, func(i, j int) bool {
|
||||
|
||||
@@ -9,7 +9,6 @@ package hostinfo
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
@@ -99,11 +98,11 @@ func linuxVersionMeta() (meta versionMeta) {
|
||||
case distro.OpenWrt:
|
||||
propFile = "/etc/openwrt_release"
|
||||
case distro.WDMyCloud:
|
||||
slurp, _ := ioutil.ReadFile("/etc/version")
|
||||
slurp, _ := os.ReadFile("/etc/version")
|
||||
meta.DistroVersion = string(bytes.TrimSpace(slurp))
|
||||
return
|
||||
case distro.QNAP:
|
||||
slurp, _ := ioutil.ReadFile("/etc/version_info")
|
||||
slurp, _ := os.ReadFile("/etc/version_info")
|
||||
meta.DistroVersion = getQnapQtsVersion(string(slurp))
|
||||
return
|
||||
}
|
||||
@@ -133,7 +132,7 @@ func linuxVersionMeta() (meta versionMeta) {
|
||||
case "debian":
|
||||
// Debian's VERSION_ID is just like "11". But /etc/debian_version has "11.5" normally.
|
||||
// Or "bookworm/sid" on sid/testing.
|
||||
slurp, _ := ioutil.ReadFile("/etc/debian_version")
|
||||
slurp, _ := os.ReadFile("/etc/debian_version")
|
||||
if v := string(bytes.TrimSpace(slurp)); v != "" {
|
||||
if '0' <= v[0] && v[0] <= '9' {
|
||||
meta.DistroVersion = v
|
||||
@@ -143,7 +142,7 @@ func linuxVersionMeta() (meta versionMeta) {
|
||||
}
|
||||
case "", "centos": // CentOS 6 has no /etc/os-release, so its id is ""
|
||||
if meta.DistroVersion == "" {
|
||||
if cr, _ := ioutil.ReadFile("/etc/centos-release"); len(cr) > 0 { // "CentOS release 6.10 (Final)
|
||||
if cr, _ := os.ReadFile("/etc/centos-release"); len(cr) > 0 { // "CentOS release 6.10 (Final)
|
||||
meta.DistroVersion = string(bytes.TrimSpace(cr))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@ type Notify struct {
|
||||
State *State // if non-nil, the new or current IPN state
|
||||
Prefs *Prefs // if non-nil, the new or current preferences
|
||||
NetMap *netmap.NetworkMap // if non-nil, the new or current netmap
|
||||
Engine *EngineStatus // if non-nil, the new or urrent wireguard stats
|
||||
Engine *EngineStatus // if non-nil, the new or current wireguard stats
|
||||
BrowseToURL *string // if non-nil, UI should open a browser right now
|
||||
BackendLogID *string // if non-nil, the public logtail ID used by backend
|
||||
|
||||
@@ -168,6 +168,11 @@ type PartialFile struct {
|
||||
// LocalBackend.userID, a string like "user-$USER_ID" (used in
|
||||
// server mode).
|
||||
// - on Linux/etc, it's always "_daemon" (ipn.GlobalDaemonStateKey)
|
||||
//
|
||||
// Additionally, the StateKey can be debug setting name:
|
||||
//
|
||||
// - "_debug_magicsock_until" with value being a unix timestamp stringified
|
||||
// - "_debug_<component>_until" with value being a unix timestamp stringified
|
||||
type StateKey string
|
||||
|
||||
type Options struct {
|
||||
|
||||
@@ -5,16 +5,64 @@
|
||||
package ipnlocal
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/util/clientmetric"
|
||||
"tailscale.com/util/goroutines"
|
||||
)
|
||||
|
||||
func (b *LocalBackend) handleC2N(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON := func(v any) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(v)
|
||||
}
|
||||
switch r.URL.Path {
|
||||
case "/echo":
|
||||
// Test handler.
|
||||
body, _ := io.ReadAll(r.Body)
|
||||
w.Write(body)
|
||||
case "/debug/goroutines":
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
w.Write(goroutines.ScrubbedGoroutineDump())
|
||||
case "/debug/prefs":
|
||||
writeJSON(b.Prefs())
|
||||
case "/debug/metrics":
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
clientmetric.WritePrometheusExpositionFormat(w)
|
||||
case "/debug/component-logging":
|
||||
component := r.FormValue("component")
|
||||
secs, _ := strconv.Atoi(r.FormValue("secs"))
|
||||
if secs == 0 {
|
||||
secs -= 1
|
||||
}
|
||||
until := time.Now().Add(time.Duration(secs) * time.Second)
|
||||
err := b.SetComponentDebugLogging(component, until)
|
||||
var res struct {
|
||||
Error string `json:",omitempty"`
|
||||
}
|
||||
if err != nil {
|
||||
res.Error = err.Error()
|
||||
}
|
||||
writeJSON(res)
|
||||
case "/ssh/usernames":
|
||||
var req tailcfg.C2NSSHUsernamesRequest
|
||||
if r.Method == "POST" {
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
}
|
||||
res, err := b.getSSHUsernames(&req)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
return
|
||||
}
|
||||
writeJSON(res)
|
||||
default:
|
||||
http.Error(w, "unknown c2n path", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
@@ -27,6 +27,8 @@ import (
|
||||
"golang.org/x/exp/slices"
|
||||
"tailscale.com/client/tailscale/apitype"
|
||||
"tailscale.com/control/controlclient"
|
||||
"tailscale.com/doctor"
|
||||
"tailscale.com/doctor/routetable"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/health"
|
||||
"tailscale.com/hostinfo"
|
||||
@@ -54,6 +56,7 @@ import (
|
||||
"tailscale.com/types/views"
|
||||
"tailscale.com/util/deephash"
|
||||
"tailscale.com/util/dnsname"
|
||||
"tailscale.com/util/mak"
|
||||
"tailscale.com/util/multierr"
|
||||
"tailscale.com/util/osshare"
|
||||
"tailscale.com/util/systemd"
|
||||
@@ -68,7 +71,6 @@ import (
|
||||
)
|
||||
|
||||
var controlDebugFlags = getControlDebugFlags()
|
||||
var canSSH = envknob.CanSSHD()
|
||||
|
||||
func getControlDebugFlags() []string {
|
||||
if e := envknob.String("TS_DEBUG_CONTROL_FLAGS"); e != "" {
|
||||
@@ -186,11 +188,16 @@ type LocalBackend struct {
|
||||
// *.partial file to its final name on completion.
|
||||
directFileRoot string
|
||||
directFileDoFinalRename bool // false on macOS, true on several NAS platforms
|
||||
componentLogUntil map[string]componentLogState
|
||||
|
||||
// statusLock must be held before calling statusChanged.Wait() or
|
||||
// statusChanged.Broadcast().
|
||||
statusLock sync.Mutex
|
||||
statusChanged *sync.Cond
|
||||
|
||||
// dialPlan is any dial plan that we've received from the control
|
||||
// server during a previous connection; it is cleared on logout.
|
||||
dialPlan atomic.Pointer[tailcfg.ControlDialPlan]
|
||||
}
|
||||
|
||||
// clientGen is a func that creates a control plane client.
|
||||
@@ -210,7 +217,7 @@ func NewLocalBackend(logf logger.Logf, logid string, store ipn.StateStore, diale
|
||||
logf.JSON(1, "Hostinfo", hi)
|
||||
envknob.LogCurrent(logf)
|
||||
if dialer == nil {
|
||||
dialer = new(tsdial.Dialer)
|
||||
dialer = &tsdial.Dialer{Logf: logf}
|
||||
}
|
||||
|
||||
osshare.SetFileSharingEnabled(false, logf)
|
||||
@@ -263,9 +270,91 @@ func NewLocalBackend(logf logger.Logf, logid string, store ipn.StateStore, diale
|
||||
b.logf("[unexpected] failed to wire up peer API port for engine %T", e)
|
||||
}
|
||||
|
||||
for _, component := range debuggableComponents {
|
||||
key := componentStateKey(component)
|
||||
if ut, err := ipn.ReadStoreInt(store, key); err == nil {
|
||||
if until := time.Unix(ut, 0); until.After(time.Now()) {
|
||||
// conditional to avoid log spam at start when off
|
||||
b.SetComponentDebugLogging(component, until)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return b, nil
|
||||
}
|
||||
|
||||
type componentLogState struct {
|
||||
until time.Time
|
||||
timer *time.Timer // if non-nil, the AfterFunc to disable it
|
||||
}
|
||||
|
||||
var debuggableComponents = []string{
|
||||
"magicsock",
|
||||
}
|
||||
|
||||
func componentStateKey(component string) ipn.StateKey {
|
||||
return ipn.StateKey("_debug_" + component + "_until")
|
||||
}
|
||||
|
||||
// SetComponentDebugLogging sets component's debug logging enabled until the until time.
|
||||
// If until is in the past, the component's debug logging is disabled.
|
||||
//
|
||||
// The following components are recognized:
|
||||
//
|
||||
// - magicsock
|
||||
func (b *LocalBackend) SetComponentDebugLogging(component string, until time.Time) error {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
|
||||
var setEnabled func(bool)
|
||||
switch component {
|
||||
case "magicsock":
|
||||
mc, err := b.magicConn()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
setEnabled = mc.SetDebugLoggingEnabled
|
||||
}
|
||||
if setEnabled == nil || !slices.Contains(debuggableComponents, component) {
|
||||
return fmt.Errorf("unknown component %q", component)
|
||||
}
|
||||
timeUnixOrZero := func(t time.Time) int64 {
|
||||
if t.IsZero() {
|
||||
return 0
|
||||
}
|
||||
return t.Unix()
|
||||
}
|
||||
ipn.PutStoreInt(b.store, componentStateKey(component), timeUnixOrZero(until))
|
||||
now := time.Now()
|
||||
on := now.Before(until)
|
||||
setEnabled(on)
|
||||
var onFor time.Duration
|
||||
if on {
|
||||
onFor = until.Sub(now)
|
||||
b.logf("debugging logging for component %q enabled for %v (until %v)", component, onFor.Round(time.Second), until.UTC().Format(time.RFC3339))
|
||||
} else {
|
||||
b.logf("debugging logging for component %q disabled", component)
|
||||
}
|
||||
if oldSt, ok := b.componentLogUntil[component]; ok && oldSt.timer != nil {
|
||||
oldSt.timer.Stop()
|
||||
}
|
||||
newSt := componentLogState{until: until}
|
||||
if on {
|
||||
newSt.timer = time.AfterFunc(onFor, func() {
|
||||
// Turn off logging after the timer fires, as long as the state is
|
||||
// unchanged when the timer actually fires.
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
if ls := b.componentLogUntil[component]; ls.until == until {
|
||||
setEnabled(false)
|
||||
b.logf("debugging logging for component %q disabled (by timer)", component)
|
||||
}
|
||||
})
|
||||
}
|
||||
mak.Set(&b.componentLogUntil, component, newSt)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Dialer returns the backend's dialer.
|
||||
func (b *LocalBackend) Dialer() *tsdial.Dialer {
|
||||
return b.dialer
|
||||
@@ -575,6 +664,10 @@ func (b *LocalBackend) WhoIs(ipp netip.AddrPort) (n *tailcfg.Node, u tailcfg.Use
|
||||
func (b *LocalBackend) PeerCaps(src netip.Addr) []string {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
return b.peerCapsLocked(src)
|
||||
}
|
||||
|
||||
func (b *LocalBackend) peerCapsLocked(src netip.Addr) []string {
|
||||
if b.netMap == nil {
|
||||
return nil
|
||||
}
|
||||
@@ -586,9 +679,9 @@ func (b *LocalBackend) PeerCaps(src netip.Addr) []string {
|
||||
if !a.IsSingleIP() {
|
||||
continue
|
||||
}
|
||||
dstIP := a.Addr()
|
||||
if dstIP.BitLen() == src.BitLen() {
|
||||
return filt.AppendCaps(nil, src, a.Addr())
|
||||
dst := a.Addr()
|
||||
if dst.BitLen() == src.BitLen() { // match on family
|
||||
return filt.AppendCaps(nil, src, dst)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
@@ -682,6 +775,12 @@ func (b *LocalBackend) setClientStatus(st controlclient.Status) {
|
||||
}
|
||||
}
|
||||
if st.NetMap != nil {
|
||||
if err := b.tkaSyncIfNeededLocked(st.NetMap); err != nil {
|
||||
b.logf("[v1] TKA sync error: %v", err)
|
||||
}
|
||||
if !envknob.TKASkipSignatureCheck() {
|
||||
b.tkaFilterNetmapLocked(st.NetMap)
|
||||
}
|
||||
if b.findExitNodeIDLocked(st.NetMap) {
|
||||
prefsChanged = true
|
||||
}
|
||||
@@ -1082,6 +1181,7 @@ func (b *LocalBackend) Start(opts ipn.Options) error {
|
||||
Dialer: b.Dialer(),
|
||||
Status: b.setClientStatus,
|
||||
C2NHandler: http.HandlerFunc(b.handleC2N),
|
||||
DialPlan: &b.dialPlan, // pointer because it can't be copied
|
||||
|
||||
// Don't warn about broken Linux IP forwarding when
|
||||
// netstack is being used.
|
||||
@@ -1510,12 +1610,12 @@ func (b *LocalBackend) tellClientToBrowseToURL(url string) {
|
||||
}
|
||||
|
||||
// For testing lazy machine key generation.
|
||||
var panicOnMachineKeyGeneration = envknob.Bool("TS_DEBUG_PANIC_MACHINE_KEY")
|
||||
var panicOnMachineKeyGeneration = envknob.RegisterBool("TS_DEBUG_PANIC_MACHINE_KEY")
|
||||
|
||||
func (b *LocalBackend) createGetMachinePrivateKeyFunc() func() (key.MachinePrivate, error) {
|
||||
var cache syncs.AtomicValue[key.MachinePrivate]
|
||||
return func() (key.MachinePrivate, error) {
|
||||
if panicOnMachineKeyGeneration {
|
||||
if panicOnMachineKeyGeneration() {
|
||||
panic("machine key generated")
|
||||
}
|
||||
if v, ok := cache.LoadOk(); ok {
|
||||
@@ -1752,7 +1852,7 @@ func (b *LocalBackend) loadStateLocked(key ipn.StateKey, prefs *ipn.Prefs) (err
|
||||
// setAtomicValuesFromPrefs populates sshAtomicBool and containsViaIPFuncAtomic
|
||||
// from the prefs p, which may be nil.
|
||||
func (b *LocalBackend) setAtomicValuesFromPrefs(p *ipn.Prefs) {
|
||||
b.sshAtomicBool.Store(p != nil && p.RunSSH && canSSH)
|
||||
b.sshAtomicBool.Store(p != nil && p.RunSSH && envknob.CanSSHD())
|
||||
|
||||
if p == nil {
|
||||
b.containsViaIPFuncAtomic.Store(tsaddr.NewContainsIPFunc(nil))
|
||||
@@ -1967,7 +2067,7 @@ func (b *LocalBackend) checkSSHPrefsLocked(p *ipn.Prefs) error {
|
||||
default:
|
||||
return errors.New("The Tailscale SSH server is not supported on " + runtime.GOOS)
|
||||
}
|
||||
if !canSSH {
|
||||
if !envknob.CanSSHD() {
|
||||
return errors.New("The Tailscale SSH server has been administratively disabled.")
|
||||
}
|
||||
if envknob.SSHIgnoreTailnetPolicy() || envknob.SSHPolicyFile() != "" {
|
||||
@@ -2032,7 +2132,7 @@ func (b *LocalBackend) EditPrefs(mp *ipn.MaskedPrefs) (*ipn.Prefs, error) {
|
||||
b.logf("EditPrefs check error: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
if p1.RunSSH && !canSSH {
|
||||
if p1.RunSSH && !envknob.CanSSHD() {
|
||||
b.mu.Unlock()
|
||||
b.logf("EditPrefs requests SSH, but disabled by envknob; returning error")
|
||||
return nil, errors.New("Tailscale SSH server administratively disabled.")
|
||||
@@ -2161,7 +2261,7 @@ func (b *LocalBackend) GetPeerAPIPort(ip netip.Addr) (port uint16, ok bool) {
|
||||
// ServePeerAPIConnection serves an already-accepted connection c.
|
||||
//
|
||||
// The remote parameter is the remote address.
|
||||
// The local paramater is the local address (either a Tailscale IPv4
|
||||
// The local parameter is the local address (either a Tailscale IPv4
|
||||
// or IPv6 IP and the peerapi port for that address).
|
||||
//
|
||||
// The connection will be closed by ServePeerAPIConnection.
|
||||
@@ -2854,7 +2954,7 @@ func (b *LocalBackend) applyPrefsToHostinfo(hi *tailcfg.Hostinfo, prefs *ipn.Pre
|
||||
hi.ShieldsUp = prefs.ShieldsUp
|
||||
|
||||
var sshHostKeys []string
|
||||
if prefs.RunSSH && canSSH {
|
||||
if prefs.RunSSH && envknob.CanSSHD() {
|
||||
// TODO(bradfitz): this is called with b.mu held. Not ideal.
|
||||
// If the filesystem gets wedged or something we could block for
|
||||
// a long time. But probably fine.
|
||||
@@ -3015,7 +3115,7 @@ func (b *LocalBackend) RequestEngineStatus() {
|
||||
// that have happened. It is invoked from the various callbacks that
|
||||
// feed events into LocalBackend.
|
||||
//
|
||||
// TODO(apenwarr): use a channel or something to prevent re-entrancy?
|
||||
// TODO(apenwarr): use a channel or something to prevent reentrancy?
|
||||
// Or maybe just call the state machine from fewer places.
|
||||
func (b *LocalBackend) stateMachine() {
|
||||
b.enterState(b.nextState())
|
||||
@@ -3073,9 +3173,9 @@ func (b *LocalBackend) ResetForClientDisconnect() {
|
||||
b.setAtomicValuesFromPrefs(nil)
|
||||
}
|
||||
|
||||
func (b *LocalBackend) ShouldRunSSH() bool { return b.sshAtomicBool.Load() && canSSH }
|
||||
func (b *LocalBackend) ShouldRunSSH() bool { return b.sshAtomicBool.Load() && envknob.CanSSHD() }
|
||||
|
||||
// ShouldHandleViaIP reports whether whether ip is an IPv6 address in the
|
||||
// ShouldHandleViaIP reports whether ip is an IPv6 address in the
|
||||
// Tailscale ULA's v6 "via" range embedding an IPv4 address to be forwarded to
|
||||
// by Tailscale.
|
||||
func (b *LocalBackend) ShouldHandleViaIP(ip netip.Addr) bool {
|
||||
@@ -3107,6 +3207,9 @@ func (b *LocalBackend) logout(ctx context.Context, sync bool) error {
|
||||
Prefs: ipn.Prefs{WantRunning: false, LoggedOut: true},
|
||||
})
|
||||
|
||||
// Clear any previous dial plan(s), if set.
|
||||
b.dialPlan.Store(nil)
|
||||
|
||||
if cc == nil {
|
||||
// Double Logout can happen via repeated IPN
|
||||
// connections to ipnserver making it repeatedly
|
||||
@@ -3223,6 +3326,17 @@ func (b *LocalBackend) setNetMapLocked(nm *netmap.NetworkMap) {
|
||||
}
|
||||
}
|
||||
|
||||
// operatorUserName returns the current pref's OperatorUser's name, or the
|
||||
// empty string if none.
|
||||
func (b *LocalBackend) operatorUserName() string {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
if b.prefs == nil {
|
||||
return ""
|
||||
}
|
||||
return b.prefs.OperatorUser
|
||||
}
|
||||
|
||||
// OperatorUserID returns the current pref's OperatorUser's ID (in
|
||||
// os/user.User.Uid string form), or the empty string if none.
|
||||
func (b *LocalBackend) OperatorUserID() string {
|
||||
@@ -3305,13 +3419,12 @@ func (b *LocalBackend) FileTargets() ([]*apitype.FileTarget, error) {
|
||||
return nil, errors.New("file sharing not enabled by Tailscale admin")
|
||||
}
|
||||
for _, p := range nm.Peers {
|
||||
if p.User != nm.User && !slices.Contains(p.Capabilities, tailcfg.CapabilityFileSharingTarget) {
|
||||
if !b.peerIsTaildropTargetLocked(p) {
|
||||
continue
|
||||
}
|
||||
peerAPI := peerAPIBase(b.netMap, p)
|
||||
if peerAPI == "" {
|
||||
continue
|
||||
|
||||
}
|
||||
ret = append(ret, &apitype.FileTarget{
|
||||
Node: p,
|
||||
@@ -3322,6 +3435,35 @@ func (b *LocalBackend) FileTargets() ([]*apitype.FileTarget, error) {
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
// peerIsTaildropTargetLocked reports whether p is a valid Taildrop file
|
||||
// recipient from this node according to its ownership and the capabilities in
|
||||
// the netmap.
|
||||
//
|
||||
// b.mu must be locked.
|
||||
func (b *LocalBackend) peerIsTaildropTargetLocked(p *tailcfg.Node) bool {
|
||||
if b.netMap == nil || p == nil {
|
||||
return false
|
||||
}
|
||||
if b.netMap.User == p.User {
|
||||
return true
|
||||
}
|
||||
if len(p.Addresses) > 0 &&
|
||||
b.peerHasCapLocked(p.Addresses[0].Addr(), tailcfg.CapabilityFileSharingTarget) {
|
||||
// Explicitly noted in the netmap ACL caps as a target.
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (b *LocalBackend) peerHasCapLocked(addr netip.Addr, wantCap string) bool {
|
||||
for _, hasCap := range b.peerCapsLocked(addr) {
|
||||
if hasCap == wantCap {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// SetDNS adds a DNS record for the given domain name & TXT record
|
||||
// value.
|
||||
//
|
||||
@@ -3568,7 +3710,7 @@ func (b *LocalBackend) magicConn() (*magicsock.Conn, error) {
|
||||
return mc, nil
|
||||
}
|
||||
|
||||
// DoNoiseRequest sends a request to URL over the the control plane
|
||||
// DoNoiseRequest sends a request to URL over the control plane
|
||||
// Noise connection.
|
||||
func (b *LocalBackend) DoNoiseRequest(req *http.Request) (*http.Response, error) {
|
||||
b.mu.Lock()
|
||||
@@ -3580,6 +3722,17 @@ func (b *LocalBackend) DoNoiseRequest(req *http.Request) (*http.Response, error)
|
||||
return cc.DoNoiseRequest(req)
|
||||
}
|
||||
|
||||
// tailscaleSSHEnabled reports whether Tailscale SSH is currently enabled based
|
||||
// on prefs. It returns false if there are no prefs set.
|
||||
func (b *LocalBackend) tailscaleSSHEnabled() bool {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
if b.prefs == nil {
|
||||
return false
|
||||
}
|
||||
return b.prefs.RunSSH
|
||||
}
|
||||
|
||||
func (b *LocalBackend) sshServerOrInit() (_ SSHServer, err error) {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
@@ -3638,3 +3791,19 @@ func (b *LocalBackend) handleQuad100Port80Conn(w http.ResponseWriter, r *http.Re
|
||||
}
|
||||
io.WriteString(w, "</ul>\n")
|
||||
}
|
||||
|
||||
func (b *LocalBackend) Doctor(ctx context.Context, logf logger.Logf) {
|
||||
var checks []doctor.Check
|
||||
|
||||
checks = append(checks, routetable.Check{})
|
||||
|
||||
// TODO(andrew): more
|
||||
|
||||
numChecks := len(checks)
|
||||
checks = append(checks, doctor.CheckFunc("numchecks", func(_ context.Context, log logger.Logf) error {
|
||||
log("%d checks", numChecks)
|
||||
return nil
|
||||
}))
|
||||
|
||||
doctor.RunChecks(ctx, logf, checks...)
|
||||
}
|
||||
|
||||
@@ -478,8 +478,8 @@ func (panicOnUseTransport) RoundTrip(*http.Request) (*http.Response, error) {
|
||||
|
||||
// Issue 1573: don't generate a machine key if we don't want to be running.
|
||||
func TestLazyMachineKeyGeneration(t *testing.T) {
|
||||
defer func(old bool) { panicOnMachineKeyGeneration = old }(panicOnMachineKeyGeneration)
|
||||
panicOnMachineKeyGeneration = true
|
||||
defer func(old func() bool) { panicOnMachineKeyGeneration = old }(panicOnMachineKeyGeneration)
|
||||
panicOnMachineKeyGeneration = func() bool { return true }
|
||||
|
||||
var logf logger.Logf = logger.Discard
|
||||
store := new(mem.Store)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Copyright (c) 2022 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.
|
||||
|
||||
@@ -12,11 +12,12 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/logtail/backoff"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tka"
|
||||
"tailscale.com/types/key"
|
||||
@@ -24,33 +25,278 @@ import (
|
||||
"tailscale.com/types/tkatype"
|
||||
)
|
||||
|
||||
var networkLockAvailable = envknob.Bool("TS_EXPERIMENTAL_NETWORK_LOCK")
|
||||
// TODO(tom): RPC retry/backoff was broken and has been removed. Fix?
|
||||
|
||||
var (
|
||||
errMissingNetmap = errors.New("missing netmap: verify that you are logged in")
|
||||
errNetworkLockNotActive = errors.New("network-lock is not active")
|
||||
)
|
||||
|
||||
type tkaState struct {
|
||||
authority *tka.Authority
|
||||
storage *tka.FS
|
||||
}
|
||||
|
||||
// CanSupportNetworkLock returns true if tailscaled is able to operate
|
||||
// a local tailnet key authority (and hence enforce network lock).
|
||||
func (b *LocalBackend) CanSupportNetworkLock() bool {
|
||||
if b.tka != nil {
|
||||
// The TKA is being used, so yeah its supported.
|
||||
return true
|
||||
// tkaFilterNetmapLocked checks the signatures on each node key, dropping
|
||||
// nodes from the netmap who's signature does not verify.
|
||||
func (b *LocalBackend) tkaFilterNetmapLocked(nm *netmap.NetworkMap) {
|
||||
if !envknob.UseWIPCode() {
|
||||
return // Feature-flag till network-lock is in Alpha.
|
||||
}
|
||||
if b.tka == nil {
|
||||
return // TKA not enabled.
|
||||
}
|
||||
|
||||
if b.TailscaleVarRoot() != "" {
|
||||
// Theres a var root (aka --statedir), so if network lock gets
|
||||
// initialized we have somewhere to store our AUMs. Thats all
|
||||
// we need.
|
||||
return true
|
||||
toDelete := make(map[int]struct{}, len(nm.Peers))
|
||||
for i, p := range nm.Peers {
|
||||
if len(p.KeySignature) == 0 {
|
||||
b.logf("Network lock is dropping peer %v(%v) due to missing signature", p.ID, p.StableID)
|
||||
toDelete[i] = struct{}{}
|
||||
} else {
|
||||
if err := b.tka.authority.NodeKeyAuthorized(p.Key, p.KeySignature); err != nil {
|
||||
b.logf("Network lock is dropping peer %v(%v) due to failed signature check: %v", p.ID, p.StableID, err)
|
||||
toDelete[i] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
|
||||
// nm.Peers is ordered, so deletion must be order-preserving.
|
||||
peers := make([]*tailcfg.Node, 0, len(nm.Peers))
|
||||
for i, p := range nm.Peers {
|
||||
if _, delete := toDelete[i]; !delete {
|
||||
peers = append(peers, p)
|
||||
}
|
||||
}
|
||||
nm.Peers = peers
|
||||
}
|
||||
|
||||
// tkaSyncIfNeededLocked examines TKA info reported from the control plane,
|
||||
// performing the steps necessary to synchronize local tka state.
|
||||
//
|
||||
// There are 4 scenarios handled here:
|
||||
// - Enablement: nm.TKAEnabled but b.tka == nil
|
||||
// ∴ reach out to /machine/tka/bootstrap to get the genesis AUM, then
|
||||
// initialize TKA.
|
||||
// - Disablement: !nm.TKAEnabled but b.tka != nil
|
||||
// ∴ reach out to /machine/tka/bootstrap to read the disablement secret,
|
||||
// then verify and clear tka local state.
|
||||
// - Sync needed: b.tka.Head != nm.TKAHead
|
||||
// ∴ complete multi-step synchronization flow.
|
||||
// - Everything up to date: All other cases.
|
||||
// ∴ no action necessary.
|
||||
//
|
||||
// b.mu must be held. b.mu will be stepped out of (and back in) during network
|
||||
// RPCs.
|
||||
func (b *LocalBackend) tkaSyncIfNeededLocked(nm *netmap.NetworkMap) error {
|
||||
if !envknob.UseWIPCode() {
|
||||
// If the feature flag is not enabled, pretend we don't exist.
|
||||
return nil
|
||||
}
|
||||
ourNodeKey := b.prefs.Persist.PrivateNodeKey.Public()
|
||||
|
||||
isEnabled := b.tka != nil
|
||||
wantEnabled := nm.TKAEnabled
|
||||
if isEnabled != wantEnabled {
|
||||
var ourHead tka.AUMHash
|
||||
if b.tka != nil {
|
||||
ourHead = b.tka.authority.Head()
|
||||
}
|
||||
|
||||
// Regardless of whether we are moving to disabled or enabled, we
|
||||
// need information from the tka bootstrap endpoint.
|
||||
b.mu.Unlock()
|
||||
bs, err := b.tkaFetchBootstrap(ourNodeKey, ourHead)
|
||||
b.mu.Lock()
|
||||
if err != nil {
|
||||
return fmt.Errorf("fetching bootstrap: %w", err)
|
||||
}
|
||||
|
||||
if wantEnabled && !isEnabled {
|
||||
if err := b.tkaBootstrapFromGenesisLocked(bs.GenesisAUM); err != nil {
|
||||
return fmt.Errorf("bootstrap: %w", err)
|
||||
}
|
||||
isEnabled = true
|
||||
} else if !wantEnabled && isEnabled {
|
||||
if b.tka.authority.ValidDisablement(bs.DisablementSecret) {
|
||||
b.tka = nil
|
||||
isEnabled = false
|
||||
|
||||
if err := os.RemoveAll(b.chonkPath()); err != nil {
|
||||
return fmt.Errorf("os.RemoveAll: %v", err)
|
||||
}
|
||||
} else {
|
||||
b.logf("Disablement secret did not verify, leaving TKA enabled.")
|
||||
}
|
||||
} else {
|
||||
return fmt.Errorf("[bug] unreachable invariant of wantEnabled /w isEnabled")
|
||||
}
|
||||
}
|
||||
|
||||
if isEnabled && b.tka.authority.Head() != nm.TKAHead {
|
||||
if err := b.tkaSyncLocked(ourNodeKey); err != nil {
|
||||
return fmt.Errorf("tka sync: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func toSyncOffer(head string, ancestors []string) (tka.SyncOffer, error) {
|
||||
var out tka.SyncOffer
|
||||
if err := out.Head.UnmarshalText([]byte(head)); err != nil {
|
||||
return tka.SyncOffer{}, fmt.Errorf("head.UnmarshalText: %v", err)
|
||||
}
|
||||
out.Ancestors = make([]tka.AUMHash, len(ancestors))
|
||||
for i, a := range ancestors {
|
||||
if err := out.Ancestors[i].UnmarshalText([]byte(a)); err != nil {
|
||||
return tka.SyncOffer{}, fmt.Errorf("ancestor[%d].UnmarshalText: %v", i, err)
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// tkaSyncLocked synchronizes TKA state with control. b.mu must be held
|
||||
// and tka must be initialized. b.mu will be stepped out of (and back into)
|
||||
// during network RPCs.
|
||||
func (b *LocalBackend) tkaSyncLocked(ourNodeKey key.NodePublic) error {
|
||||
offer, err := b.tka.authority.SyncOffer(b.tka.storage)
|
||||
if err != nil {
|
||||
return fmt.Errorf("offer: %w", err)
|
||||
}
|
||||
|
||||
b.mu.Unlock()
|
||||
offerResp, err := b.tkaDoSyncOffer(ourNodeKey, offer)
|
||||
b.mu.Lock()
|
||||
if err != nil {
|
||||
return fmt.Errorf("offer RPC: %w", err)
|
||||
}
|
||||
controlOffer, err := toSyncOffer(offerResp.Head, offerResp.Ancestors)
|
||||
if err != nil {
|
||||
return fmt.Errorf("control offer: %v", err)
|
||||
}
|
||||
|
||||
if controlOffer.Head == offer.Head {
|
||||
// We are up to date.
|
||||
return nil
|
||||
}
|
||||
|
||||
// Compute missing AUMs before we apply any AUMs from the control-plane,
|
||||
// so we still submit AUMs to control even if they are not part of the
|
||||
// active chain.
|
||||
toSendAUMs, err := b.tka.authority.MissingAUMs(b.tka.storage, controlOffer)
|
||||
if err != nil {
|
||||
return fmt.Errorf("computing missing AUMs: %w", err)
|
||||
}
|
||||
|
||||
// If we got this far, then we are not up to date. Either the control-plane
|
||||
// has updates for us, or we have updates for the control plane.
|
||||
//
|
||||
// TODO(tom): Do we want to keep processing even if the Inform fails? Need
|
||||
// to think through if theres holdback concerns here or not.
|
||||
if len(offerResp.MissingAUMs) > 0 {
|
||||
aums := make([]tka.AUM, len(offerResp.MissingAUMs))
|
||||
for i, a := range offerResp.MissingAUMs {
|
||||
if err := aums[i].Unserialize(a); err != nil {
|
||||
return fmt.Errorf("MissingAUMs[%d]: %v", i, err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := b.tka.authority.Inform(b.tka.storage, aums); err != nil {
|
||||
return fmt.Errorf("inform failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE(tom): We could short-circuit here if our HEAD equals the
|
||||
// control-plane's head, but we don't just so control always has a
|
||||
// copy of all forks that clients had.
|
||||
|
||||
b.mu.Unlock()
|
||||
sendResp, err := b.tkaDoSyncSend(ourNodeKey, toSendAUMs, false)
|
||||
b.mu.Lock()
|
||||
if err != nil {
|
||||
return fmt.Errorf("send RPC: %v", err)
|
||||
}
|
||||
|
||||
var remoteHead tka.AUMHash
|
||||
if err := remoteHead.UnmarshalText([]byte(sendResp.Head)); err != nil {
|
||||
return fmt.Errorf("head unmarshal: %v", err)
|
||||
}
|
||||
if remoteHead != b.tka.authority.Head() {
|
||||
b.logf("TKA desync: expected consensus after sync but our head is %v and the control plane's is %v", b.tka.authority.Head(), remoteHead)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// chonkPath returns the absolute path to the directory in which TKA
|
||||
// state (the 'tailchonk') is stored.
|
||||
func (b *LocalBackend) chonkPath() string {
|
||||
return filepath.Join(b.TailscaleVarRoot(), "tka")
|
||||
}
|
||||
|
||||
// tkaBootstrapFromGenesisLocked initializes the local (on-disk) state of the
|
||||
// tailnet key authority, based on the given genesis AUM.
|
||||
//
|
||||
// b.mu must be held.
|
||||
func (b *LocalBackend) tkaBootstrapFromGenesisLocked(g tkatype.MarshaledAUM) error {
|
||||
if err := b.CanSupportNetworkLock(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var genesis tka.AUM
|
||||
if err := genesis.Unserialize(g); err != nil {
|
||||
return fmt.Errorf("reading genesis: %v", err)
|
||||
}
|
||||
|
||||
chonkDir := b.chonkPath()
|
||||
if err := os.Mkdir(chonkDir, 0755); err != nil && !os.IsExist(err) {
|
||||
return fmt.Errorf("mkdir: %v", err)
|
||||
}
|
||||
|
||||
chonk, err := tka.ChonkDir(chonkDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("chonk: %v", err)
|
||||
}
|
||||
authority, err := tka.Bootstrap(chonk, genesis)
|
||||
if err != nil {
|
||||
return fmt.Errorf("tka bootstrap: %v", err)
|
||||
}
|
||||
|
||||
b.tka = &tkaState{
|
||||
authority: authority,
|
||||
storage: chonk,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CanSupportNetworkLock returns nil if tailscaled is able to operate
|
||||
// a local tailnet key authority (and hence enforce network lock).
|
||||
func (b *LocalBackend) CanSupportNetworkLock() error {
|
||||
if !envknob.UseWIPCode() {
|
||||
return errors.New("this feature is not yet complete, a later release may support this functionality")
|
||||
}
|
||||
|
||||
if b.tka != nil {
|
||||
// If the TKA is being used, it is supported.
|
||||
return nil
|
||||
}
|
||||
|
||||
if b.TailscaleVarRoot() == "" {
|
||||
return errors.New("network-lock is not supported in this configuration, try setting --statedir")
|
||||
}
|
||||
|
||||
// There's a var root (aka --statedir), so if network lock gets
|
||||
// initialized we have somewhere to store our AUMs. That's all
|
||||
// we need.
|
||||
return nil
|
||||
}
|
||||
|
||||
// NetworkLockStatus returns a structure describing the state of the
|
||||
// tailnet key authority, if any.
|
||||
func (b *LocalBackend) NetworkLockStatus() *ipnstate.NetworkLockStatus {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
|
||||
if b.tka == nil {
|
||||
return &ipnstate.NetworkLockStatus{
|
||||
Enabled: false,
|
||||
@@ -79,18 +325,18 @@ func (b *LocalBackend) NetworkLockStatus() *ipnstate.NetworkLockStatus {
|
||||
// The Finish RPC submits signatures for all these nodes, at which point
|
||||
// Control has everything it needs to atomically enable network lock.
|
||||
func (b *LocalBackend) NetworkLockInit(keys []tka.Key) error {
|
||||
if b.tka != nil {
|
||||
return errors.New("network-lock is already initialized")
|
||||
if err := b.CanSupportNetworkLock(); err != nil {
|
||||
return err
|
||||
}
|
||||
if !networkLockAvailable {
|
||||
return errors.New("this is an experimental feature in your version of tailscale - Please upgrade to the latest to use this.")
|
||||
|
||||
var ourNodeKey key.NodePublic
|
||||
b.mu.Lock()
|
||||
if b.prefs != nil {
|
||||
ourNodeKey = b.prefs.Persist.PrivateNodeKey.Public()
|
||||
}
|
||||
if !b.CanSupportNetworkLock() {
|
||||
return errors.New("network-lock is not supported in this configuration. Did you supply a --statedir?")
|
||||
}
|
||||
nm := b.NetMap()
|
||||
if nm == nil {
|
||||
return errors.New("no netmap: are you logged into tailscale?")
|
||||
b.mu.Unlock()
|
||||
if ourNodeKey.IsZero() {
|
||||
return errors.New("no node-key: is tailscale logged in?")
|
||||
}
|
||||
|
||||
// Generates a genesis AUM representing trust in the provided keys.
|
||||
@@ -112,7 +358,7 @@ func (b *LocalBackend) NetworkLockInit(keys []tka.Key) error {
|
||||
}
|
||||
|
||||
// Phase 1/2 of initialization: Transmit the genesis AUM to Control.
|
||||
initResp, err := b.tkaInitBegin(nm, genesisAUM)
|
||||
initResp, err := b.tkaInitBegin(ourNodeKey, genesisAUM)
|
||||
if err != nil {
|
||||
return fmt.Errorf("tka init-begin RPC: %w", err)
|
||||
}
|
||||
@@ -133,10 +379,91 @@ func (b *LocalBackend) NetworkLockInit(keys []tka.Key) error {
|
||||
}
|
||||
|
||||
// Finalize enablement by transmitting signature for all nodes to Control.
|
||||
_, err = b.tkaInitFinish(nm, sigs)
|
||||
_, err = b.tkaInitFinish(ourNodeKey, sigs)
|
||||
return err
|
||||
}
|
||||
|
||||
// Only use is in tests.
|
||||
func (b *LocalBackend) NetworkLockVerifySignatureForTest(nks tkatype.MarshaledSignature, nodeKey key.NodePublic) error {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
if b.tka == nil {
|
||||
return errNetworkLockNotActive
|
||||
}
|
||||
return b.tka.authority.NodeKeyAuthorized(nodeKey, nks)
|
||||
}
|
||||
|
||||
// Only use is in tests.
|
||||
func (b *LocalBackend) NetworkLockKeyTrustedForTest(keyID tkatype.KeyID) bool {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
if b.tka == nil {
|
||||
panic("network lock not initialized")
|
||||
}
|
||||
return b.tka.authority.KeyTrusted(keyID)
|
||||
}
|
||||
|
||||
// NetworkLockModify adds and/or removes keys in the tailnet's key authority.
|
||||
func (b *LocalBackend) NetworkLockModify(addKeys, removeKeys []tka.Key) (err error) {
|
||||
defer func() {
|
||||
if err != nil {
|
||||
err = fmt.Errorf("modify network-lock keys: %w", err)
|
||||
}
|
||||
}()
|
||||
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
|
||||
if err := b.CanSupportNetworkLock(); err != nil {
|
||||
return err
|
||||
}
|
||||
if b.tka == nil {
|
||||
return errNetworkLockNotActive
|
||||
}
|
||||
|
||||
updater := b.tka.authority.NewUpdater(b.nlPrivKey)
|
||||
|
||||
for _, addKey := range addKeys {
|
||||
if err := updater.AddKey(addKey); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, removeKey := range removeKeys {
|
||||
if err := updater.RemoveKey(removeKey.ID()); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
aums, err := updater.Finalize(b.tka.storage)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(aums) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
ourNodeKey := b.prefs.Persist.PrivateNodeKey.Public()
|
||||
b.mu.Unlock()
|
||||
resp, err := b.tkaDoSyncSend(ourNodeKey, aums, true)
|
||||
b.mu.Lock()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var controlHead tka.AUMHash
|
||||
if err := controlHead.UnmarshalText([]byte(resp.Head)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
lastHead := aums[len(aums)-1].Hash()
|
||||
if controlHead != lastHead {
|
||||
return errors.New("central tka head differs from submitted AUM, try again")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func signNodeKey(nodeInfo tailcfg.TKASignInfo, signer key.NLPrivate) (*tka.NodeKeySignature, error) {
|
||||
p, err := nodeInfo.NodePublic.MarshalBinary()
|
||||
if err != nil {
|
||||
@@ -156,10 +483,11 @@ func signNodeKey(nodeInfo tailcfg.TKASignInfo, signer key.NLPrivate) (*tka.NodeK
|
||||
return &sig, nil
|
||||
}
|
||||
|
||||
func (b *LocalBackend) tkaInitBegin(nm *netmap.NetworkMap, aum tka.AUM) (*tailcfg.TKAInitBeginResponse, error) {
|
||||
func (b *LocalBackend) tkaInitBegin(ourNodeKey key.NodePublic, aum tka.AUM) (*tailcfg.TKAInitBeginResponse, error) {
|
||||
var req bytes.Buffer
|
||||
if err := json.NewEncoder(&req).Encode(tailcfg.TKAInitBeginRequest{
|
||||
NodeID: nm.SelfNode.ID,
|
||||
Version: tailcfg.CurrentCapabilityVersion,
|
||||
NodeKey: ourNodeKey,
|
||||
GenesisAUM: aum.Serialize(),
|
||||
}); err != nil {
|
||||
return nil, fmt.Errorf("encoding request: %v", err)
|
||||
@@ -167,40 +495,34 @@ func (b *LocalBackend) tkaInitBegin(nm *netmap.NetworkMap, aum tka.AUM) (*tailcf
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
|
||||
defer cancel()
|
||||
bo := backoff.NewBackoff("tka-init-begin", b.logf, 5*time.Second)
|
||||
for {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, fmt.Errorf("ctx: %w", err)
|
||||
}
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", "https://unused/machine/tka/init/begin", &req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("req: %w", err)
|
||||
}
|
||||
res, err := b.DoNoiseRequest(req)
|
||||
if err != nil {
|
||||
bo.BackOff(ctx, err)
|
||||
continue
|
||||
}
|
||||
if res.StatusCode != 200 {
|
||||
body, _ := io.ReadAll(res.Body)
|
||||
res.Body.Close()
|
||||
return nil, fmt.Errorf("request returned (%d): %s", res.StatusCode, string(body))
|
||||
}
|
||||
a := new(tailcfg.TKAInitBeginResponse)
|
||||
err = json.NewDecoder(res.Body).Decode(a)
|
||||
res.Body.Close()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("decoding JSON: %w", err)
|
||||
}
|
||||
|
||||
return a, nil
|
||||
req2, err := http.NewRequestWithContext(ctx, "GET", "https://unused/machine/tka/init/begin", &req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("req: %w", err)
|
||||
}
|
||||
res, err := b.DoNoiseRequest(req2)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("resp: %w", err)
|
||||
}
|
||||
if res.StatusCode != 200 {
|
||||
body, _ := io.ReadAll(res.Body)
|
||||
res.Body.Close()
|
||||
return nil, fmt.Errorf("request returned (%d): %s", res.StatusCode, string(body))
|
||||
}
|
||||
a := new(tailcfg.TKAInitBeginResponse)
|
||||
err = json.NewDecoder(res.Body).Decode(a)
|
||||
res.Body.Close()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("decoding JSON: %w", err)
|
||||
}
|
||||
|
||||
return a, nil
|
||||
}
|
||||
|
||||
func (b *LocalBackend) tkaInitFinish(nm *netmap.NetworkMap, nks map[tailcfg.NodeID]tkatype.MarshaledSignature) (*tailcfg.TKAInitFinishResponse, error) {
|
||||
func (b *LocalBackend) tkaInitFinish(ourNodeKey key.NodePublic, nks map[tailcfg.NodeID]tkatype.MarshaledSignature) (*tailcfg.TKAInitFinishResponse, error) {
|
||||
var req bytes.Buffer
|
||||
if err := json.NewEncoder(&req).Encode(tailcfg.TKAInitFinishRequest{
|
||||
NodeID: nm.SelfNode.ID,
|
||||
Version: tailcfg.CurrentCapabilityVersion,
|
||||
NodeKey: ourNodeKey,
|
||||
Signatures: nks,
|
||||
}); err != nil {
|
||||
return nil, fmt.Errorf("encoding request: %v", err)
|
||||
@@ -208,32 +530,178 @@ func (b *LocalBackend) tkaInitFinish(nm *netmap.NetworkMap, nks map[tailcfg.Node
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
|
||||
defer cancel()
|
||||
bo := backoff.NewBackoff("tka-init-finish", b.logf, 5*time.Second)
|
||||
for {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, fmt.Errorf("ctx: %w", err)
|
||||
}
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", "https://unused/machine/tka/init/finish", &req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("req: %w", err)
|
||||
}
|
||||
res, err := b.DoNoiseRequest(req)
|
||||
if err != nil {
|
||||
bo.BackOff(ctx, err)
|
||||
continue
|
||||
}
|
||||
if res.StatusCode != 200 {
|
||||
body, _ := io.ReadAll(res.Body)
|
||||
res.Body.Close()
|
||||
return nil, fmt.Errorf("request returned (%d): %s", res.StatusCode, string(body))
|
||||
}
|
||||
a := new(tailcfg.TKAInitFinishResponse)
|
||||
err = json.NewDecoder(res.Body).Decode(a)
|
||||
res.Body.Close()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("decoding JSON: %w", err)
|
||||
}
|
||||
|
||||
return a, nil
|
||||
req2, err := http.NewRequestWithContext(ctx, "GET", "https://unused/machine/tka/init/finish", &req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("req: %w", err)
|
||||
}
|
||||
res, err := b.DoNoiseRequest(req2)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("resp: %w", err)
|
||||
}
|
||||
if res.StatusCode != 200 {
|
||||
body, _ := io.ReadAll(res.Body)
|
||||
res.Body.Close()
|
||||
return nil, fmt.Errorf("request returned (%d): %s", res.StatusCode, string(body))
|
||||
}
|
||||
a := new(tailcfg.TKAInitFinishResponse)
|
||||
err = json.NewDecoder(res.Body).Decode(a)
|
||||
res.Body.Close()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("decoding JSON: %w", err)
|
||||
}
|
||||
|
||||
return a, nil
|
||||
}
|
||||
|
||||
// tkaFetchBootstrap sends a /machine/tka/bootstrap RPC to the control plane
|
||||
// over noise. This is used to get values necessary to enable or disable TKA.
|
||||
func (b *LocalBackend) tkaFetchBootstrap(ourNodeKey key.NodePublic, head tka.AUMHash) (*tailcfg.TKABootstrapResponse, error) {
|
||||
bootstrapReq := tailcfg.TKABootstrapRequest{
|
||||
Version: tailcfg.CurrentCapabilityVersion,
|
||||
NodeKey: ourNodeKey,
|
||||
}
|
||||
if !head.IsZero() {
|
||||
head, err := head.MarshalText()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("head.MarshalText failed: %v", err)
|
||||
}
|
||||
bootstrapReq.Head = string(head)
|
||||
}
|
||||
|
||||
var req bytes.Buffer
|
||||
if err := json.NewEncoder(&req).Encode(bootstrapReq); err != nil {
|
||||
return nil, fmt.Errorf("encoding request: %v", err)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
|
||||
defer cancel()
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, fmt.Errorf("ctx: %w", err)
|
||||
}
|
||||
req2, err := http.NewRequestWithContext(ctx, "GET", "https://unused/machine/tka/bootstrap", &req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("req: %w", err)
|
||||
}
|
||||
res, err := b.DoNoiseRequest(req2)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("resp: %w", err)
|
||||
}
|
||||
if res.StatusCode != 200 {
|
||||
body, _ := io.ReadAll(res.Body)
|
||||
res.Body.Close()
|
||||
return nil, fmt.Errorf("request returned (%d): %s", res.StatusCode, string(body))
|
||||
}
|
||||
a := new(tailcfg.TKABootstrapResponse)
|
||||
err = json.NewDecoder(res.Body).Decode(a)
|
||||
res.Body.Close()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("decoding JSON: %w", err)
|
||||
}
|
||||
|
||||
return a, nil
|
||||
}
|
||||
|
||||
func fromSyncOffer(offer tka.SyncOffer) (head string, ancestors []string, err error) {
|
||||
headBytes, err := offer.Head.MarshalText()
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("head.MarshalText: %v", err)
|
||||
}
|
||||
|
||||
ancestors = make([]string, len(offer.Ancestors))
|
||||
for i, ancestor := range offer.Ancestors {
|
||||
hash, err := ancestor.MarshalText()
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("ancestor[%d].MarshalText: %v", i, err)
|
||||
}
|
||||
ancestors[i] = string(hash)
|
||||
}
|
||||
return string(headBytes), ancestors, nil
|
||||
}
|
||||
|
||||
// tkaDoSyncOffer sends a /machine/tka/sync/offer RPC to the control plane
|
||||
// over noise. This is the first of two RPCs implementing tka synchronization.
|
||||
func (b *LocalBackend) tkaDoSyncOffer(ourNodeKey key.NodePublic, offer tka.SyncOffer) (*tailcfg.TKASyncOfferResponse, error) {
|
||||
head, ancestors, err := fromSyncOffer(offer)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("encoding offer: %v", err)
|
||||
}
|
||||
syncReq := tailcfg.TKASyncOfferRequest{
|
||||
Version: tailcfg.CurrentCapabilityVersion,
|
||||
NodeKey: ourNodeKey,
|
||||
Head: head,
|
||||
Ancestors: ancestors,
|
||||
}
|
||||
|
||||
var req bytes.Buffer
|
||||
if err := json.NewEncoder(&req).Encode(syncReq); err != nil {
|
||||
return nil, fmt.Errorf("encoding request: %v", err)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
|
||||
defer cancel()
|
||||
req2, err := http.NewRequestWithContext(ctx, "GET", "https://unused/machine/tka/sync/offer", &req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("req: %w", err)
|
||||
}
|
||||
res, err := b.DoNoiseRequest(req2)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("resp: %w", err)
|
||||
}
|
||||
if res.StatusCode != 200 {
|
||||
body, _ := io.ReadAll(res.Body)
|
||||
res.Body.Close()
|
||||
return nil, fmt.Errorf("request returned (%d): %s", res.StatusCode, string(body))
|
||||
}
|
||||
a := new(tailcfg.TKASyncOfferResponse)
|
||||
err = json.NewDecoder(res.Body).Decode(a)
|
||||
res.Body.Close()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("decoding JSON: %w", err)
|
||||
}
|
||||
|
||||
return a, nil
|
||||
}
|
||||
|
||||
// tkaDoSyncSend sends a /machine/tka/sync/send RPC to the control plane
|
||||
// over noise. This is the second of two RPCs implementing tka synchronization.
|
||||
func (b *LocalBackend) tkaDoSyncSend(ourNodeKey key.NodePublic, aums []tka.AUM, interactive bool) (*tailcfg.TKASyncSendResponse, error) {
|
||||
sendReq := tailcfg.TKASyncSendRequest{
|
||||
Version: tailcfg.CurrentCapabilityVersion,
|
||||
NodeKey: ourNodeKey,
|
||||
MissingAUMs: make([]tkatype.MarshaledAUM, len(aums)),
|
||||
Interactive: interactive,
|
||||
}
|
||||
for i, a := range aums {
|
||||
sendReq.MissingAUMs[i] = a.Serialize()
|
||||
}
|
||||
|
||||
var req bytes.Buffer
|
||||
if err := json.NewEncoder(&req).Encode(sendReq); err != nil {
|
||||
return nil, fmt.Errorf("encoding request: %v", err)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
|
||||
defer cancel()
|
||||
req2, err := http.NewRequestWithContext(ctx, "GET", "https://unused/machine/tka/sync/send", &req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("req: %w", err)
|
||||
}
|
||||
res, err := b.DoNoiseRequest(req2)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("resp: %w", err)
|
||||
}
|
||||
if res.StatusCode != 200 {
|
||||
body, _ := io.ReadAll(res.Body)
|
||||
res.Body.Close()
|
||||
return nil, fmt.Errorf("request returned (%d): %s", res.StatusCode, string(body))
|
||||
}
|
||||
a := new(tailcfg.TKASyncSendResponse)
|
||||
err = json.NewDecoder(res.Body).Decode(a)
|
||||
res.Body.Close()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("decoding JSON: %w", err)
|
||||
}
|
||||
|
||||
return a, nil
|
||||
}
|
||||
|
||||
546
ipn/ipnlocal/network-lock_test.go
Normal file
546
ipn/ipnlocal/network-lock_test.go
Normal file
@@ -0,0 +1,546 @@
|
||||
// Copyright (c) 2022 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 (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"tailscale.com/control/controlclient"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/hostinfo"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tka"
|
||||
"tailscale.com/types/key"
|
||||
"tailscale.com/types/netmap"
|
||||
"tailscale.com/types/persist"
|
||||
"tailscale.com/types/tkatype"
|
||||
)
|
||||
|
||||
func fakeControlClient(t *testing.T, c *http.Client) *controlclient.Auto {
|
||||
hi := hostinfo.New()
|
||||
ni := tailcfg.NetInfo{LinkType: "wired"}
|
||||
hi.NetInfo = &ni
|
||||
|
||||
k := key.NewMachine()
|
||||
opts := controlclient.Options{
|
||||
ServerURL: "https://example.com",
|
||||
Hostinfo: hi,
|
||||
GetMachinePrivateKey: func() (key.MachinePrivate, error) {
|
||||
return k, nil
|
||||
},
|
||||
HTTPTestClient: c,
|
||||
NoiseTestClient: c,
|
||||
Status: func(controlclient.Status) {},
|
||||
}
|
||||
|
||||
cc, err := controlclient.NewNoStart(opts)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return cc
|
||||
}
|
||||
|
||||
func fakeNoiseServer(t *testing.T, handler http.HandlerFunc) (*httptest.Server, *http.Client) {
|
||||
ts := httptest.NewUnstartedServer(handler)
|
||||
ts.StartTLS()
|
||||
client := ts.Client()
|
||||
client.Transport.(*http.Transport).TLSClientConfig.InsecureSkipVerify = true
|
||||
client.Transport.(*http.Transport).DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
|
||||
return (&net.Dialer{}).DialContext(ctx, network, ts.Listener.Addr().String())
|
||||
}
|
||||
return ts, client
|
||||
}
|
||||
|
||||
func TestTKAEnablementFlow(t *testing.T) {
|
||||
envknob.Setenv("TAILSCALE_USE_WIP_CODE", "1")
|
||||
nodePriv := key.NewNode()
|
||||
|
||||
// Make a fake TKA authority, getting a usable genesis AUM which
|
||||
// our mock server can communicate.
|
||||
nlPriv := key.NewNLPrivate()
|
||||
key := tka.Key{Kind: tka.Key25519, Public: nlPriv.Public().Verifier(), Votes: 2}
|
||||
a1, genesisAUM, err := tka.Create(&tka.Mem{}, tka.State{
|
||||
Keys: []tka.Key{key},
|
||||
DisablementSecrets: [][]byte{bytes.Repeat([]byte{0xa5}, 32)},
|
||||
}, nlPriv)
|
||||
if err != nil {
|
||||
t.Fatalf("tka.Create() failed: %v", err)
|
||||
}
|
||||
|
||||
ts, client := fakeNoiseServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
defer r.Body.Close()
|
||||
switch r.URL.Path {
|
||||
case "/machine/tka/bootstrap":
|
||||
body := new(tailcfg.TKABootstrapRequest)
|
||||
if err := json.NewDecoder(r.Body).Decode(body); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if body.Version != tailcfg.CurrentCapabilityVersion {
|
||||
t.Errorf("bootstrap CapVer = %v, want %v", body.Version, tailcfg.CurrentCapabilityVersion)
|
||||
}
|
||||
if body.NodeKey != nodePriv.Public() {
|
||||
t.Errorf("bootstrap nodeKey=%v, want %v", body.NodeKey, nodePriv.Public())
|
||||
}
|
||||
if body.Head != "" {
|
||||
t.Errorf("bootstrap head=%s, want empty hash", body.Head)
|
||||
}
|
||||
|
||||
w.WriteHeader(200)
|
||||
out := tailcfg.TKABootstrapResponse{
|
||||
GenesisAUM: genesisAUM.Serialize(),
|
||||
}
|
||||
if err := json.NewEncoder(w).Encode(out); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
case "/machine/tka/sync/offer", "/machine/tka/sync/send":
|
||||
t.Error("node attempted to sync, but should have been up to date")
|
||||
|
||||
default:
|
||||
t.Errorf("unhandled endpoint path: %v", r.URL.Path)
|
||||
w.WriteHeader(404)
|
||||
}
|
||||
}))
|
||||
defer ts.Close()
|
||||
temp := t.TempDir()
|
||||
|
||||
cc := fakeControlClient(t, client)
|
||||
b := LocalBackend{
|
||||
varRoot: temp,
|
||||
cc: cc,
|
||||
ccAuto: cc,
|
||||
logf: t.Logf,
|
||||
prefs: &ipn.Prefs{
|
||||
Persist: &persist.Persist{PrivateNodeKey: nodePriv},
|
||||
},
|
||||
}
|
||||
|
||||
b.mu.Lock()
|
||||
err = b.tkaSyncIfNeededLocked(&netmap.NetworkMap{
|
||||
TKAEnabled: true,
|
||||
TKAHead: a1.Head(),
|
||||
})
|
||||
b.mu.Unlock()
|
||||
if err != nil {
|
||||
t.Errorf("tkaSyncIfNeededLocked() failed: %v", err)
|
||||
}
|
||||
if b.tka == nil {
|
||||
t.Fatal("tka was not initialized")
|
||||
}
|
||||
if b.tka.authority.Head() != a1.Head() {
|
||||
t.Errorf("authority.Head() = %x, want %x", b.tka.authority.Head(), a1.Head())
|
||||
}
|
||||
}
|
||||
|
||||
func TestTKADisablementFlow(t *testing.T) {
|
||||
envknob.Setenv("TAILSCALE_USE_WIP_CODE", "1")
|
||||
temp := t.TempDir()
|
||||
os.Mkdir(filepath.Join(temp, "tka"), 0755)
|
||||
nodePriv := key.NewNode()
|
||||
|
||||
// Make a fake TKA authority, to seed local state.
|
||||
disablementSecret := bytes.Repeat([]byte{0xa5}, 32)
|
||||
nlPriv := key.NewNLPrivate()
|
||||
key := tka.Key{Kind: tka.Key25519, Public: nlPriv.Public().Verifier(), Votes: 2}
|
||||
chonk, err := tka.ChonkDir(filepath.Join(temp, "tka"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
authority, _, err := tka.Create(chonk, tka.State{
|
||||
Keys: []tka.Key{key},
|
||||
DisablementSecrets: [][]byte{tka.DisablementKDF(disablementSecret)},
|
||||
}, nlPriv)
|
||||
if err != nil {
|
||||
t.Fatalf("tka.Create() failed: %v", err)
|
||||
}
|
||||
|
||||
returnWrongSecret := false
|
||||
ts, client := fakeNoiseServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
defer r.Body.Close()
|
||||
switch r.URL.Path {
|
||||
case "/machine/tka/bootstrap":
|
||||
body := new(tailcfg.TKABootstrapRequest)
|
||||
if err := json.NewDecoder(r.Body).Decode(body); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if body.Version != tailcfg.CurrentCapabilityVersion {
|
||||
t.Errorf("bootstrap CapVer = %v, want %v", body.Version, tailcfg.CurrentCapabilityVersion)
|
||||
}
|
||||
if body.NodeKey != nodePriv.Public() {
|
||||
t.Errorf("nodeKey=%v, want %v", body.NodeKey, nodePriv.Public())
|
||||
}
|
||||
var head tka.AUMHash
|
||||
if err := head.UnmarshalText([]byte(body.Head)); err != nil {
|
||||
t.Fatalf("failed unmarshal of body.Head: %v", err)
|
||||
}
|
||||
if head != authority.Head() {
|
||||
t.Errorf("reported head = %x, want %x", head, authority.Head())
|
||||
}
|
||||
|
||||
var disablement []byte
|
||||
if returnWrongSecret {
|
||||
disablement = bytes.Repeat([]byte{0x42}, 32) // wrong secret
|
||||
} else {
|
||||
disablement = disablementSecret
|
||||
}
|
||||
|
||||
w.WriteHeader(200)
|
||||
out := tailcfg.TKABootstrapResponse{
|
||||
DisablementSecret: disablement,
|
||||
}
|
||||
if err := json.NewEncoder(w).Encode(out); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
default:
|
||||
t.Errorf("unhandled endpoint path: %v", r.URL.Path)
|
||||
w.WriteHeader(404)
|
||||
}
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
cc := fakeControlClient(t, client)
|
||||
b := LocalBackend{
|
||||
varRoot: temp,
|
||||
cc: cc,
|
||||
ccAuto: cc,
|
||||
logf: t.Logf,
|
||||
tka: &tkaState{
|
||||
authority: authority,
|
||||
storage: chonk,
|
||||
},
|
||||
prefs: &ipn.Prefs{
|
||||
Persist: &persist.Persist{PrivateNodeKey: nodePriv},
|
||||
},
|
||||
}
|
||||
|
||||
// Test that the wrong disablement secret does not shut down the authority.
|
||||
returnWrongSecret = true
|
||||
b.mu.Lock()
|
||||
err = b.tkaSyncIfNeededLocked(&netmap.NetworkMap{
|
||||
TKAEnabled: false,
|
||||
TKAHead: authority.Head(),
|
||||
})
|
||||
b.mu.Unlock()
|
||||
if err != nil {
|
||||
t.Errorf("tkaSyncIfNeededLocked() failed: %v", err)
|
||||
}
|
||||
if b.tka == nil {
|
||||
t.Error("TKA was disabled despite incorrect disablement secret")
|
||||
}
|
||||
|
||||
// Test the correct disablement secret shuts down the authority.
|
||||
returnWrongSecret = false
|
||||
b.mu.Lock()
|
||||
err = b.tkaSyncIfNeededLocked(&netmap.NetworkMap{
|
||||
TKAEnabled: false,
|
||||
TKAHead: authority.Head(),
|
||||
})
|
||||
b.mu.Unlock()
|
||||
if err != nil {
|
||||
t.Errorf("tkaSyncIfNeededLocked() failed: %v", err)
|
||||
}
|
||||
|
||||
if b.tka != nil {
|
||||
t.Fatal("tka was not shut down")
|
||||
}
|
||||
if _, err := os.Stat(b.chonkPath()); err == nil || !os.IsNotExist(err) {
|
||||
t.Errorf("os.Stat(chonkDir) = %v, want ErrNotExist", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTKASync(t *testing.T) {
|
||||
envknob.Setenv("TAILSCALE_USE_WIP_CODE", "1")
|
||||
|
||||
someKeyPriv := key.NewNLPrivate()
|
||||
someKey := tka.Key{Kind: tka.Key25519, Public: someKeyPriv.Public().Verifier(), Votes: 1}
|
||||
|
||||
type tkaSyncScenario struct {
|
||||
name string
|
||||
// controlAUMs is called (if non-nil) to get any AUMs which the tka state
|
||||
// on control should be seeded with.
|
||||
controlAUMs func(*testing.T, *tka.Authority, tka.Chonk, tka.Signer) []tka.AUM
|
||||
// controlAUMs is called (if non-nil) to get any AUMs which the tka state
|
||||
// on the node should be seeded with.
|
||||
nodeAUMs func(*testing.T, *tka.Authority, tka.Chonk, tka.Signer) []tka.AUM
|
||||
}
|
||||
|
||||
tcs := []tkaSyncScenario{
|
||||
{name: "up to date"},
|
||||
{
|
||||
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 {
|
||||
t.Fatal(err)
|
||||
}
|
||||
aums, err := b.Finalize(storage)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return aums
|
||||
},
|
||||
},
|
||||
{
|
||||
// AKA 'control data loss' scenario
|
||||
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 {
|
||||
t.Fatal(err)
|
||||
}
|
||||
aums, err := b.Finalize(storage)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return aums
|
||||
},
|
||||
},
|
||||
{
|
||||
// AKA 'control data loss + update in the meantime' scenario
|
||||
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 {
|
||||
t.Fatal(err)
|
||||
}
|
||||
aums, err := b.Finalize(storage)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return aums
|
||||
},
|
||||
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 {
|
||||
t.Fatal(err)
|
||||
}
|
||||
aums, err := b.Finalize(storage)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return aums
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tcs {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
temp := t.TempDir()
|
||||
os.Mkdir(filepath.Join(temp, "tka"), 0755)
|
||||
nodePriv := key.NewNode()
|
||||
nlPriv := key.NewNLPrivate()
|
||||
|
||||
// Setup the tka authority on the control plane.
|
||||
key := tka.Key{Kind: tka.Key25519, Public: nlPriv.Public().Verifier(), Votes: 2}
|
||||
controlStorage := &tka.Mem{}
|
||||
controlAuthority, bootstrap, err := tka.Create(controlStorage, tka.State{
|
||||
Keys: []tka.Key{key, someKey},
|
||||
DisablementSecrets: [][]byte{bytes.Repeat([]byte{0xa5}, 32)},
|
||||
}, nlPriv)
|
||||
if err != nil {
|
||||
t.Fatalf("tka.Create() failed: %v", err)
|
||||
}
|
||||
if tc.controlAUMs != nil {
|
||||
if err := controlAuthority.Inform(controlStorage, tc.controlAUMs(t, controlAuthority, controlStorage, nlPriv)); err != nil {
|
||||
t.Fatalf("controlAuthority.Inform() failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Setup the TKA authority on the node.
|
||||
nodeStorage, err := tka.ChonkDir(filepath.Join(temp, "tka"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
nodeAuthority, err := tka.Bootstrap(nodeStorage, bootstrap)
|
||||
if err != nil {
|
||||
t.Fatalf("tka.Bootstrap() failed: %v", err)
|
||||
}
|
||||
if tc.nodeAUMs != nil {
|
||||
if err := nodeAuthority.Inform(nodeStorage, tc.nodeAUMs(t, nodeAuthority, nodeStorage, nlPriv)); err != nil {
|
||||
t.Fatalf("nodeAuthority.Inform() failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Make a mock control server.
|
||||
ts, client := fakeNoiseServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
defer r.Body.Close()
|
||||
switch r.URL.Path {
|
||||
case "/machine/tka/sync/offer":
|
||||
body := new(tailcfg.TKASyncOfferRequest)
|
||||
if err := json.NewDecoder(r.Body).Decode(body); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Logf("got sync offer:\n%+v", body)
|
||||
nodeOffer, err := toSyncOffer(body.Head, body.Ancestors)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
controlOffer, err := controlAuthority.SyncOffer(controlStorage)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
sendAUMs, err := controlAuthority.MissingAUMs(controlStorage, nodeOffer)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
head, ancestors, err := fromSyncOffer(controlOffer)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
resp := tailcfg.TKASyncOfferResponse{
|
||||
Head: head,
|
||||
Ancestors: ancestors,
|
||||
MissingAUMs: make([]tkatype.MarshaledAUM, len(sendAUMs)),
|
||||
}
|
||||
for i, a := range sendAUMs {
|
||||
resp.MissingAUMs[i] = a.Serialize()
|
||||
}
|
||||
|
||||
t.Logf("responding to sync offer with:\n%+v", resp)
|
||||
w.WriteHeader(200)
|
||||
if err := json.NewEncoder(w).Encode(resp); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
case "/machine/tka/sync/send":
|
||||
body := new(tailcfg.TKASyncSendRequest)
|
||||
if err := json.NewDecoder(r.Body).Decode(body); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Logf("got sync send:\n%+v", body)
|
||||
toApply := make([]tka.AUM, len(body.MissingAUMs))
|
||||
for i, a := range body.MissingAUMs {
|
||||
if err := toApply[i].Unserialize(a); err != nil {
|
||||
t.Fatalf("decoding missingAUM[%d]: %v", i, err)
|
||||
}
|
||||
}
|
||||
|
||||
if len(toApply) > 0 {
|
||||
if err := controlAuthority.Inform(controlStorage, toApply); err != nil {
|
||||
t.Fatalf("control.Inform(%+v) failed: %v", toApply, err)
|
||||
}
|
||||
}
|
||||
head, err := controlAuthority.Head().MarshalText()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
w.WriteHeader(200)
|
||||
if err := json.NewEncoder(w).Encode(tailcfg.TKASyncSendResponse{Head: string(head)}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
default:
|
||||
t.Errorf("unhandled endpoint path: %v", r.URL.Path)
|
||||
w.WriteHeader(404)
|
||||
}
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
// Setup the client.
|
||||
cc := fakeControlClient(t, client)
|
||||
b := LocalBackend{
|
||||
varRoot: temp,
|
||||
cc: cc,
|
||||
ccAuto: cc,
|
||||
logf: t.Logf,
|
||||
tka: &tkaState{
|
||||
authority: nodeAuthority,
|
||||
storage: nodeStorage,
|
||||
},
|
||||
prefs: &ipn.Prefs{
|
||||
Persist: &persist.Persist{PrivateNodeKey: nodePriv},
|
||||
},
|
||||
}
|
||||
|
||||
// Finally, lets trigger a sync.
|
||||
b.mu.Lock()
|
||||
err = b.tkaSyncIfNeededLocked(&netmap.NetworkMap{
|
||||
TKAEnabled: true,
|
||||
TKAHead: controlAuthority.Head(),
|
||||
})
|
||||
b.mu.Unlock()
|
||||
if err != nil {
|
||||
t.Errorf("tkaSyncIfNeededLocked() failed: %v", err)
|
||||
}
|
||||
|
||||
// Check that at the end of this ordeal, the node and the control
|
||||
// plane are in sync.
|
||||
if nodeHead, controlHead := b.tka.authority.Head(), controlAuthority.Head(); nodeHead != controlHead {
|
||||
t.Errorf("node head = %v, want %v", nodeHead, controlHead)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTKAFilterNetmap(t *testing.T) {
|
||||
envknob.Setenv("TAILSCALE_USE_WIP_CODE", "1")
|
||||
|
||||
nlPriv := key.NewNLPrivate()
|
||||
nlKey := tka.Key{Kind: tka.Key25519, Public: nlPriv.Public().Verifier(), Votes: 2}
|
||||
storage := &tka.Mem{}
|
||||
authority, _, err := tka.Create(storage, tka.State{
|
||||
Keys: []tka.Key{nlKey},
|
||||
DisablementSecrets: [][]byte{bytes.Repeat([]byte{0xa5}, 32)},
|
||||
}, nlPriv)
|
||||
if err != nil {
|
||||
t.Fatalf("tka.Create() failed: %v", err)
|
||||
}
|
||||
|
||||
n1, n2, n3, n4, n5 := key.NewNode(), key.NewNode(), key.NewNode(), key.NewNode(), key.NewNode()
|
||||
n1GoodSig, err := signNodeKey(tailcfg.TKASignInfo{NodePublic: n1.Public()}, nlPriv)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
n4Sig, err := signNodeKey(tailcfg.TKASignInfo{NodePublic: n4.Public()}, nlPriv)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
n4Sig.Signature[3] = 42 // mess up the signature
|
||||
n4Sig.Signature[4] = 42 // mess up the signature
|
||||
n5GoodSig, err := signNodeKey(tailcfg.TKASignInfo{NodePublic: n5.Public()}, nlPriv)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
nm := netmap.NetworkMap{
|
||||
Peers: []*tailcfg.Node{
|
||||
{ID: 1, Key: n1.Public(), KeySignature: n1GoodSig.Serialize()},
|
||||
{ID: 2, Key: n2.Public(), KeySignature: nil}, // missing sig
|
||||
{ID: 3, Key: n3.Public(), KeySignature: n1GoodSig.Serialize()}, // someone elses sig
|
||||
{ID: 4, Key: n4.Public(), KeySignature: n4Sig.Serialize()}, // messed-up signature
|
||||
{ID: 5, Key: n5.Public(), KeySignature: n5GoodSig.Serialize()},
|
||||
},
|
||||
}
|
||||
|
||||
b := &LocalBackend{
|
||||
logf: t.Logf,
|
||||
tka: &tkaState{authority: authority},
|
||||
}
|
||||
b.tkaFilterNetmapLocked(&nm)
|
||||
|
||||
want := []*tailcfg.Node{
|
||||
{ID: 1, Key: n1.Public(), KeySignature: n1GoodSig.Serialize()},
|
||||
{ID: 5, Key: n5.Public(), KeySignature: n5GoodSig.Serialize()},
|
||||
}
|
||||
nodePubComparer := cmp.Comparer(func(x, y key.NodePublic) bool {
|
||||
return x.Raw32() == y.Raw32()
|
||||
})
|
||||
if diff := cmp.Diff(nm.Peers, want, nodePubComparer); diff != "" {
|
||||
t.Errorf("filtered netmap differs (-want, +got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
@@ -44,6 +44,7 @@ import (
|
||||
"tailscale.com/net/netutil"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/util/clientmetric"
|
||||
"tailscale.com/util/strs"
|
||||
"tailscale.com/wgengine"
|
||||
"tailscale.com/wgengine/filter"
|
||||
)
|
||||
@@ -78,7 +79,7 @@ type peerAPIServer struct {
|
||||
}
|
||||
|
||||
const (
|
||||
// partialSuffix is the suffix appened to files while they're
|
||||
// partialSuffix is the suffix appended to files while they're
|
||||
// still in the process of being transferred.
|
||||
partialSuffix = ".partial"
|
||||
|
||||
@@ -720,8 +721,8 @@ func (h *peerAPIHandler) handlePeerPut(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
rawPath := r.URL.EscapedPath()
|
||||
suffix := strings.TrimPrefix(rawPath, "/v0/put/")
|
||||
if suffix == rawPath {
|
||||
suffix, ok := strs.CutPrefix(rawPath, "/v0/put/")
|
||||
if !ok {
|
||||
http.Error(w, "misconfigured internals", 500)
|
||||
return
|
||||
}
|
||||
@@ -1183,7 +1184,7 @@ func newFakePeerAPIListener(ip netip.Addr) net.Listener {
|
||||
// even if the kernel isn't cooperating (like on Android: Issue 4449, 4293, etc)
|
||||
// or we lack permission to listen on a port. It's okay to not actually listen via
|
||||
// the kernel because on almost all platforms (except iOS as of 2022-04-20) we
|
||||
// also intercept netstack TCP requests in to our peerapi port and hand it over
|
||||
// also intercept incoming netstack TCP requests to our peerapi port and hand them over
|
||||
// directly to peerapi, without involving the kernel. So this doesn't need to be
|
||||
// real. But the port number we return (1, in this case) is the port number we advertise
|
||||
// to peers and they connect to. 1 seems pretty safe to use. Even if the kernel's
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"io/ioutil"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
@@ -87,7 +86,7 @@ func fileHasContents(name string, want string) check {
|
||||
return
|
||||
}
|
||||
path := filepath.Join(root, name)
|
||||
got, err := ioutil.ReadFile(path)
|
||||
got, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Errorf("fileHasContents: %v", err)
|
||||
return
|
||||
@@ -110,7 +109,7 @@ func TestHandlePeerAPI(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
isSelf bool // the peer sending the request is owned by us
|
||||
capSharing bool // self node has file sharing capabilty
|
||||
capSharing bool // self node has file sharing capability
|
||||
omitRoot bool // don't configure
|
||||
req *http.Request
|
||||
checks []check
|
||||
@@ -517,7 +516,7 @@ func TestDeletedMarkers(t *testing.T) {
|
||||
}
|
||||
wantEmptyTempDir := func() {
|
||||
t.Helper()
|
||||
if fis, err := ioutil.ReadDir(dir); err != nil {
|
||||
if fis, err := os.ReadDir(dir); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if len(fis) > 0 && runtime.GOOS != "windows" {
|
||||
for _, fi := range fis {
|
||||
|
||||
@@ -18,24 +18,98 @@ import (
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/tailscale/golang-x-crypto/ssh"
|
||||
"tailscale.com/envknob"
|
||||
"go4.org/mem"
|
||||
"golang.org/x/exp/slices"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/util/lineread"
|
||||
"tailscale.com/util/mak"
|
||||
)
|
||||
|
||||
var useHostKeys = envknob.Bool("TS_USE_SYSTEM_SSH_HOST_KEYS")
|
||||
|
||||
// keyTypes are the SSH key types that we either try to read from the
|
||||
// system's OpenSSH keys or try to generate for ourselves when not
|
||||
// running as root.
|
||||
var keyTypes = []string{"rsa", "ecdsa", "ed25519"}
|
||||
|
||||
// getSSHUsernames discovers and returns the list of usernames that are
|
||||
// potential Tailscale SSH user targets.
|
||||
//
|
||||
// Invariant: must not be called with b.mu held.
|
||||
func (b *LocalBackend) getSSHUsernames(req *tailcfg.C2NSSHUsernamesRequest) (*tailcfg.C2NSSHUsernamesResponse, error) {
|
||||
res := new(tailcfg.C2NSSHUsernamesResponse)
|
||||
if !b.tailscaleSSHEnabled() {
|
||||
return res, nil
|
||||
}
|
||||
|
||||
max := 10
|
||||
if req != nil && req.Max != 0 {
|
||||
max = req.Max
|
||||
}
|
||||
|
||||
add := func(u string) {
|
||||
if req != nil && req.Exclude[u] {
|
||||
return
|
||||
}
|
||||
switch u {
|
||||
case "nobody", "daemon", "sync":
|
||||
return
|
||||
}
|
||||
if slices.Contains(res.Usernames, u) {
|
||||
return
|
||||
}
|
||||
if len(res.Usernames) > max {
|
||||
// Enough for a hint.
|
||||
return
|
||||
}
|
||||
res.Usernames = append(res.Usernames, u)
|
||||
}
|
||||
|
||||
if opUser := b.operatorUserName(); opUser != "" {
|
||||
add(opUser)
|
||||
}
|
||||
|
||||
// Check popular usernames and see if they exist with a real shell.
|
||||
switch runtime.GOOS {
|
||||
case "darwin":
|
||||
out, err := exec.Command("dscl", ".", "list", "/Users").Output()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
lineread.Reader(bytes.NewReader(out), func(line []byte) error {
|
||||
line = bytes.TrimSpace(line)
|
||||
if len(line) == 0 || line[0] == '_' {
|
||||
return nil
|
||||
}
|
||||
add(string(line))
|
||||
return nil
|
||||
})
|
||||
default:
|
||||
lineread.File("/etc/passwd", func(line []byte) error {
|
||||
line = bytes.TrimSpace(line)
|
||||
if len(line) == 0 || line[0] == '#' || line[0] == '_' {
|
||||
return nil
|
||||
}
|
||||
if mem.HasSuffix(mem.B(line), mem.S("/nologin")) ||
|
||||
mem.HasSuffix(mem.B(line), mem.S("/false")) {
|
||||
return nil
|
||||
}
|
||||
colon := bytes.IndexByte(line, ':')
|
||||
if colon != -1 {
|
||||
add(string(line[:colon]))
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (b *LocalBackend) GetSSH_HostKeys() (keys []ssh.Signer, err error) {
|
||||
var existing map[string]ssh.Signer
|
||||
if os.Geteuid() == 0 {
|
||||
@@ -83,7 +157,7 @@ func (b *LocalBackend) hostKeyFileOrCreate(keyDir, typ string) ([]byte, error) {
|
||||
defer keyGenMu.Unlock()
|
||||
|
||||
path := filepath.Join(keyDir, "ssh_host_"+typ+"_key")
|
||||
v, err := ioutil.ReadFile(path)
|
||||
v, err := os.ReadFile(path)
|
||||
if err == nil {
|
||||
return v, nil
|
||||
}
|
||||
@@ -124,7 +198,7 @@ func (b *LocalBackend) hostKeyFileOrCreate(keyDir, typ string) ([]byte, error) {
|
||||
func (b *LocalBackend) getSystemSSH_HostKeys() (ret map[string]ssh.Signer) {
|
||||
for _, typ := range keyTypes {
|
||||
filename := "/etc/ssh/ssh_host_" + typ + "_key"
|
||||
hostKey, err := ioutil.ReadFile(filename)
|
||||
hostKey, err := os.ReadFile(filename)
|
||||
if err != nil || len(bytes.TrimSpace(hostKey)) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -6,6 +6,16 @@
|
||||
|
||||
package ipnlocal
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"tailscale.com/tailcfg"
|
||||
)
|
||||
|
||||
func (b *LocalBackend) getSSHHostKeyPublicStrings() []string {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *LocalBackend) getSSHUsernames(*tailcfg.C2NSSHUsernamesRequest) (*tailcfg.C2NSSHUsernamesResponse, error) {
|
||||
return nil, errors.New("not implemented")
|
||||
}
|
||||
|
||||
@@ -2,14 +2,18 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build linux
|
||||
// +build linux
|
||||
//go:build linux || (darwin && !ios)
|
||||
// +build linux darwin,!ios
|
||||
|
||||
package ipnlocal
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/util/must"
|
||||
)
|
||||
|
||||
func TestSSHKeyGen(t *testing.T) {
|
||||
@@ -40,3 +44,17 @@ func TestSSHKeyGen(t *testing.T) {
|
||||
t.Errorf("got different keys on second call")
|
||||
}
|
||||
}
|
||||
|
||||
type fakeSSHServer struct {
|
||||
SSHServer
|
||||
}
|
||||
|
||||
func TestGetSSHUsernames(t *testing.T) {
|
||||
b := new(LocalBackend)
|
||||
b.sshServer = fakeSSHServer{}
|
||||
res, err := b.getSSHUsernames(new(tailcfg.C2NSSHUsernamesRequest))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Logf("Got: %s", must.Get(json.Marshal(res)))
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
@@ -58,7 +57,7 @@ import (
|
||||
|
||||
// Options is the configuration of the Tailscale node agent.
|
||||
type Options struct {
|
||||
// VarRoot is the the Tailscale daemon's private writable
|
||||
// VarRoot is the Tailscale daemon's private writable
|
||||
// directory (usually "/var/lib/tailscale" on Linux) that
|
||||
// contains the "tailscaled.state" file, the "certs" directory
|
||||
// for TLS certs, and the "files" directory for incoming
|
||||
@@ -215,7 +214,7 @@ func (s *Server) blockWhileInUse(conn io.Reader, ci connIdentity) {
|
||||
s.logf("blocking client while server in use; connIdentity=%v", ci)
|
||||
connDone := make(chan struct{})
|
||||
go func() {
|
||||
io.Copy(ioutil.Discard, conn)
|
||||
io.Copy(io.Discard, conn)
|
||||
close(connDone)
|
||||
}()
|
||||
ch := make(chan struct{}, 1)
|
||||
@@ -773,7 +772,7 @@ func New(logf logger.Logf, logid string, store ipn.StateStore, eng wgengine.Engi
|
||||
})
|
||||
|
||||
if root := b.TailscaleVarRoot(); root != "" {
|
||||
chonkDir := filepath.Join(root, "chonk")
|
||||
chonkDir := filepath.Join(root, "tka")
|
||||
if _, err := os.Stat(chonkDir); err == nil {
|
||||
// The directory exists, which means network-lock has been initialized.
|
||||
storage, err := tka.ChonkDir(chonkDir)
|
||||
@@ -933,14 +932,6 @@ func BabysitProc(ctx context.Context, args []string, logf logger.Logf) {
|
||||
startTime := time.Now()
|
||||
log.Printf("exec: %#v %v", executable, args)
|
||||
cmd := exec.Command(executable, args...)
|
||||
if runtime.GOOS == "windows" {
|
||||
extraEnv, err := loadExtraEnv()
|
||||
if err != nil {
|
||||
logf("errors loading extra env file; ignoring: %v", err)
|
||||
} else {
|
||||
cmd.Env = append(os.Environ(), extraEnv...)
|
||||
}
|
||||
}
|
||||
|
||||
// Create a pipe object to use as the subproc's stdin.
|
||||
// When the writer goes away, the reader gets EOF.
|
||||
@@ -1175,7 +1166,7 @@ func findTrueNASTaildropDir(name string) (dir string, err error) {
|
||||
}
|
||||
|
||||
// but if running on the host, it may be something like /mnt/Primary/Taildrop
|
||||
fis, err := ioutil.ReadDir("/mnt")
|
||||
fis, err := os.ReadDir("/mnt")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error reading /mnt: %w", err)
|
||||
}
|
||||
@@ -1209,38 +1200,3 @@ func findQnapTaildropDir(name string) (string, error) {
|
||||
}
|
||||
return "", fmt.Errorf("shared folder %q not found", name)
|
||||
}
|
||||
|
||||
func loadExtraEnv() (env []string, err error) {
|
||||
if runtime.GOOS != "windows" {
|
||||
return nil, nil
|
||||
}
|
||||
name := filepath.Join(os.Getenv("ProgramData"), "Tailscale", "tailscaled-env.txt")
|
||||
contents, err := os.ReadFile(name)
|
||||
if os.IsNotExist(err) {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, line := range strings.Split(string(contents), "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" || line[0] == '#' {
|
||||
continue
|
||||
}
|
||||
k, v, ok := strings.Cut(line, "=")
|
||||
if !ok || k == "" {
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(v, `"`) {
|
||||
var err error
|
||||
v, err = strconv.Unquote(v)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid value in line %q: %v", line, err)
|
||||
}
|
||||
env = append(env, k+"="+v)
|
||||
} else {
|
||||
env = append(env, line)
|
||||
}
|
||||
}
|
||||
return env, nil
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
@@ -38,6 +37,7 @@ import (
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/strs"
|
||||
"tailscale.com/version"
|
||||
"tailscale.com/version/distro"
|
||||
)
|
||||
@@ -73,7 +73,7 @@ func (h *Handler) certDir() (string, error) {
|
||||
return full, nil
|
||||
}
|
||||
|
||||
var acmeDebug = envknob.Bool("TS_DEBUG_ACME")
|
||||
var acmeDebug = envknob.RegisterBool("TS_DEBUG_ACME")
|
||||
|
||||
func (h *Handler) serveCert(w http.ResponseWriter, r *http.Request) {
|
||||
if !h.PermitWrite && !h.PermitCert {
|
||||
@@ -87,16 +87,19 @@ func (h *Handler) serveCert(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
domain := strings.TrimPrefix(r.URL.Path, "/localapi/v0/cert/")
|
||||
if domain == r.URL.Path {
|
||||
domain, ok := strs.CutPrefix(r.URL.Path, "/localapi/v0/cert/")
|
||||
if !ok {
|
||||
http.Error(w, "internal handler config wired wrong", 500)
|
||||
return
|
||||
}
|
||||
|
||||
if !validLookingCertDomain(domain) {
|
||||
http.Error(w, "invalid domain", 400)
|
||||
return
|
||||
}
|
||||
now := time.Now()
|
||||
logf := logger.WithPrefix(h.logf, fmt.Sprintf("cert(%q): ", domain))
|
||||
traceACME := func(v any) {
|
||||
if !acmeDebug {
|
||||
if !acmeDebug() {
|
||||
return
|
||||
}
|
||||
j, _ := json.MarshalIndent(v, "", "\t")
|
||||
@@ -165,6 +168,11 @@ func certFile(dir, domain string) string { return filepath.Join(dir, domain+".cr
|
||||
// keypair for domain exists on disk in dir that is valid at the
|
||||
// provided now time.
|
||||
func (h *Handler) getCertPEMCached(dir, domain string, now time.Time) (p *keyPair, ok bool) {
|
||||
if !validLookingCertDomain(domain) {
|
||||
// Before we read files from disk using it, validate it's halfway
|
||||
// reasonable looking.
|
||||
return nil, false
|
||||
}
|
||||
if keyPEM, err := os.ReadFile(keyFile(dir, domain)); err == nil {
|
||||
certPEM, _ := os.ReadFile(certFile(dir, domain))
|
||||
if validCertPEM(domain, keyPEM, certPEM, now) {
|
||||
@@ -293,7 +301,7 @@ func (h *Handler) getCertPEM(ctx context.Context, logf logger.Logf, traceACME fu
|
||||
if err := encodeECDSAKey(&privPEM, certPrivKey); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := ioutil.WriteFile(keyFile(dir, domain), privPEM.Bytes(), 0600); err != nil {
|
||||
if err := os.WriteFile(keyFile(dir, domain), privPEM.Bytes(), 0600); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -316,7 +324,7 @@ func (h *Handler) getCertPEM(ctx context.Context, logf logger.Logf, traceACME fu
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if err := ioutil.WriteFile(certFile(dir, domain), certPEM.Bytes(), 0644); err != nil {
|
||||
if err := os.WriteFile(certFile(dir, domain), certPEM.Bytes(), 0644); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -372,7 +380,7 @@ func parsePrivateKey(der []byte) (crypto.Signer, error) {
|
||||
|
||||
func acmeKey(dir string) (crypto.Signer, error) {
|
||||
pemName := filepath.Join(dir, "acme-account.key.pem")
|
||||
if v, err := ioutil.ReadFile(pemName); err == nil {
|
||||
if v, err := os.ReadFile(pemName); err == nil {
|
||||
priv, _ := pem.Decode(v)
|
||||
if priv == nil || !strings.Contains(priv.Type, "PRIVATE") {
|
||||
return nil, errors.New("acme/autocert: invalid account key found in cache")
|
||||
@@ -388,7 +396,7 @@ func acmeKey(dir string) (crypto.Signer, error) {
|
||||
if err := encodeECDSAKey(&pemBuf, privKey); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := ioutil.WriteFile(pemName, pemBuf.Bytes(), 0600); err != nil {
|
||||
if err := os.WriteFile(pemName, pemBuf.Bytes(), 0600); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return privKey, nil
|
||||
@@ -426,6 +434,21 @@ func validCertPEM(domain string, keyPEM, certPEM []byte, now time.Time) bool {
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// validLookingCertDomain reports whether name looks like a valid domain name that
|
||||
// we might be able to get a cert for.
|
||||
//
|
||||
// It's a light check primarily for double checking before it's used
|
||||
// as part of a filesystem path. The actual validation happens in checkCertDomain.
|
||||
func validLookingCertDomain(name string) bool {
|
||||
if name == "" ||
|
||||
strings.Contains(name, "..") ||
|
||||
strings.ContainsAny(name, ":/\\\x00") ||
|
||||
!strings.Contains(name, ".") {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func checkCertDomain(st *ipnstate.Status, domain string) error {
|
||||
if domain == "" {
|
||||
return errors.New("missing domain name")
|
||||
|
||||
30
ipn/localapi/cert_test.go
Normal file
30
ipn/localapi/cert_test.go
Normal file
@@ -0,0 +1,30 @@
|
||||
// Copyright (c) 2021 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 !ios && !android && !js
|
||||
// +build !ios,!android,!js
|
||||
|
||||
package localapi
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestValidLookingCertDomain(t *testing.T) {
|
||||
tests := []struct {
|
||||
in string
|
||||
want bool
|
||||
}{
|
||||
{"foo.com", true},
|
||||
{"foo..com", false},
|
||||
{"foo/com.com", false},
|
||||
{"NUL", false},
|
||||
{"", false},
|
||||
{"foo\\bar.com", false},
|
||||
{"foo\x00bar.com", false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
if got := validLookingCertDomain(tt.in); got != tt.want {
|
||||
t.Errorf("validLookingCertDomain(%q) = %v, want %v", tt.in, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -26,6 +26,8 @@ import (
|
||||
|
||||
"tailscale.com/client/tailscale/apitype"
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/health"
|
||||
"tailscale.com/hostinfo"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnlocal"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
@@ -35,9 +37,49 @@ import (
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/util/clientmetric"
|
||||
"tailscale.com/util/mak"
|
||||
"tailscale.com/util/strs"
|
||||
"tailscale.com/version"
|
||||
)
|
||||
|
||||
type localAPIHandler func(*Handler, http.ResponseWriter, *http.Request)
|
||||
|
||||
// handler is the set of LocalAPI handlers, keyed by the part of the
|
||||
// Request.URL.Path after "/localapi/v0/". If the key ends with a trailing slash
|
||||
// then it's a prefix match.
|
||||
var handler = map[string]localAPIHandler{
|
||||
// The prefix match handlers end with a slash:
|
||||
"cert/": (*Handler).serveCert,
|
||||
"file-put/": (*Handler).serveFilePut,
|
||||
"files/": (*Handler).serveFiles,
|
||||
|
||||
// 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,
|
||||
"derpmap": (*Handler).serveDERPMap,
|
||||
"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,
|
||||
"profile": (*Handler).serveProfile,
|
||||
"set-dns": (*Handler).serveSetDNS,
|
||||
"set-expiry-sooner": (*Handler).serveSetExpirySooner,
|
||||
"status": (*Handler).serveStatus,
|
||||
"tka/init": (*Handler).serveTKAInit,
|
||||
"tka/modify": (*Handler).serveTKAModify,
|
||||
"tka/status": (*Handler).serveTKAStatus,
|
||||
"upload-client-metrics": (*Handler).serveUploadClientMetrics,
|
||||
"whois": (*Handler).serveWhoIs,
|
||||
}
|
||||
|
||||
func randHex(n int) string {
|
||||
b := make([]byte, n)
|
||||
rand.Read(b)
|
||||
@@ -99,68 +141,45 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
}
|
||||
if strings.HasPrefix(r.URL.Path, "/localapi/v0/files/") {
|
||||
h.serveFiles(w, r)
|
||||
return
|
||||
if fn, ok := handlerForPath(r.URL.Path); ok {
|
||||
fn(h, w, r)
|
||||
} else {
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
if strings.HasPrefix(r.URL.Path, "/localapi/v0/file-put/") {
|
||||
h.serveFilePut(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// handlerForPath returns the LocalAPI handler for the provided Request.URI.Path.
|
||||
// (the path doesn't include any query parameters)
|
||||
func handlerForPath(urlPath string) (h localAPIHandler, ok bool) {
|
||||
if urlPath == "/" {
|
||||
return (*Handler).serveLocalAPIRoot, true
|
||||
}
|
||||
if strings.HasPrefix(r.URL.Path, "/localapi/v0/cert/") {
|
||||
h.serveCert(w, r)
|
||||
return
|
||||
suff, ok := strs.CutPrefix(urlPath, "/localapi/v0/")
|
||||
if !ok {
|
||||
// Currently all LocalAPI methods start with "/localapi/v0/" to signal
|
||||
// to people that they're not necessarily stable APIs. In practice we'll
|
||||
// probably need to keep them pretty stable anyway, but for now treat
|
||||
// them as an internal implementation detail.
|
||||
return nil, false
|
||||
}
|
||||
switch r.URL.Path {
|
||||
case "/localapi/v0/whois":
|
||||
h.serveWhoIs(w, r)
|
||||
case "/localapi/v0/goroutines":
|
||||
h.serveGoroutines(w, r)
|
||||
case "/localapi/v0/profile":
|
||||
h.serveProfile(w, r)
|
||||
case "/localapi/v0/status":
|
||||
h.serveStatus(w, r)
|
||||
case "/localapi/v0/logout":
|
||||
h.serveLogout(w, r)
|
||||
case "/localapi/v0/login-interactive":
|
||||
h.serveLoginInteractive(w, r)
|
||||
case "/localapi/v0/prefs":
|
||||
h.servePrefs(w, r)
|
||||
case "/localapi/v0/ping":
|
||||
h.servePing(w, r)
|
||||
case "/localapi/v0/check-prefs":
|
||||
h.serveCheckPrefs(w, r)
|
||||
case "/localapi/v0/check-ip-forwarding":
|
||||
h.serveCheckIPForwarding(w, r)
|
||||
case "/localapi/v0/bugreport":
|
||||
h.serveBugReport(w, r)
|
||||
case "/localapi/v0/file-targets":
|
||||
h.serveFileTargets(w, r)
|
||||
case "/localapi/v0/set-dns":
|
||||
h.serveSetDNS(w, r)
|
||||
case "/localapi/v0/derpmap":
|
||||
h.serveDERPMap(w, r)
|
||||
case "/localapi/v0/metrics":
|
||||
h.serveMetrics(w, r)
|
||||
case "/localapi/v0/debug":
|
||||
h.serveDebug(w, r)
|
||||
case "/localapi/v0/set-expiry-sooner":
|
||||
h.serveSetExpirySooner(w, r)
|
||||
case "/localapi/v0/dial":
|
||||
h.serveDial(w, r)
|
||||
case "/localapi/v0/id-token":
|
||||
h.serveIDToken(w, r)
|
||||
case "/localapi/v0/upload-client-metrics":
|
||||
h.serveUploadClientMetrics(w, r)
|
||||
case "/localapi/v0/tka/status":
|
||||
h.serveTkaStatus(w, r)
|
||||
case "/localapi/v0/tka/init":
|
||||
h.serveTkaInit(w, r)
|
||||
case "/":
|
||||
io.WriteString(w, "tailscaled\n")
|
||||
default:
|
||||
http.Error(w, "404 not found", 404)
|
||||
if fn, ok := handler[suff]; ok {
|
||||
// Here we match exact handler suffixes like "status" or ones with a
|
||||
// slash already in their name, like "tka/status".
|
||||
return fn, true
|
||||
}
|
||||
// Otherwise, it might be a prefix match like "files/*" which we look up
|
||||
// by the prefix including first trailing slash.
|
||||
if i := strings.IndexByte(suff, '/'); i != -1 {
|
||||
suff = suff[:i+1]
|
||||
if fn, ok := handler[suff]; ok {
|
||||
return fn, true
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func (*Handler) serveLocalAPIRoot(w http.ResponseWriter, r *http.Request) {
|
||||
io.WriteString(w, "tailscaled\n")
|
||||
}
|
||||
|
||||
// serveIDToken handles requests to get an OIDC ID token.
|
||||
@@ -221,6 +240,16 @@ func (h *Handler) serveBugReport(w http.ResponseWriter, r *http.Request) {
|
||||
if note := r.FormValue("note"); len(note) > 0 {
|
||||
h.logf("user bugreport note: %s", note)
|
||||
}
|
||||
hi, _ := json.Marshal(hostinfo.New())
|
||||
h.logf("user bugreport hostinfo: %s", hi)
|
||||
if err := health.OverallError(); err != nil {
|
||||
h.logf("user bugreport health: %s", err.Error())
|
||||
} else {
|
||||
h.logf("user bugreport health: ok")
|
||||
}
|
||||
if defBool(r.FormValue("diagnose"), false) {
|
||||
h.b.Doctor(r.Context(), logger.WithPrefix(h.logf, "diag: "))
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
fmt.Fprintln(w, logMarker)
|
||||
}
|
||||
@@ -315,6 +344,24 @@ func (h *Handler) serveDebug(w http.ResponseWriter, r *http.Request) {
|
||||
io.WriteString(w, "done\n")
|
||||
}
|
||||
|
||||
func (h *Handler) serveComponentDebugLogging(w http.ResponseWriter, r *http.Request) {
|
||||
if !h.PermitWrite {
|
||||
http.Error(w, "debug access denied", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
component := r.FormValue("component")
|
||||
secs, _ := strconv.Atoi(r.FormValue("secs"))
|
||||
err := h.b.SetComponentDebugLogging(component, time.Now().Add(time.Duration(secs)*time.Second))
|
||||
var res struct {
|
||||
Error string
|
||||
}
|
||||
if err != nil {
|
||||
res.Error = err.Error()
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(res)
|
||||
}
|
||||
|
||||
// serveProfileFunc is the implementation of Handler.serveProfile, after auth,
|
||||
// for platforms where we want to link it in.
|
||||
var serveProfileFunc func(http.ResponseWriter, *http.Request)
|
||||
@@ -800,13 +847,13 @@ func (h *Handler) serveUploadClientMetrics(w http.ResponseWriter, r *http.Reques
|
||||
json.NewEncoder(w).Encode(struct{}{})
|
||||
}
|
||||
|
||||
func (h *Handler) serveTkaStatus(w http.ResponseWriter, r *http.Request) {
|
||||
func (h *Handler) serveTKAStatus(w http.ResponseWriter, r *http.Request) {
|
||||
if !h.PermitRead {
|
||||
http.Error(w, "lock status access denied", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
if r.Method != http.MethodGet {
|
||||
http.Error(w, "use Get", http.StatusMethodNotAllowed)
|
||||
http.Error(w, "use GET", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -819,7 +866,7 @@ func (h *Handler) serveTkaStatus(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write(j)
|
||||
}
|
||||
|
||||
func (h *Handler) serveTkaInit(w http.ResponseWriter, r *http.Request) {
|
||||
func (h *Handler) serveTKAInit(w http.ResponseWriter, r *http.Request) {
|
||||
if !h.PermitWrite {
|
||||
http.Error(w, "lock init access denied", http.StatusForbidden)
|
||||
return
|
||||
@@ -852,6 +899,40 @@ func (h *Handler) serveTkaInit(w http.ResponseWriter, r *http.Request) {
|
||||
w.Write(j)
|
||||
}
|
||||
|
||||
func (h *Handler) serveTKAModify(w http.ResponseWriter, r *http.Request) {
|
||||
if !h.PermitWrite {
|
||||
http.Error(w, "network-lock modify access denied", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "use POST", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
type modifyRequest struct {
|
||||
AddKeys []tka.Key
|
||||
RemoveKeys []tka.Key
|
||||
}
|
||||
var req modifyRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "invalid JSON body", 400)
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.b.NetworkLockModify(req.AddKeys, req.RemoveKeys); err != nil {
|
||||
http.Error(w, "network-lock modify failed: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
j, err := json.MarshalIndent(h.b.NetworkLockStatus(), "", "\t")
|
||||
if err != nil {
|
||||
http.Error(w, "JSON encoding error", 500)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(j)
|
||||
}
|
||||
|
||||
func defBool(a string, def bool) bool {
|
||||
if a == "" {
|
||||
return def
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/netip"
|
||||
"os"
|
||||
@@ -618,7 +617,7 @@ func PrefsFromBytes(b []byte) (*Prefs, error) {
|
||||
// LoadPrefs loads a legacy relaynode config file into Prefs
|
||||
// with sensible migration defaults set.
|
||||
func LoadPrefs(filename string) (*Prefs, error) {
|
||||
data, err := ioutil.ReadFile(filename)
|
||||
data, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("LoadPrefs open: %w", err) // err includes path
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/netip"
|
||||
"os"
|
||||
"reflect"
|
||||
@@ -471,10 +470,10 @@ func TestLoadPrefsNotExist(t *testing.T) {
|
||||
t.Fatalf("unexpected prefs=%#v, err=%v", p, err)
|
||||
}
|
||||
|
||||
// TestLoadPrefsFileWithZeroInIt verifies that LoadPrefs hanldes corrupted input files.
|
||||
// TestLoadPrefsFileWithZeroInIt verifies that LoadPrefs handles corrupted input files.
|
||||
// See issue #954 for details.
|
||||
func TestLoadPrefsFileWithZeroInIt(t *testing.T) {
|
||||
f, err := ioutil.TempFile("", "TestLoadPrefsFileWithZeroInIt")
|
||||
f, err := os.CreateTemp("", "TestLoadPrefsFileWithZeroInIt")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
18
ipn/store.go
18
ipn/store.go
@@ -6,6 +6,8 @@ package ipn
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// ErrStateNotExist is returned by StateStore.ReadState when the
|
||||
@@ -35,7 +37,7 @@ const (
|
||||
// StateKey "user-1234".
|
||||
ServerModeStartKey = StateKey("server-mode-start-key")
|
||||
|
||||
// NLKeyStateKey is the key under which we store the nodes'
|
||||
// NLKeyStateKey is the key under which we store the node's
|
||||
// network-lock node key, in its key.NLPrivate.MarshalText representation.
|
||||
NLKeyStateKey = StateKey("_nl-node-key")
|
||||
)
|
||||
@@ -48,3 +50,17 @@ type StateStore interface {
|
||||
// WriteState saves bs as the state associated with ID.
|
||||
WriteState(id StateKey, bs []byte) error
|
||||
}
|
||||
|
||||
// ReadStoreInt reads an integer from a StateStore.
|
||||
func ReadStoreInt(store StateStore, id StateKey) (int64, error) {
|
||||
v, err := store.ReadState(id)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return strconv.ParseInt(string(v), 10, 64)
|
||||
}
|
||||
|
||||
// PutStoreInt puts an integer into a StateStore.
|
||||
func PutStoreInt(store StateStore, id StateKey, val int64) error {
|
||||
return store.WriteState(id, fmt.Appendf(nil, "%d", val))
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
@@ -128,7 +127,7 @@ func NewFileStore(logf logger.Logf, path string) (ipn.StateStore, error) {
|
||||
return nil, fmt.Errorf("creating state directory: %w", err)
|
||||
}
|
||||
|
||||
bs, err := ioutil.ReadFile(path)
|
||||
bs, err := os.ReadFile(path)
|
||||
|
||||
// Treat an empty file as a missing file.
|
||||
// (https://github.com/tailscale/tailscale/issues/895#issuecomment-723255589)
|
||||
|
||||
@@ -38,7 +38,7 @@ Client][]. 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/golang-x-crypto](https://pkg.go.dev/github.com/tailscale/golang-x-crypto) ([BSD-3-Clause](https://github.com/tailscale/golang-x-crypto/blob/0b941c09a5e1/LICENSE))
|
||||
- [github.com/tailscale/golang-x-crypto](https://pkg.go.dev/github.com/tailscale/golang-x-crypto) ([BSD-3-Clause](https://github.com/tailscale/golang-x-crypto/blob/62f465106986/LICENSE))
|
||||
- [github.com/tailscale/goupnp](https://pkg.go.dev/github.com/tailscale/goupnp) ([BSD-2-Clause](https://github.com/tailscale/goupnp/blob/c64d0f06ea05/LICENSE))
|
||||
- [github.com/tailscale/netlink](https://pkg.go.dev/github.com/tailscale/netlink) ([Apache-2.0](https://github.com/tailscale/netlink/blob/cabfb018fe85/LICENSE))
|
||||
- [github.com/tailscale/tailscale-android](https://pkg.go.dev/github.com/tailscale/tailscale-android) ([BSD-3-Clause](https://github.com/tailscale/tailscale-android/blob/HEAD/LICENSE))
|
||||
@@ -55,9 +55,9 @@ 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/+/a9213eeb: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/+/807a2327:shiny/LICENSE))
|
||||
- [golang.org/x/image](https://pkg.go.dev/golang.org/x/image) ([BSD-3-Clause](https://cs.opensource.google/go/x/image/+/a66eb644:LICENSE))
|
||||
- [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://cs.opensource.google/go/x/net/+/c690dde0:LICENSE))
|
||||
- [golang.org/x/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://cs.opensource.google/go/x/net/+/bcab6841: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/+/0de741cf:LICENSE))
|
||||
- [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/c0bba94a:LICENSE))
|
||||
- [golang.org/x/sys](https://pkg.go.dev/golang.org/x/sys) ([BSD-3-Clause](https://cs.opensource.google/go/x/sys/+/3c1f3524:LICENSE))
|
||||
- [golang.org/x/term](https://pkg.go.dev/golang.org/x/term) ([BSD-3-Clause](https://cs.opensource.google/go/x/term/+/03fcf44c:LICENSE))
|
||||
- [golang.org/x/text](https://pkg.go.dev/golang.org/x/text) ([BSD-3-Clause](https://cs.opensource.google/go/x/text/+/18b340fc: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/+/f0f3c7e8:LICENSE))
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user