Compare commits
104 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9dd89b8c26 | ||
|
|
b7d0a67f5e | ||
|
|
7045359322 | ||
|
|
49ae82e8bd | ||
|
|
7077adc475 | ||
|
|
5e65717c7f | ||
|
|
39d73f9fae | ||
|
|
6dbfafd75f | ||
|
|
f99a3e5fd9 | ||
|
|
c68ba18d43 | ||
|
|
6cee582f67 | ||
|
|
0cd803e174 | ||
|
|
6a7e66b666 | ||
|
|
7a3a1e3e68 | ||
|
|
637b4b72c0 | ||
|
|
af0d2bd407 | ||
|
|
ca827b9a04 | ||
|
|
5cb9db9950 | ||
|
|
df91c8f153 | ||
|
|
7c1ba770be | ||
|
|
c1522858bb | ||
|
|
1251e128da | ||
|
|
5ab91f8b99 | ||
|
|
3344521e88 | ||
|
|
7dc95765f7 | ||
|
|
c7ddb42083 | ||
|
|
77ba681a7b | ||
|
|
54e8fa172b | ||
|
|
8a9888aea9 | ||
|
|
5bdf8e21c8 | ||
|
|
78c60b49f7 | ||
|
|
f8497daa68 | ||
|
|
8023971bff | ||
|
|
0cc397e96d | ||
|
|
46235b790d | ||
|
|
b6ce364bf7 | ||
|
|
78dec82736 | ||
|
|
7c2fdcd028 | ||
|
|
613d624bea | ||
|
|
d982963e0b | ||
|
|
cdf7ae8066 | ||
|
|
30afe38cb9 | ||
|
|
2a6afafc76 | ||
|
|
23a664325e | ||
|
|
b9e1c18578 | ||
|
|
a5340a07cf | ||
|
|
ccca9faaf8 | ||
|
|
f7c15dd0b0 | ||
|
|
a780929391 | ||
|
|
fc688fe024 | ||
|
|
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 |
@@ -1 +1 @@
|
||||
1.31.0
|
||||
1.32.3
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -7,6 +7,9 @@ package apitype
|
||||
|
||||
import "tailscale.com/tailcfg"
|
||||
|
||||
// LocalAPIHost is the Host header value used by the LocalAPI.
|
||||
const LocalAPIHost = "local-tailscaled.sock"
|
||||
|
||||
// WhoIsResponse is the JSON type returned by tailscaled debug server's /whois?ip=$IP handler.
|
||||
type WhoIsResponse struct {
|
||||
Node *tailcfg.Node
|
||||
|
||||
@@ -197,7 +197,7 @@ func SetVersionMismatchHandler(f func(clientVer, serverVer string)) {
|
||||
}
|
||||
|
||||
func (lc *LocalClient) send(ctx context.Context, method, path string, wantStatus int, body io.Reader) ([]byte, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, method, "http://local-tailscaled.sock"+path, body)
|
||||
req, err := http.NewRequestWithContext(ctx, method, "http://"+apitype.LocalAPIHost+path, body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -276,6 +276,12 @@ type BugReportOpts struct {
|
||||
// Diagnose specifies whether to print additional diagnostic information to
|
||||
// the logs when generating this bugreport.
|
||||
Diagnose bool
|
||||
|
||||
// Record specifies, if non-nil, whether to perform a bugreport
|
||||
// "recording"–generating an initial log marker, then waiting for
|
||||
// this channel to be closed before finishing the request, which
|
||||
// generates another log marker.
|
||||
Record <-chan struct{}
|
||||
}
|
||||
|
||||
// BugReportWithOpts logs and returns a log marker that can be shared by the
|
||||
@@ -284,16 +290,40 @@ type BugReportOpts struct {
|
||||
// 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
|
||||
qparams := make(url.Values)
|
||||
if opts.Note != "" {
|
||||
qparams.Set("note", opts.Note)
|
||||
}
|
||||
if opts.Diagnose {
|
||||
qparams.Set("diagnose", "true")
|
||||
}
|
||||
if opts.Record != nil {
|
||||
qparams.Set("record", "true")
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
|
||||
var requestBody io.Reader
|
||||
if opts.Record != nil {
|
||||
pr, pw := io.Pipe()
|
||||
requestBody = pr
|
||||
|
||||
// This goroutine waits for the 'Record' channel to be closed,
|
||||
// and then closes the write end of our pipe to unblock the
|
||||
// reader.
|
||||
go func() {
|
||||
defer pw.Close()
|
||||
select {
|
||||
case <-opts.Record:
|
||||
case <-ctx.Done():
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// lc.send might block if opts.Record != nil; see above.
|
||||
uri := fmt.Sprintf("/localapi/v0/bugreport?%s", qparams.Encode())
|
||||
body, err := lc.send(ctx, "POST", uri, 200, nil)
|
||||
body, err := lc.send(ctx, "POST", uri, 200, requestBody)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -318,6 +348,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)
|
||||
@@ -383,7 +435,7 @@ func (lc *LocalClient) DeleteWaitingFile(ctx context.Context, baseName string) e
|
||||
}
|
||||
|
||||
func (lc *LocalClient) GetWaitingFile(ctx context.Context, baseName string) (rc io.ReadCloser, size int64, err error) {
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", "http://local-tailscaled.sock/localapi/v0/files/"+url.PathEscape(baseName), nil)
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", "http://"+apitype.LocalAPIHost+"/localapi/v0/files/"+url.PathEscape(baseName), nil)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
@@ -420,7 +472,7 @@ func (lc *LocalClient) FileTargets(ctx context.Context) ([]apitype.FileTarget, e
|
||||
// A size of -1 means unknown.
|
||||
// The name parameter is the original filename, not escaped.
|
||||
func (lc *LocalClient) PushFile(ctx context.Context, target tailcfg.StableNodeID, size int64, name string, r io.Reader) error {
|
||||
req, err := http.NewRequestWithContext(ctx, "PUT", "http://local-tailscaled.sock/localapi/v0/file-put/"+string(target)+"/"+url.PathEscape(name), r)
|
||||
req, err := http.NewRequestWithContext(ctx, "PUT", "http://"+apitype.LocalAPIHost+"/localapi/v0/file-put/"+string(target)+"/"+url.PathEscape(name), r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -543,7 +595,7 @@ func (lc *LocalClient) DialTCP(ctx context.Context, host string, port uint16) (n
|
||||
},
|
||||
}
|
||||
ctx = httptrace.WithClientTrace(ctx, &trace)
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", "http://local-tailscaled.sock/localapi/v0/dial", nil)
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", "http://"+apitype.LocalAPIHost+"/localapi/v0/dial", nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -674,14 +726,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 {
|
||||
@@ -748,6 +800,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.
|
||||
//
|
||||
|
||||
@@ -115,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 {
|
||||
|
||||
@@ -47,6 +47,7 @@ tailscale.com/cmd/derper dependencies: (generated by github.com/tailscale/depawa
|
||||
tailscale.com/net/tlsdial from tailscale.com/derp/derphttp
|
||||
tailscale.com/net/tsaddr from tailscale.com/ipn+
|
||||
💣 tailscale.com/net/tshttpproxy from tailscale.com/derp/derphttp+
|
||||
tailscale.com/net/wsconn from tailscale.com/cmd/derper+
|
||||
tailscale.com/paths from tailscale.com/client/tailscale
|
||||
tailscale.com/safesocket from tailscale.com/client/tailscale
|
||||
tailscale.com/syncs from tailscale.com/cmd/derper+
|
||||
@@ -107,6 +108,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+
|
||||
|
||||
@@ -325,11 +325,31 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
noContentChallengeHeader = "X-Tailscale-Challenge"
|
||||
noContentResponseHeader = "X-Tailscale-Response"
|
||||
)
|
||||
|
||||
// For captive portal detection
|
||||
func serveNoContent(w http.ResponseWriter, r *http.Request) {
|
||||
if challenge := r.Header.Get(noContentChallengeHeader); challenge != "" {
|
||||
badChar := strings.IndexFunc(challenge, func(r rune) bool {
|
||||
return !isChallengeChar(r)
|
||||
}) != -1
|
||||
if len(challenge) <= 64 && !badChar {
|
||||
w.Header().Set(noContentResponseHeader, "response "+challenge)
|
||||
}
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func isChallengeChar(c rune) bool {
|
||||
// Semi-randomly chosen as a limited set of valid characters
|
||||
return ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z') ||
|
||||
('0' <= c && c <= '9') ||
|
||||
c == '.' || c == '-' || c == '_'
|
||||
}
|
||||
|
||||
// probeHandler is the endpoint that js/wasm clients hit to measure
|
||||
// DERP latency, since they can't do UDP STUN queries.
|
||||
func probeHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
@@ -7,6 +7,9 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"tailscale.com/net/stun"
|
||||
@@ -67,3 +70,57 @@ func BenchmarkServerSTUN(b *testing.B) {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestNoContent(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
input string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "no challenge",
|
||||
},
|
||||
{
|
||||
name: "valid challenge",
|
||||
input: "input",
|
||||
want: "response input",
|
||||
},
|
||||
{
|
||||
name: "invalid challenge",
|
||||
input: "foo\x00bar",
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "whitespace invalid challenge",
|
||||
input: "foo bar",
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "long challenge",
|
||||
input: strings.Repeat("x", 65),
|
||||
want: "",
|
||||
},
|
||||
}
|
||||
for _, tt := range testCases {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
req, _ := http.NewRequest("GET", "https://localhost/generate_204", nil)
|
||||
if tt.input != "" {
|
||||
req.Header.Set(noContentChallengeHeader, tt.input)
|
||||
}
|
||||
w := httptest.NewRecorder()
|
||||
serveNoContent(w, req)
|
||||
resp := w.Result()
|
||||
|
||||
if tt.want == "" {
|
||||
if h, found := resp.Header[noContentResponseHeader]; found {
|
||||
t.Errorf("got %+v; expected no response header", h)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if got := resp.Header.Get(noContentResponseHeader); got != tt.want {
|
||||
t.Errorf("got %q; want %q", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
|
||||
"nhooyr.io/websocket"
|
||||
"tailscale.com/derp"
|
||||
"tailscale.com/net/wsconn"
|
||||
)
|
||||
|
||||
var counterWebSocketAccepts = expvar.NewInt("derp_websocket_accepts")
|
||||
@@ -23,7 +24,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)
|
||||
@@ -50,7 +51,7 @@ func addWebSocketSupport(s *derp.Server, base http.Handler) http.Handler {
|
||||
return
|
||||
}
|
||||
counterWebSocketAccepts.Add(1)
|
||||
wc := websocket.NetConn(r.Context(), c, websocket.MessageBinary)
|
||||
wc := wsconn.NetConn(r.Context(), c, websocket.MessageBinary)
|
||||
brw := bufio.NewReadWriter(bufio.NewReader(wc), bufio.NewWriter(wc))
|
||||
s.Accept(r.Context(), wc, brw, r.RemoteAddr)
|
||||
})
|
||||
|
||||
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
|
||||
}
|
||||
171
cmd/ssh-auth-none-demo/ssh-auth-none-demo.go
Normal file
171
cmd/ssh-auth-none-demo/ssh-auth-none-demo.go
Normal file
@@ -0,0 +1,171 @@
|
||||
// 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 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 {
|
||||
return &gossh.ServerConfig{
|
||||
ImplicitAuthMethod: "tailscale",
|
||||
NoClientAuth: true, // required for the NoClientAuthCallback to run
|
||||
NoClientAuthCallback: func(gossh.ConnMetadata) (*gossh.Permissions, error) {
|
||||
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
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
|
||||
"github.com/peterbourgon/ff/v3/ffcli"
|
||||
"tailscale.com/client/tailscale"
|
||||
@@ -21,12 +22,14 @@ var bugReportCmd = &ffcli.Command{
|
||||
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 {
|
||||
@@ -36,15 +39,46 @@ 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.BugReportWithOpts(ctx, tailscale.BugReportOpts{
|
||||
opts := tailscale.BugReportOpts{
|
||||
Note: note,
|
||||
Diagnose: bugReportArgs.diagnose,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
outln(logMarker)
|
||||
if !bugReportArgs.record {
|
||||
// Simple, non-record case
|
||||
logMarker, err := localClient.BugReportWithOpts(ctx, opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
outln(logMarker)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Recording; run the request in the background
|
||||
done := make(chan struct{})
|
||||
opts.Record = done
|
||||
|
||||
type bugReportResp struct {
|
||||
marker string
|
||||
err error
|
||||
}
|
||||
resCh := make(chan bugReportResp, 1)
|
||||
go func() {
|
||||
m, err := localClient.BugReportWithOpts(ctx, opts)
|
||||
resCh <- bugReportResp{m, err}
|
||||
}()
|
||||
|
||||
outln("Recording started; please reproduce your issue and then press Enter...")
|
||||
fmt.Scanln()
|
||||
close(done)
|
||||
res := <-resCh
|
||||
|
||||
if res.err != nil {
|
||||
return res.err
|
||||
}
|
||||
|
||||
outln(res.marker)
|
||||
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{
|
||||
|
||||
@@ -42,7 +42,7 @@ var debugCmd = &ffcli.Command{
|
||||
FlagSet: (func() *flag.FlagSet {
|
||||
fs := newFlagSet("debug")
|
||||
fs.StringVar(&debugArgs.file, "file", "", "get, delete:NAME, or NAME")
|
||||
fs.StringVar(&debugArgs.cpuFile, "cpu-profile", "", "if non-empty, grab a CPU profile for --profile-sec seconds and write it to this file; - for stdout")
|
||||
fs.StringVar(&debugArgs.cpuFile, "cpu-profile", "", "if non-empty, grab a CPU profile for --profile-seconds seconds and write it to this file; - for stdout")
|
||||
fs.StringVar(&debugArgs.memFile, "mem-profile", "", "if non-empty, grab a memory profile and write it to this file; - for stdout")
|
||||
fs.IntVar(&debugArgs.cpuSec, "profile-seconds", 15, "number of seconds to run a CPU profile for, when --cpu-profile is non-empty")
|
||||
return fs
|
||||
@@ -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,
|
||||
@@ -513,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
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ import (
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/net/tsaddr"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/util/quarantine"
|
||||
"tailscale.com/version"
|
||||
)
|
||||
|
||||
@@ -393,6 +394,10 @@ func receiveFile(ctx context.Context, wf apitype.WaitingFile, dir string) (targe
|
||||
if err != nil {
|
||||
return "", 0, err
|
||||
}
|
||||
// Apply quarantine attribute before copying
|
||||
if err := quarantine.SetOnFile(f); err != nil {
|
||||
return "", 0, fmt.Errorf("failed to apply quarantine attribute to file %v: %v", f.Name(), err)
|
||||
}
|
||||
_, err = io.Copy(f, rc)
|
||||
if err != nil {
|
||||
f.Close()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -501,7 +501,7 @@ func runUp(ctx context.Context, args []string) (retErr error) {
|
||||
fatalf("%s", err)
|
||||
}
|
||||
if justEditMP != nil {
|
||||
justEditMP.EggSet = true
|
||||
justEditMP.EggSet = egg
|
||||
_, err := localClient.EditPrefs(ctx, justEditMP)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -474,8 +474,8 @@ func tailscaleUp(ctx context.Context, prefs *ipn.Prefs, forceReauth bool) (authU
|
||||
authURL = *url
|
||||
cancel()
|
||||
}
|
||||
if !forceReauth && n.Prefs != nil {
|
||||
p1, p2 := *n.Prefs, *prefs
|
||||
if !forceReauth && n.Prefs != nil && n.Prefs.Valid() {
|
||||
p1, p2 := n.Prefs.AsStruct(), *prefs
|
||||
p1.Persist = nil
|
||||
p2.Persist = nil
|
||||
if p1.Equals(&p2) {
|
||||
|
||||
@@ -7,6 +7,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
W 💣 github.com/alexbrainman/sspi/negotiate from tailscale.com/net/tshttpproxy
|
||||
github.com/fxamacker/cbor/v2 from tailscale.com/tka
|
||||
github.com/golang/groupcache/lru from tailscale.com/net/dnscache
|
||||
D github.com/google/uuid from tailscale.com/util/quarantine
|
||||
github.com/hdevalence/ed25519consensus from tailscale.com/tka
|
||||
L github.com/josharian/native from github.com/mdlayher/netlink+
|
||||
L 💣 github.com/jsimonetti/rtnetlink from tailscale.com/net/interfaces
|
||||
@@ -70,6 +71,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
tailscale.com/net/tlsdial from tailscale.com/derp/derphttp+
|
||||
tailscale.com/net/tsaddr from tailscale.com/net/interfaces+
|
||||
💣 tailscale.com/net/tshttpproxy from tailscale.com/derp/derphttp+
|
||||
tailscale.com/net/wsconn from tailscale.com/control/controlhttp+
|
||||
tailscale.com/paths from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/safesocket from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/syncs from tailscale.com/net/netcheck+
|
||||
@@ -101,6 +103,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
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/quarantine from tailscale.com/cmd/tailscale/cli
|
||||
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+
|
||||
@@ -135,6 +138,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+
|
||||
@@ -167,6 +172,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
||||
crypto/tls from github.com/tcnksm/go-httpstat+
|
||||
crypto/x509 from crypto/tls+
|
||||
crypto/x509/pkix from crypto/x509+
|
||||
D database/sql/driver from github.com/google/uuid
|
||||
embed from tailscale.com/cmd/tailscale/cli+
|
||||
encoding from encoding/json+
|
||||
encoding/asn1 from crypto/x509+
|
||||
|
||||
@@ -240,6 +240,8 @@ 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/net/wsconn from tailscale.com/control/controlhttp+
|
||||
tailscale.com/paths from tailscale.com/ipn/ipnlocal+
|
||||
tailscale.com/portlist from tailscale.com/ipn/ipnlocal
|
||||
tailscale.com/safesocket from tailscale.com/client/tailscale+
|
||||
@@ -321,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+
|
||||
@@ -342,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+
|
||||
|
||||
@@ -88,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"
|
||||
@@ -158,7 +158,7 @@ func main() {
|
||||
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, 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 emphemeral node. If empty and --statedir is provided, the default is <statedir>/tailscaled.state. Default: "+paths.DefaultTailscaledStateFile())
|
||||
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")
|
||||
@@ -375,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)
|
||||
|
||||
@@ -23,6 +23,7 @@ package main // import "tailscale.com/cmd/tailscaled"
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/netip"
|
||||
@@ -192,7 +193,7 @@ 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)
|
||||
@@ -265,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)
|
||||
|
||||
@@ -57,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 {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -42,7 +42,7 @@ 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) {
|
||||
@@ -60,8 +60,8 @@ export function runSSHSession(
|
||||
onDone() {
|
||||
resizeObserver?.disconnect()
|
||||
term.dispose()
|
||||
if (handleBeforeUnload) {
|
||||
parentWindow.removeEventListener("beforeunload", handleBeforeUnload)
|
||||
if (handleUnload) {
|
||||
parentWindow.removeEventListener("unload", handleUnload)
|
||||
}
|
||||
onDone()
|
||||
},
|
||||
@@ -75,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
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -761,7 +761,7 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, readOnly bool
|
||||
request := &tailcfg.MapRequest{
|
||||
Version: tailcfg.CurrentCapabilityVersion,
|
||||
KeepAlive: c.keepAlive,
|
||||
NodeKey: persist.PrivateNodeKey.Public(),
|
||||
NodeKey: persist.PublicNodeKey(),
|
||||
DiscoKey: c.discoPubKey,
|
||||
Endpoints: epStrs,
|
||||
EndpointTypes: epTypes,
|
||||
@@ -776,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()),
|
||||
}
|
||||
|
||||
@@ -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,6 +45,7 @@ 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
|
||||
@@ -113,6 +114,9 @@ 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
|
||||
}
|
||||
@@ -143,20 +147,21 @@ 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,
|
||||
TKAEnabled: ms.lastTKAInfo != nil && !ms.lastTKAInfo.Disabled,
|
||||
NodeKey: ms.privateNodeKey.Public(),
|
||||
PrivateKey: ms.privateNodeKey,
|
||||
MachineKey: ms.machinePubKey,
|
||||
Peers: resp.Peers,
|
||||
UserProfiles: make(map[tailcfg.UserID]tailcfg.UserProfile),
|
||||
Domain: ms.lastDomain,
|
||||
DomainAuditLogID: ms.lastDomainAuditLogID,
|
||||
DNS: *ms.lastDNSConfig,
|
||||
PacketFilter: ms.lastParsedPacketFilter,
|
||||
SSHPolicy: ms.lastSSHPolicy,
|
||||
CollectServices: ms.collectServices,
|
||||
DERPMap: ms.lastDERPMap,
|
||||
Debug: debug,
|
||||
ControlHealth: ms.lastHealth,
|
||||
TKAEnabled: ms.lastTKAInfo != nil && !ms.lastTKAInfo.Disabled,
|
||||
}
|
||||
ms.netMapBuilding = nm
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
|
||||
"nhooyr.io/websocket"
|
||||
"tailscale.com/control/controlbase"
|
||||
"tailscale.com/net/wsconn"
|
||||
)
|
||||
|
||||
// Variant of Dial that tunnels the request over WebSockets, since we cannot do
|
||||
@@ -51,7 +52,7 @@ func (d *Dialer) Dial(ctx context.Context) (*controlbase.Conn, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
netConn := websocket.NetConn(context.Background(), wsConn, websocket.MessageBinary)
|
||||
netConn := wsconn.NetConn(context.Background(), wsConn, websocket.MessageBinary)
|
||||
cbConn, err := cont(ctx, netConn)
|
||||
if err != nil {
|
||||
netConn.Close()
|
||||
|
||||
@@ -459,13 +459,26 @@ func TestDialPlan(t *testing.T) {
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
getRandomPort := func() string {
|
||||
ln, err := net.Listen("tcp", ":0")
|
||||
if err != nil {
|
||||
t.Fatalf("net.Listen: %v", err)
|
||||
}
|
||||
defer ln.Close()
|
||||
_, port, err := net.SplitHostPort(ln.Addr().String())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return port
|
||||
}
|
||||
|
||||
// We need consistent ports for each address; these are chosen
|
||||
// randomly and we hope that they won't conflict during this test.
|
||||
httpPort := getRandomPort()
|
||||
httpsPort := getRandomPort()
|
||||
|
||||
makeHandler := func(t *testing.T, name string, host netip.Addr, wrap func(http.Handler) http.Handler) {
|
||||
done := make(chan struct{})
|
||||
t.Cleanup(func() {
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"nhooyr.io/websocket"
|
||||
"tailscale.com/control/controlbase"
|
||||
"tailscale.com/net/netutil"
|
||||
"tailscale.com/net/wsconn"
|
||||
"tailscale.com/types/key"
|
||||
)
|
||||
|
||||
@@ -111,7 +112,7 @@ func acceptWebsocket(ctx context.Context, w http.ResponseWriter, r *http.Request
|
||||
return nil, fmt.Errorf("decoding base64 handshake parameter: %v", err)
|
||||
}
|
||||
|
||||
conn := websocket.NetConn(ctx, c, websocket.MessageBinary)
|
||||
conn := wsconn.NetConn(ctx, c, websocket.MessageBinary)
|
||||
nc, err := controlbase.Server(ctx, conn, private, init)
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -96,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}
|
||||
@@ -199,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
|
||||
@@ -985,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()
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"net"
|
||||
|
||||
"nhooyr.io/websocket"
|
||||
"tailscale.com/net/wsconn"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -28,6 +29,6 @@ func dialWebsocket(ctx context.Context, urlStr string) (net.Conn, error) {
|
||||
return nil, err
|
||||
}
|
||||
log.Printf("websocket: connected to %v", urlStr)
|
||||
netConn := websocket.NetConn(context.Background(), c, websocket.MessageBinary)
|
||||
netConn := wsconn.NetConn(context.Background(), c, websocket.MessageBinary)
|
||||
return netConn, nil
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -277,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 {
|
||||
|
||||
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-20221009170451-62f465106986
|
||||
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-20221009170451-62f465106986 h1:jWSwTR9CY13oa2oxhR3FInk1ybqC1NbF9cFeoWrrx+E=
|
||||
github.com/tailscale/golang-x-crypto v0.0.0-20221009170451-62f465106986/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
|
||||
|
||||
@@ -67,9 +67,9 @@ type Notify struct {
|
||||
|
||||
LoginFinished *empty.Message // non-nil when/if the login process succeeded
|
||||
State *State // if non-nil, the new or current IPN state
|
||||
Prefs *Prefs // if non-nil, the new or current preferences
|
||||
Prefs *PrefsView // if non-nil && Valid, 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
|
||||
|
||||
@@ -106,7 +106,7 @@ func (n Notify) String() string {
|
||||
if n.State != nil {
|
||||
fmt.Fprintf(&sb, "state=%v ", *n.State)
|
||||
}
|
||||
if n.Prefs != nil {
|
||||
if n.Prefs.Valid() {
|
||||
fmt.Fprintf(&sb, "%v ", n.Prefs.Pretty())
|
||||
}
|
||||
if n.NetMap != nil {
|
||||
@@ -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 {
|
||||
|
||||
@@ -22,7 +22,8 @@ func (b *FakeBackend) Start(opts Options) error {
|
||||
}
|
||||
nl := NeedsLogin
|
||||
if b.notify != nil {
|
||||
b.notify(Notify{Prefs: opts.Prefs})
|
||||
p := opts.Prefs.View()
|
||||
b.notify(Notify{Prefs: &p})
|
||||
b.notify(Notify{State: &nl})
|
||||
}
|
||||
return nil
|
||||
@@ -83,7 +84,8 @@ func (b *FakeBackend) SetPrefs(new *Prefs) {
|
||||
}
|
||||
|
||||
if b.notify != nil {
|
||||
b.notify(Notify{Prefs: new.Clone()})
|
||||
p := new.View()
|
||||
b.notify(Notify{Prefs: &p})
|
||||
}
|
||||
if new.WantRunning && !b.live {
|
||||
b.newState(Starting)
|
||||
|
||||
176
ipn/handle.go
176
ipn/handle.go
@@ -1,176 +0,0 @@
|
||||
// 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.
|
||||
|
||||
package ipn
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/netmap"
|
||||
)
|
||||
|
||||
type Handle struct {
|
||||
b Backend
|
||||
logf logger.Logf
|
||||
|
||||
// Mutex protects everything below
|
||||
mu sync.Mutex
|
||||
xnotify func(Notify)
|
||||
frontendLogID string
|
||||
netmapCache *netmap.NetworkMap
|
||||
engineStatusCache EngineStatus
|
||||
stateCache State
|
||||
prefsCache *Prefs
|
||||
}
|
||||
|
||||
func NewHandle(b Backend, logf logger.Logf, notify func(Notify), opts Options) (*Handle, error) {
|
||||
h := &Handle{
|
||||
b: b,
|
||||
logf: logf,
|
||||
}
|
||||
|
||||
h.SetNotifyCallback(notify)
|
||||
err := h.Start(opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return h, nil
|
||||
}
|
||||
|
||||
func (h *Handle) SetNotifyCallback(notify func(Notify)) {
|
||||
h.mu.Lock()
|
||||
h.xnotify = notify
|
||||
h.mu.Unlock()
|
||||
|
||||
h.b.SetNotifyCallback(h.notify)
|
||||
}
|
||||
|
||||
func (h *Handle) Start(opts Options) error {
|
||||
h.mu.Lock()
|
||||
h.frontendLogID = opts.FrontendLogID
|
||||
h.netmapCache = nil
|
||||
h.engineStatusCache = EngineStatus{}
|
||||
h.stateCache = NoState
|
||||
if opts.Prefs != nil {
|
||||
h.prefsCache = opts.Prefs.Clone()
|
||||
}
|
||||
h.mu.Unlock()
|
||||
return h.b.Start(opts)
|
||||
}
|
||||
|
||||
func (h *Handle) Reset() {
|
||||
st := NoState
|
||||
h.notify(Notify{State: &st})
|
||||
}
|
||||
|
||||
func (h *Handle) notify(n Notify) {
|
||||
h.mu.Lock()
|
||||
if n.BackendLogID != nil {
|
||||
h.logf("Handle: logs: be:%v fe:%v",
|
||||
*n.BackendLogID, h.frontendLogID)
|
||||
}
|
||||
if n.State != nil {
|
||||
h.stateCache = *n.State
|
||||
}
|
||||
if n.Prefs != nil {
|
||||
h.prefsCache = n.Prefs.Clone()
|
||||
}
|
||||
if n.NetMap != nil {
|
||||
h.netmapCache = n.NetMap
|
||||
}
|
||||
if n.Engine != nil {
|
||||
h.engineStatusCache = *n.Engine
|
||||
}
|
||||
h.mu.Unlock()
|
||||
|
||||
if h.xnotify != nil {
|
||||
// Forward onward to our parent's notifier
|
||||
h.xnotify(n)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handle) Prefs() *Prefs {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
|
||||
return h.prefsCache.Clone()
|
||||
}
|
||||
|
||||
func (h *Handle) UpdatePrefs(updateFn func(p *Prefs)) {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
|
||||
new := h.prefsCache.Clone()
|
||||
updateFn(new)
|
||||
h.prefsCache = new
|
||||
h.b.SetPrefs(new)
|
||||
}
|
||||
|
||||
func (h *Handle) State() State {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
|
||||
return h.stateCache
|
||||
}
|
||||
|
||||
func (h *Handle) EngineStatus() EngineStatus {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
|
||||
return h.engineStatusCache
|
||||
}
|
||||
|
||||
func (h *Handle) LocalAddrs() []netip.Prefix {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
|
||||
nm := h.netmapCache
|
||||
if nm != nil {
|
||||
return nm.Addresses
|
||||
}
|
||||
return []netip.Prefix{}
|
||||
}
|
||||
|
||||
func (h *Handle) NetMap() *netmap.NetworkMap {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
|
||||
return h.netmapCache
|
||||
}
|
||||
|
||||
func (h *Handle) Expiry() time.Time {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
|
||||
nm := h.netmapCache
|
||||
if nm != nil {
|
||||
return nm.Expiry
|
||||
}
|
||||
return time.Time{}
|
||||
}
|
||||
|
||||
func (h *Handle) AdminPageURL() string {
|
||||
return h.prefsCache.AdminPageURL()
|
||||
}
|
||||
|
||||
func (h *Handle) StartLoginInteractive() {
|
||||
h.b.StartLoginInteractive()
|
||||
}
|
||||
|
||||
func (h *Handle) Login(token *tailcfg.Oauth2Token) {
|
||||
h.b.Login(token)
|
||||
}
|
||||
|
||||
func (h *Handle) Logout() {
|
||||
h.b.Logout()
|
||||
}
|
||||
|
||||
func (h *Handle) RequestEngineStatus() {
|
||||
h.b.RequestEngineStatus()
|
||||
}
|
||||
120
ipn/ipn_view.go
Normal file
120
ipn/ipn_view.go
Normal file
@@ -0,0 +1,120 @@
|
||||
// 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.
|
||||
|
||||
// Code generated by tailscale/cmd/viewer; DO NOT EDIT.
|
||||
|
||||
package ipn
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/netip"
|
||||
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/persist"
|
||||
"tailscale.com/types/preftype"
|
||||
"tailscale.com/types/views"
|
||||
)
|
||||
|
||||
//go:generate go run tailscale.com/cmd/cloner -clonefunc=false -type=Prefs
|
||||
|
||||
// View returns a readonly view of Prefs.
|
||||
func (p *Prefs) View() PrefsView {
|
||||
return PrefsView{ж: p}
|
||||
}
|
||||
|
||||
// PrefsView provides a read-only view over Prefs.
|
||||
//
|
||||
// Its methods should only be called if `Valid()` returns true.
|
||||
type PrefsView struct {
|
||||
// ж is the underlying mutable value, named with a hard-to-type
|
||||
// character that looks pointy like a pointer.
|
||||
// It is named distinctively to make you think of how dangerous it is to escape
|
||||
// to callers. You must not let callers be able to mutate it.
|
||||
ж *Prefs
|
||||
}
|
||||
|
||||
// Valid reports whether underlying value is non-nil.
|
||||
func (v PrefsView) Valid() bool { return v.ж != nil }
|
||||
|
||||
// AsStruct returns a clone of the underlying value which aliases no memory with
|
||||
// the original.
|
||||
func (v PrefsView) AsStruct() *Prefs {
|
||||
if v.ж == nil {
|
||||
return nil
|
||||
}
|
||||
return v.ж.Clone()
|
||||
}
|
||||
|
||||
func (v PrefsView) MarshalJSON() ([]byte, error) { return json.Marshal(v.ж) }
|
||||
|
||||
func (v *PrefsView) UnmarshalJSON(b []byte) error {
|
||||
if v.ж != nil {
|
||||
return errors.New("already initialized")
|
||||
}
|
||||
if len(b) == 0 {
|
||||
return nil
|
||||
}
|
||||
var x Prefs
|
||||
if err := json.Unmarshal(b, &x); err != nil {
|
||||
return err
|
||||
}
|
||||
v.ж = &x
|
||||
return nil
|
||||
}
|
||||
|
||||
func (v PrefsView) ControlURL() string { return v.ж.ControlURL }
|
||||
func (v PrefsView) RouteAll() bool { return v.ж.RouteAll }
|
||||
func (v PrefsView) AllowSingleHosts() bool { return v.ж.AllowSingleHosts }
|
||||
func (v PrefsView) ExitNodeID() tailcfg.StableNodeID { return v.ж.ExitNodeID }
|
||||
func (v PrefsView) ExitNodeIP() netip.Addr { return v.ж.ExitNodeIP }
|
||||
func (v PrefsView) ExitNodeAllowLANAccess() bool { return v.ж.ExitNodeAllowLANAccess }
|
||||
func (v PrefsView) CorpDNS() bool { return v.ж.CorpDNS }
|
||||
func (v PrefsView) RunSSH() bool { return v.ж.RunSSH }
|
||||
func (v PrefsView) WantRunning() bool { return v.ж.WantRunning }
|
||||
func (v PrefsView) LoggedOut() bool { return v.ж.LoggedOut }
|
||||
func (v PrefsView) ShieldsUp() bool { return v.ж.ShieldsUp }
|
||||
func (v PrefsView) AdvertiseTags() views.Slice[string] { return views.SliceOf(v.ж.AdvertiseTags) }
|
||||
func (v PrefsView) Hostname() string { return v.ж.Hostname }
|
||||
func (v PrefsView) NotepadURLs() bool { return v.ж.NotepadURLs }
|
||||
func (v PrefsView) ForceDaemon() bool { return v.ж.ForceDaemon }
|
||||
func (v PrefsView) Egg() bool { return v.ж.Egg }
|
||||
func (v PrefsView) AdvertiseRoutes() views.IPPrefixSlice {
|
||||
return views.IPPrefixSliceOf(v.ж.AdvertiseRoutes)
|
||||
}
|
||||
func (v PrefsView) NoSNAT() bool { return v.ж.NoSNAT }
|
||||
func (v PrefsView) NetfilterMode() preftype.NetfilterMode { return v.ж.NetfilterMode }
|
||||
func (v PrefsView) OperatorUser() string { return v.ж.OperatorUser }
|
||||
func (v PrefsView) Persist() *persist.Persist {
|
||||
if v.ж.Persist == nil {
|
||||
return nil
|
||||
}
|
||||
x := *v.ж.Persist
|
||||
return &x
|
||||
}
|
||||
|
||||
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
|
||||
var _PrefsViewNeedsRegeneration = Prefs(struct {
|
||||
ControlURL string
|
||||
RouteAll bool
|
||||
AllowSingleHosts bool
|
||||
ExitNodeID tailcfg.StableNodeID
|
||||
ExitNodeIP netip.Addr
|
||||
ExitNodeAllowLANAccess bool
|
||||
CorpDNS bool
|
||||
RunSSH bool
|
||||
WantRunning bool
|
||||
LoggedOut bool
|
||||
ShieldsUp bool
|
||||
AdvertiseTags []string
|
||||
Hostname string
|
||||
NotepadURLs bool
|
||||
ForceDaemon bool
|
||||
Egg bool
|
||||
AdvertiseRoutes []netip.Prefix
|
||||
NoSNAT bool
|
||||
NetfilterMode preftype.NetfilterMode
|
||||
OperatorUser string
|
||||
Persist *persist.Persist
|
||||
}{})
|
||||
@@ -8,6 +8,8 @@ import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/util/clientmetric"
|
||||
@@ -32,6 +34,21 @@ func (b *LocalBackend) handleC2N(w http.ResponseWriter, r *http.Request) {
|
||||
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" {
|
||||
|
||||
@@ -314,7 +314,7 @@ func TestDNSConfigForNetmap(t *testing.T) {
|
||||
verOS = "linux"
|
||||
}
|
||||
var log tstest.MemLogger
|
||||
got := dnsConfigForNetmap(tt.nm, tt.prefs, log.Logf, verOS)
|
||||
got := dnsConfigForNetmap(tt.nm, tt.prefs.View(), log.Logf, verOS)
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
gotj, _ := json.MarshalIndent(got, "", "\t")
|
||||
wantj, _ := json.MarshalIndent(tt.want, "", "\t")
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -62,7 +62,7 @@ func TestLocalLogLines(t *testing.T) {
|
||||
defer lb.Shutdown()
|
||||
|
||||
// custom adjustments for required non-nil fields
|
||||
lb.prefs = ipn.NewPrefs()
|
||||
lb.prefs = ipn.NewPrefs().View()
|
||||
lb.hostinfo = &tailcfg.Hostinfo{}
|
||||
// hacky manual override of the usual log-on-change behaviour of keylogf
|
||||
lb.keyLogf = logListen.Logf
|
||||
|
||||
@@ -17,8 +17,8 @@ import (
|
||||
"time"
|
||||
|
||||
"tailscale.com/envknob"
|
||||
"tailscale.com/ipn"
|
||||
"tailscale.com/ipn/ipnstate"
|
||||
"tailscale.com/logtail/backoff"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tka"
|
||||
"tailscale.com/types/key"
|
||||
@@ -26,36 +26,83 @@ import (
|
||||
"tailscale.com/types/tkatype"
|
||||
)
|
||||
|
||||
var networkLockAvailable = envknob.RegisterBool("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
|
||||
}
|
||||
|
||||
// tkaSyncIfNeededLocked examines TKA info reported from the control plane,
|
||||
// tkaFilterNetmapLocked checks the signatures on each node key, dropping
|
||||
// nodes from the netmap who's signature does not verify.
|
||||
//
|
||||
// b.mu must be held.
|
||||
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.
|
||||
}
|
||||
|
||||
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{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// tkaSyncIfNeeded 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/boostrap to get the genesis AUM, then
|
||||
// ∴ 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/boostrap to read the disablement secret,
|
||||
// ∴ 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 !networkLockAvailable() {
|
||||
// tkaSyncIfNeeded immediately takes b.takeSyncLock which is held throughout,
|
||||
// and may take b.mu as required.
|
||||
func (b *LocalBackend) tkaSyncIfNeeded(nm *netmap.NetworkMap, prefs ipn.PrefsView) error {
|
||||
if !envknob.UseWIPCode() {
|
||||
// If the feature flag is not enabled, pretend we don't exist.
|
||||
return nil
|
||||
}
|
||||
|
||||
b.tkaSyncLock.Lock() // take tkaSyncLock to make this function an exclusive section.
|
||||
defer b.tkaSyncLock.Unlock()
|
||||
b.mu.Lock() // take mu to protect access to synchronized fields.
|
||||
defer b.mu.Unlock()
|
||||
|
||||
ourNodeKey := prefs.Persist().PublicNodeKey()
|
||||
|
||||
isEnabled := b.tka != nil
|
||||
wantEnabled := nm.TKAEnabled
|
||||
if isEnabled != wantEnabled {
|
||||
@@ -66,17 +113,16 @@ func (b *LocalBackend) tkaSyncIfNeededLocked(nm *netmap.NetworkMap) error {
|
||||
|
||||
// Regardless of whether we are moving to disabled or enabled, we
|
||||
// need information from the tka bootstrap endpoint.
|
||||
ourNodeKey := b.prefs.Persist.PrivateNodeKey.Public()
|
||||
b.mu.Unlock()
|
||||
bs, err := b.tkaFetchBootstrap(ourNodeKey, ourHead)
|
||||
b.mu.Lock()
|
||||
if err != nil {
|
||||
return fmt.Errorf("fetching bootstrap: %v", err)
|
||||
return fmt.Errorf("fetching bootstrap: %w", err)
|
||||
}
|
||||
|
||||
if wantEnabled && !isEnabled {
|
||||
if err := b.tkaBootstrapFromGenesisLocked(bs.GenesisAUM); err != nil {
|
||||
return fmt.Errorf("bootstrap: %v", err)
|
||||
return fmt.Errorf("bootstrap: %w", err)
|
||||
}
|
||||
isEnabled = true
|
||||
} else if !wantEnabled && isEnabled {
|
||||
@@ -96,7 +142,98 @@ func (b *LocalBackend) tkaSyncIfNeededLocked(nm *netmap.NetworkMap) error {
|
||||
}
|
||||
|
||||
if isEnabled && b.tka.authority.Head() != nm.TKAHead {
|
||||
// TODO(tom): Implement sync
|
||||
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.
|
||||
//
|
||||
// b.mu must be held.
|
||||
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
|
||||
@@ -113,8 +250,8 @@ func (b *LocalBackend) chonkPath() string {
|
||||
//
|
||||
// b.mu must be held.
|
||||
func (b *LocalBackend) tkaBootstrapFromGenesisLocked(g tkatype.MarshaledAUM) error {
|
||||
if !b.CanSupportNetworkLock() {
|
||||
return errors.New("network lock not supported in this configuration")
|
||||
if err := b.CanSupportNetworkLock(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var genesis tka.AUM
|
||||
@@ -143,26 +280,34 @@ func (b *LocalBackend) tkaBootstrapFromGenesisLocked(g tkatype.MarshaledAUM) err
|
||||
return nil
|
||||
}
|
||||
|
||||
// CanSupportNetworkLock returns true if tailscaled is able to operate
|
||||
// CanSupportNetworkLock returns nil 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
|
||||
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.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
|
||||
if b.tka != nil {
|
||||
// If the TKA is being used, it is supported.
|
||||
return nil
|
||||
}
|
||||
return false
|
||||
|
||||
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,
|
||||
@@ -191,20 +336,14 @@ 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 !networkLockAvailable() {
|
||||
return errors.New("this is an experimental feature in your version of tailscale - Please upgrade to the latest to use this.")
|
||||
}
|
||||
if !b.CanSupportNetworkLock() {
|
||||
return errors.New("network-lock is not supported in this configuration. Did you supply a --statedir?")
|
||||
if err := b.CanSupportNetworkLock(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var ourNodeKey key.NodePublic
|
||||
b.mu.Lock()
|
||||
if b.prefs != nil {
|
||||
ourNodeKey = b.prefs.Persist.PrivateNodeKey.Public()
|
||||
if b.prefs.Valid() {
|
||||
ourNodeKey = b.prefs.Persist().PublicNodeKey()
|
||||
}
|
||||
b.mu.Unlock()
|
||||
if ourNodeKey.IsZero() {
|
||||
@@ -255,6 +394,87 @@ func (b *LocalBackend) NetworkLockInit(keys []tka.Key) error {
|
||||
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().PublicNodeKey()
|
||||
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 {
|
||||
@@ -286,34 +506,27 @@ func (b *LocalBackend) tkaInitBegin(ourNodeKey key.NodePublic, aum tka.AUM) (*ta
|
||||
|
||||
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(ourNodeKey key.NodePublic, nks map[tailcfg.NodeID]tkatype.MarshaledSignature) (*tailcfg.TKAInitFinishResponse, error) {
|
||||
@@ -328,34 +541,28 @@ func (b *LocalBackend) tkaInitFinish(ourNodeKey key.NodePublic, nks map[tailcfg.
|
||||
|
||||
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
|
||||
@@ -405,3 +612,107 @@ func (b *LocalBackend) tkaFetchBootstrap(ourNodeKey key.NodePublic, head tka.AUM
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -15,7 +15,9 @@ import (
|
||||
"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"
|
||||
@@ -23,6 +25,7 @@ import (
|
||||
"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 {
|
||||
@@ -49,8 +52,6 @@ func fakeControlClient(t *testing.T, c *http.Client) *controlclient.Auto {
|
||||
return cc
|
||||
}
|
||||
|
||||
// NOTE: URLs must have a https scheme and example.com domain to work with the underlying
|
||||
// httptest plumbing, despite the domain being unused in the actual noise request transport.
|
||||
func fakeNoiseServer(t *testing.T, handler http.HandlerFunc) (*httptest.Server, *http.Client) {
|
||||
ts := httptest.NewUnstartedServer(handler)
|
||||
ts.StartTLS()
|
||||
@@ -63,7 +64,7 @@ func fakeNoiseServer(t *testing.T, handler http.HandlerFunc) (*httptest.Server,
|
||||
}
|
||||
|
||||
func TestTKAEnablementFlow(t *testing.T) {
|
||||
networkLockAvailable = func() bool { return true } // Enable the feature flag
|
||||
envknob.Setenv("TAILSCALE_USE_WIP_CODE", "1")
|
||||
nodePriv := key.NewNode()
|
||||
|
||||
// Make a fake TKA authority, getting a usable genesis AUM which
|
||||
@@ -104,6 +105,9 @@ func TestTKAEnablementFlow(t *testing.T) {
|
||||
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)
|
||||
@@ -118,17 +122,15 @@ func TestTKAEnablementFlow(t *testing.T) {
|
||||
cc: cc,
|
||||
ccAuto: cc,
|
||||
logf: t.Logf,
|
||||
prefs: &ipn.Prefs{
|
||||
prefs: (&ipn.Prefs{
|
||||
Persist: &persist.Persist{PrivateNodeKey: nodePriv},
|
||||
},
|
||||
}).View(),
|
||||
}
|
||||
|
||||
b.mu.Lock()
|
||||
err = b.tkaSyncIfNeededLocked(&netmap.NetworkMap{
|
||||
err = b.tkaSyncIfNeeded(&netmap.NetworkMap{
|
||||
TKAEnabled: true,
|
||||
TKAHead: tka.AUMHash{},
|
||||
})
|
||||
b.mu.Unlock()
|
||||
TKAHead: a1.Head(),
|
||||
}, b.prefs)
|
||||
if err != nil {
|
||||
t.Errorf("tkaSyncIfNeededLocked() failed: %v", err)
|
||||
}
|
||||
@@ -141,7 +143,7 @@ func TestTKAEnablementFlow(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestTKADisablementFlow(t *testing.T) {
|
||||
networkLockAvailable = func() bool { return true } // Enable the feature flag
|
||||
envknob.Setenv("TAILSCALE_USE_WIP_CODE", "1")
|
||||
temp := t.TempDir()
|
||||
os.Mkdir(filepath.Join(temp, "tka"), 0755)
|
||||
nodePriv := key.NewNode()
|
||||
@@ -217,19 +219,17 @@ func TestTKADisablementFlow(t *testing.T) {
|
||||
authority: authority,
|
||||
storage: chonk,
|
||||
},
|
||||
prefs: &ipn.Prefs{
|
||||
prefs: (&ipn.Prefs{
|
||||
Persist: &persist.Persist{PrivateNodeKey: nodePriv},
|
||||
},
|
||||
}).View(),
|
||||
}
|
||||
|
||||
// Test that the wrong disablement secret does not shut down the authority.
|
||||
returnWrongSecret = true
|
||||
b.mu.Lock()
|
||||
err = b.tkaSyncIfNeededLocked(&netmap.NetworkMap{
|
||||
err = b.tkaSyncIfNeeded(&netmap.NetworkMap{
|
||||
TKAEnabled: false,
|
||||
TKAHead: authority.Head(),
|
||||
})
|
||||
b.mu.Unlock()
|
||||
}, b.prefs)
|
||||
if err != nil {
|
||||
t.Errorf("tkaSyncIfNeededLocked() failed: %v", err)
|
||||
}
|
||||
@@ -239,12 +239,10 @@ func TestTKADisablementFlow(t *testing.T) {
|
||||
|
||||
// Test the correct disablement secret shuts down the authority.
|
||||
returnWrongSecret = false
|
||||
b.mu.Lock()
|
||||
err = b.tkaSyncIfNeededLocked(&netmap.NetworkMap{
|
||||
err = b.tkaSyncIfNeeded(&netmap.NetworkMap{
|
||||
TKAEnabled: false,
|
||||
TKAHead: authority.Head(),
|
||||
})
|
||||
b.mu.Unlock()
|
||||
}, b.prefs)
|
||||
if err != nil {
|
||||
t.Errorf("tkaSyncIfNeededLocked() failed: %v", err)
|
||||
}
|
||||
@@ -256,3 +254,285 @@ func TestTKADisablementFlow(t *testing.T) {
|
||||
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},
|
||||
}).View(),
|
||||
}
|
||||
|
||||
// Finally, lets trigger a sync.
|
||||
err = b.tkaSyncIfNeeded(&netmap.NetworkMap{
|
||||
TKAEnabled: true,
|
||||
TKAHead: controlAuthority.Head(),
|
||||
}, b.prefs)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ import (
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/kortschak/wol"
|
||||
"golang.org/x/exp/slices"
|
||||
"golang.org/x/net/dns/dnsmessage"
|
||||
"tailscale.com/client/tailscale/apitype"
|
||||
"tailscale.com/health"
|
||||
@@ -58,7 +59,6 @@ var addH2C func(*http.Server)
|
||||
type peerAPIServer struct {
|
||||
b *LocalBackend
|
||||
rootDir string // empty means file receiving unavailable
|
||||
selfNode *tailcfg.Node
|
||||
knownEmpty atomic.Bool
|
||||
resolver *resolver.Resolver
|
||||
|
||||
@@ -79,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"
|
||||
|
||||
@@ -512,10 +512,17 @@ func (pln *peerAPIListener) ServeConn(src netip.AddrPort, c net.Conn) {
|
||||
c.Close()
|
||||
return
|
||||
}
|
||||
nm := pln.lb.NetMap()
|
||||
if nm == nil || nm.SelfNode == nil {
|
||||
logf("peerapi: no netmap")
|
||||
c.Close()
|
||||
return
|
||||
}
|
||||
h := &peerAPIHandler{
|
||||
ps: pln.ps,
|
||||
isSelf: pln.ps.selfNode.User == peerNode.User,
|
||||
isSelf: nm.SelfNode.User == peerNode.User,
|
||||
remoteAddr: src,
|
||||
selfNode: nm.SelfNode,
|
||||
peerNode: peerNode,
|
||||
peerUser: peerUser,
|
||||
}
|
||||
@@ -533,6 +540,7 @@ type peerAPIHandler struct {
|
||||
ps *peerAPIServer
|
||||
remoteAddr netip.AddrPort
|
||||
isSelf bool // whether peerNode is owned by same user as this node
|
||||
selfNode *tailcfg.Node // this node; always non-nil
|
||||
peerNode *tailcfg.Node // peerNode is who's making the request
|
||||
peerUser tailcfg.UserProfile // profile of peerNode
|
||||
}
|
||||
@@ -541,7 +549,37 @@ func (h *peerAPIHandler) logf(format string, a ...any) {
|
||||
h.ps.b.logf("peerapi: "+format, a...)
|
||||
}
|
||||
|
||||
func (h *peerAPIHandler) validateHost(r *http.Request) error {
|
||||
if r.Host == "peer" {
|
||||
return nil
|
||||
}
|
||||
ap, err := netip.ParseAddrPort(r.Host)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
hostIPPfx := netip.PrefixFrom(ap.Addr(), ap.Addr().BitLen())
|
||||
if !slices.Contains(h.selfNode.Addresses, hostIPPfx) {
|
||||
return fmt.Errorf("%v not found in self addresses", hostIPPfx)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *peerAPIHandler) validatePeerAPIRequest(r *http.Request) error {
|
||||
if r.Referer() != "" {
|
||||
return errors.New("unexpected Referer")
|
||||
}
|
||||
if r.Header.Get("Origin") != "" {
|
||||
return errors.New("unexpected Origin")
|
||||
}
|
||||
return h.validateHost(r)
|
||||
}
|
||||
|
||||
func (h *peerAPIHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if err := h.validatePeerAPIRequest(r); err != nil {
|
||||
h.logf("invalid request from %v: %v", h.remoteAddr, err)
|
||||
http.Error(w, "invalid peerapi request", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
if strings.HasPrefix(r.URL.Path, "/v0/put/") {
|
||||
h.handlePeerPut(w, r)
|
||||
return
|
||||
@@ -686,6 +724,10 @@ func (h *peerAPIHandler) canPutFile() bool {
|
||||
// canDebug reports whether h can debug this node (goroutines, metrics,
|
||||
// magicsock internal state, etc).
|
||||
func (h *peerAPIHandler) canDebug() bool {
|
||||
if !slices.Contains(h.selfNode.Capabilities, tailcfg.CapabilityDebug) {
|
||||
// This node does not expose debug info.
|
||||
return false
|
||||
}
|
||||
return h.isSelf || h.peerHasCap(tailcfg.CapabilityDebugPeer)
|
||||
}
|
||||
|
||||
@@ -1184,7 +1226,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
|
||||
|
||||
@@ -24,6 +24,7 @@ import (
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tstest"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/netmap"
|
||||
"tailscale.com/wgengine"
|
||||
"tailscale.com/wgengine/filter"
|
||||
)
|
||||
@@ -106,10 +107,12 @@ func hexAll(v string) string {
|
||||
}
|
||||
|
||||
func TestHandlePeerAPI(t *testing.T) {
|
||||
const nodeFQDN = "self-node.tail-scale.ts.net."
|
||||
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
|
||||
debugCap bool // self node has debug capability
|
||||
omitRoot bool // don't configure
|
||||
req *http.Request
|
||||
checks []check
|
||||
@@ -137,15 +140,24 @@ func TestHandlePeerAPI(t *testing.T) {
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "peer_api_goroutines_deny",
|
||||
isSelf: false,
|
||||
req: httptest.NewRequest("GET", "/v0/goroutines", nil),
|
||||
checks: checks(httpStatus(403)),
|
||||
name: "goroutines/deny-self-no-cap",
|
||||
isSelf: true,
|
||||
debugCap: false,
|
||||
req: httptest.NewRequest("GET", "/v0/goroutines", nil),
|
||||
checks: checks(httpStatus(403)),
|
||||
},
|
||||
{
|
||||
name: "peer_api_goroutines",
|
||||
isSelf: true,
|
||||
req: httptest.NewRequest("GET", "/v0/goroutines", nil),
|
||||
name: "goroutines/deny-nonself",
|
||||
isSelf: false,
|
||||
debugCap: true,
|
||||
req: httptest.NewRequest("GET", "/v0/goroutines", nil),
|
||||
checks: checks(httpStatus(403)),
|
||||
},
|
||||
{
|
||||
name: "goroutines/accept-self",
|
||||
isSelf: true,
|
||||
debugCap: true,
|
||||
req: httptest.NewRequest("GET", "/v0/goroutines", nil),
|
||||
checks: checks(
|
||||
httpStatus(200),
|
||||
bodyContains("ServeHTTP"),
|
||||
@@ -400,16 +412,53 @@ func TestHandlePeerAPI(t *testing.T) {
|
||||
bodyContains("bad filename"),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "host-val/bad-ip",
|
||||
isSelf: true,
|
||||
debugCap: true,
|
||||
req: httptest.NewRequest("GET", "http://12.23.45.66:1234/v0/env", nil),
|
||||
checks: checks(
|
||||
httpStatus(403),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "host-val/no-port",
|
||||
isSelf: true,
|
||||
debugCap: true,
|
||||
req: httptest.NewRequest("GET", "http://100.100.100.101/v0/env", nil),
|
||||
checks: checks(
|
||||
httpStatus(403),
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "host-val/peer",
|
||||
isSelf: true,
|
||||
debugCap: true,
|
||||
req: httptest.NewRequest("GET", "http://peer/v0/env", nil),
|
||||
checks: checks(
|
||||
httpStatus(200),
|
||||
),
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
selfNode := &tailcfg.Node{
|
||||
Addresses: []netip.Prefix{
|
||||
netip.MustParsePrefix("100.100.100.101/32"),
|
||||
},
|
||||
}
|
||||
if tt.debugCap {
|
||||
selfNode.Capabilities = append(selfNode.Capabilities, tailcfg.CapabilityDebug)
|
||||
}
|
||||
var e peerAPITestEnv
|
||||
lb := &LocalBackend{
|
||||
logf: e.logBuf.Logf,
|
||||
capFileSharing: tt.capSharing,
|
||||
netMap: &netmap.NetworkMap{SelfNode: selfNode},
|
||||
}
|
||||
e.ph = &peerAPIHandler{
|
||||
isSelf: tt.isSelf,
|
||||
isSelf: tt.isSelf,
|
||||
selfNode: selfNode,
|
||||
peerNode: &tailcfg.Node{
|
||||
ComputedName: "some-peer-name",
|
||||
},
|
||||
@@ -423,6 +472,9 @@ func TestHandlePeerAPI(t *testing.T) {
|
||||
e.ph.ps.rootDir = rootDir
|
||||
}
|
||||
e.rr = httptest.NewRecorder()
|
||||
if tt.req.Host == "example.com" {
|
||||
tt.req.Host = "100.100.100.101:12345"
|
||||
}
|
||||
e.ph.ServeHTTP(e.rr, tt.req)
|
||||
for _, f := range tt.checks {
|
||||
f(t, &e)
|
||||
@@ -460,12 +512,15 @@ func TestFileDeleteRace(t *testing.T) {
|
||||
peerNode: &tailcfg.Node{
|
||||
ComputedName: "some-peer-name",
|
||||
},
|
||||
selfNode: &tailcfg.Node{
|
||||
Addresses: []netip.Prefix{netip.MustParsePrefix("100.100.100.101/32")},
|
||||
},
|
||||
ps: ps,
|
||||
}
|
||||
buf := make([]byte, 2<<20)
|
||||
for i := 0; i < 30; i++ {
|
||||
rr := httptest.NewRecorder()
|
||||
ph.ServeHTTP(rr, httptest.NewRequest("PUT", "/v0/put/foo.txt", bytes.NewReader(buf[:rand.Intn(len(buf))])))
|
||||
ph.ServeHTTP(rr, httptest.NewRequest("PUT", "http://100.100.100.101:123/v0/put/foo.txt", bytes.NewReader(buf[:rand.Intn(len(buf))])))
|
||||
if res := rr.Result(); res.StatusCode != 200 {
|
||||
t.Fatal(res.Status)
|
||||
}
|
||||
@@ -593,12 +648,12 @@ func TestPeerAPIReplyToDNSQueries(t *testing.T) {
|
||||
if h.ps.b.OfferingExitNode() {
|
||||
t.Fatal("unexpectedly offering exit node")
|
||||
}
|
||||
h.ps.b.prefs = &ipn.Prefs{
|
||||
h.ps.b.prefs = (&ipn.Prefs{
|
||||
AdvertiseRoutes: []netip.Prefix{
|
||||
netip.MustParsePrefix("0.0.0.0/0"),
|
||||
netip.MustParsePrefix("::/0"),
|
||||
},
|
||||
}
|
||||
}).View()
|
||||
if !h.ps.b.OfferingExitNode() {
|
||||
t.Fatal("unexpectedly not offering exit node")
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ func (nt *notifyThrottler) expect(count int) {
|
||||
|
||||
// put adds one notification into the throttler's queue.
|
||||
func (nt *notifyThrottler) put(n ipn.Notify) {
|
||||
nt.t.Helper()
|
||||
nt.mu.Lock()
|
||||
ch := nt.ch
|
||||
nt.mu.Unlock()
|
||||
@@ -315,7 +316,7 @@ func TestStateMachine(t *testing.T) {
|
||||
return
|
||||
}
|
||||
if n.State != nil ||
|
||||
n.Prefs != nil ||
|
||||
(n.Prefs != nil && n.Prefs.Valid()) ||
|
||||
n.BrowseToURL != nil ||
|
||||
n.LoginFinished != nil {
|
||||
logf("\n%v\n\n", n)
|
||||
@@ -344,12 +345,12 @@ func TestStateMachine(t *testing.T) {
|
||||
cc.assertCalls()
|
||||
c.Assert(nn[0].Prefs, qt.IsNotNil)
|
||||
c.Assert(nn[1].State, qt.IsNotNil)
|
||||
prefs := *nn[0].Prefs
|
||||
prefs := nn[0].Prefs
|
||||
// Note: a totally fresh system has Prefs.LoggedOut=false by
|
||||
// default. We are logged out, but not because the user asked
|
||||
// for it, so it doesn't count as Prefs.LoggedOut==true.
|
||||
c.Assert(nn[0].Prefs.LoggedOut, qt.IsFalse)
|
||||
c.Assert(prefs.WantRunning, qt.IsFalse)
|
||||
c.Assert(prefs.LoggedOut(), qt.IsFalse)
|
||||
c.Assert(prefs.WantRunning(), qt.IsFalse)
|
||||
c.Assert(ipn.NeedsLogin, qt.Equals, *nn[1].State)
|
||||
c.Assert(ipn.NeedsLogin, qt.Equals, b.State())
|
||||
}
|
||||
@@ -369,8 +370,8 @@ func TestStateMachine(t *testing.T) {
|
||||
cc.assertCalls()
|
||||
c.Assert(nn[0].Prefs, qt.IsNotNil)
|
||||
c.Assert(nn[1].State, qt.IsNotNil)
|
||||
c.Assert(nn[0].Prefs.LoggedOut, qt.IsFalse)
|
||||
c.Assert(nn[0].Prefs.WantRunning, qt.IsFalse)
|
||||
c.Assert(nn[0].Prefs.LoggedOut(), qt.IsFalse)
|
||||
c.Assert(nn[0].Prefs.WantRunning(), qt.IsFalse)
|
||||
c.Assert(ipn.NeedsLogin, qt.Equals, *nn[1].State)
|
||||
c.Assert(ipn.NeedsLogin, qt.Equals, b.State())
|
||||
}
|
||||
@@ -395,7 +396,7 @@ func TestStateMachine(t *testing.T) {
|
||||
// the user needs to visit a login URL.
|
||||
t.Logf("\n\nLogin (url response)")
|
||||
notifies.expect(1)
|
||||
url1 := "http://localhost:1/1"
|
||||
url1 := "https://localhost:1/1"
|
||||
cc.send(nil, url1, false, nil)
|
||||
{
|
||||
cc.assertCalls("unpause")
|
||||
@@ -406,8 +407,8 @@ func TestStateMachine(t *testing.T) {
|
||||
nn := notifies.drain(1)
|
||||
|
||||
c.Assert(nn[0].Prefs, qt.IsNotNil)
|
||||
c.Assert(nn[0].Prefs.LoggedOut, qt.IsFalse)
|
||||
c.Assert(nn[0].Prefs.WantRunning, qt.IsFalse)
|
||||
c.Assert(nn[0].Prefs.LoggedOut(), qt.IsFalse)
|
||||
c.Assert(nn[0].Prefs.WantRunning(), qt.IsFalse)
|
||||
}
|
||||
|
||||
// Now we'll try an interactive login.
|
||||
@@ -441,7 +442,7 @@ func TestStateMachine(t *testing.T) {
|
||||
// Provide a new interactive login URL.
|
||||
t.Logf("\n\nLogin2 (url response)")
|
||||
notifies.expect(1)
|
||||
url2 := "http://localhost:1/2"
|
||||
url2 := "https://localhost:1/2"
|
||||
cc.send(nil, url2, false, nil)
|
||||
{
|
||||
cc.assertCalls("unpause", "unpause")
|
||||
@@ -476,7 +477,7 @@ func TestStateMachine(t *testing.T) {
|
||||
c.Assert(nn[0].LoginFinished, qt.IsNotNil)
|
||||
c.Assert(nn[1].Prefs, qt.IsNotNil)
|
||||
c.Assert(nn[2].State, qt.IsNotNil)
|
||||
c.Assert(nn[1].Prefs.Persist.LoginName, qt.Equals, "user1")
|
||||
c.Assert(nn[1].Prefs.Persist().LoginName, qt.Equals, "user1")
|
||||
c.Assert(ipn.NeedsMachineAuth, qt.Equals, *nn[2].State)
|
||||
}
|
||||
|
||||
@@ -576,8 +577,8 @@ func TestStateMachine(t *testing.T) {
|
||||
c.Assert(nn[0].State, qt.IsNotNil)
|
||||
c.Assert(nn[1].Prefs, qt.IsNotNil)
|
||||
c.Assert(ipn.Stopped, qt.Equals, *nn[0].State)
|
||||
c.Assert(nn[1].Prefs.LoggedOut, qt.IsTrue)
|
||||
c.Assert(nn[1].Prefs.WantRunning, qt.IsFalse)
|
||||
c.Assert(nn[1].Prefs.LoggedOut(), qt.IsTrue)
|
||||
c.Assert(nn[1].Prefs.WantRunning(), qt.IsFalse)
|
||||
c.Assert(ipn.Stopped, qt.Equals, b.State())
|
||||
c.Assert(store.sawWrite(), qt.IsTrue)
|
||||
}
|
||||
@@ -592,8 +593,8 @@ func TestStateMachine(t *testing.T) {
|
||||
cc.assertCalls("unpause", "unpause")
|
||||
c.Assert(nn[0].State, qt.IsNotNil)
|
||||
c.Assert(ipn.NeedsLogin, qt.Equals, *nn[0].State)
|
||||
c.Assert(b.Prefs().LoggedOut, qt.IsTrue)
|
||||
c.Assert(b.Prefs().WantRunning, qt.IsFalse)
|
||||
c.Assert(b.Prefs().LoggedOut(), qt.IsTrue)
|
||||
c.Assert(b.Prefs().WantRunning(), qt.IsFalse)
|
||||
c.Assert(ipn.NeedsLogin, qt.Equals, b.State())
|
||||
}
|
||||
|
||||
@@ -607,8 +608,8 @@ func TestStateMachine(t *testing.T) {
|
||||
// still logged out. So it shouldn't call it again.
|
||||
cc.assertCalls("StartLogout", "unpause")
|
||||
cc.assertCalls()
|
||||
c.Assert(b.Prefs().LoggedOut, qt.IsTrue)
|
||||
c.Assert(b.Prefs().WantRunning, qt.IsFalse)
|
||||
c.Assert(b.Prefs().LoggedOut(), qt.IsTrue)
|
||||
c.Assert(b.Prefs().WantRunning(), qt.IsFalse)
|
||||
c.Assert(ipn.NeedsLogin, qt.Equals, b.State())
|
||||
}
|
||||
|
||||
@@ -620,8 +621,8 @@ func TestStateMachine(t *testing.T) {
|
||||
{
|
||||
notifies.drain(0)
|
||||
cc.assertCalls("unpause", "unpause")
|
||||
c.Assert(b.Prefs().LoggedOut, qt.IsTrue)
|
||||
c.Assert(b.Prefs().WantRunning, qt.IsFalse)
|
||||
c.Assert(b.Prefs().LoggedOut(), qt.IsTrue)
|
||||
c.Assert(b.Prefs().WantRunning(), qt.IsFalse)
|
||||
c.Assert(ipn.NeedsLogin, qt.Equals, b.State())
|
||||
}
|
||||
|
||||
@@ -634,8 +635,8 @@ func TestStateMachine(t *testing.T) {
|
||||
{
|
||||
notifies.drain(0)
|
||||
cc.assertCalls("Logout", "unpause")
|
||||
c.Assert(b.Prefs().LoggedOut, qt.IsTrue)
|
||||
c.Assert(b.Prefs().WantRunning, qt.IsFalse)
|
||||
c.Assert(b.Prefs().LoggedOut(), qt.IsTrue)
|
||||
c.Assert(b.Prefs().WantRunning(), qt.IsFalse)
|
||||
c.Assert(ipn.NeedsLogin, qt.Equals, b.State())
|
||||
}
|
||||
|
||||
@@ -647,8 +648,8 @@ func TestStateMachine(t *testing.T) {
|
||||
{
|
||||
notifies.drain(0)
|
||||
cc.assertCalls("unpause", "unpause")
|
||||
c.Assert(b.Prefs().LoggedOut, qt.IsTrue)
|
||||
c.Assert(b.Prefs().WantRunning, qt.IsFalse)
|
||||
c.Assert(b.Prefs().LoggedOut(), qt.IsTrue)
|
||||
c.Assert(b.Prefs().WantRunning(), qt.IsFalse)
|
||||
c.Assert(ipn.NeedsLogin, qt.Equals, b.State())
|
||||
}
|
||||
|
||||
@@ -672,8 +673,8 @@ func TestStateMachine(t *testing.T) {
|
||||
cc.assertCalls()
|
||||
c.Assert(nn[0].Prefs, qt.IsNotNil)
|
||||
c.Assert(nn[1].State, qt.IsNotNil)
|
||||
c.Assert(nn[0].Prefs.LoggedOut, qt.IsTrue)
|
||||
c.Assert(nn[0].Prefs.WantRunning, qt.IsFalse)
|
||||
c.Assert(nn[0].Prefs.LoggedOut(), qt.IsTrue)
|
||||
c.Assert(nn[0].Prefs.WantRunning(), qt.IsFalse)
|
||||
c.Assert(ipn.NeedsLogin, qt.Equals, *nn[1].State)
|
||||
c.Assert(ipn.NeedsLogin, qt.Equals, b.State())
|
||||
}
|
||||
@@ -696,9 +697,9 @@ func TestStateMachine(t *testing.T) {
|
||||
c.Assert(nn[1].Prefs, qt.IsNotNil)
|
||||
c.Assert(nn[2].State, qt.IsNotNil)
|
||||
// Prefs after finishing the login, so LoginName updated.
|
||||
c.Assert(nn[1].Prefs.Persist.LoginName, qt.Equals, "user2")
|
||||
c.Assert(nn[1].Prefs.LoggedOut, qt.IsFalse)
|
||||
c.Assert(nn[1].Prefs.WantRunning, qt.IsTrue)
|
||||
c.Assert(nn[1].Prefs.Persist().LoginName, qt.Equals, "user2")
|
||||
c.Assert(nn[1].Prefs.LoggedOut(), qt.IsFalse)
|
||||
c.Assert(nn[1].Prefs.WantRunning(), qt.IsTrue)
|
||||
c.Assert(ipn.Starting, qt.Equals, *nn[2].State)
|
||||
}
|
||||
|
||||
@@ -716,7 +717,7 @@ func TestStateMachine(t *testing.T) {
|
||||
c.Assert(nn[0].State, qt.IsNotNil)
|
||||
c.Assert(nn[1].Prefs, qt.IsNotNil)
|
||||
c.Assert(ipn.Stopped, qt.Equals, *nn[0].State)
|
||||
c.Assert(nn[1].Prefs.LoggedOut, qt.IsFalse)
|
||||
c.Assert(nn[1].Prefs.LoggedOut(), qt.IsFalse)
|
||||
}
|
||||
|
||||
// One more restart, this time with a valid key, but WantRunning=false.
|
||||
@@ -734,8 +735,8 @@ func TestStateMachine(t *testing.T) {
|
||||
cc.assertCalls("Shutdown", "unpause", "New", "Login", "unpause")
|
||||
c.Assert(nn[0].Prefs, qt.IsNotNil)
|
||||
c.Assert(nn[1].State, qt.IsNotNil)
|
||||
c.Assert(nn[0].Prefs.WantRunning, qt.IsFalse)
|
||||
c.Assert(nn[0].Prefs.LoggedOut, qt.IsFalse)
|
||||
c.Assert(nn[0].Prefs.WantRunning(), qt.IsFalse)
|
||||
c.Assert(nn[0].Prefs.LoggedOut(), qt.IsFalse)
|
||||
c.Assert(ipn.Stopped, qt.Equals, *nn[1].State)
|
||||
}
|
||||
|
||||
@@ -796,7 +797,7 @@ func TestStateMachine(t *testing.T) {
|
||||
t.Logf("\n\nLoginDifferent")
|
||||
notifies.expect(1)
|
||||
b.StartLoginInteractive()
|
||||
url3 := "http://localhost:1/3"
|
||||
url3 := "https://localhost:1/3"
|
||||
cc.send(nil, url3, false, nil)
|
||||
{
|
||||
nn := notifies.drain(1)
|
||||
@@ -834,9 +835,9 @@ func TestStateMachine(t *testing.T) {
|
||||
c.Assert(nn[1].Prefs, qt.IsNotNil)
|
||||
c.Assert(nn[2].State, qt.IsNotNil)
|
||||
// Prefs after finishing the login, so LoginName updated.
|
||||
c.Assert(nn[1].Prefs.Persist.LoginName, qt.Equals, "user3")
|
||||
c.Assert(nn[1].Prefs.LoggedOut, qt.IsFalse)
|
||||
c.Assert(nn[1].Prefs.WantRunning, qt.IsTrue)
|
||||
c.Assert(nn[1].Prefs.Persist().LoginName, qt.Equals, "user3")
|
||||
c.Assert(nn[1].Prefs.LoggedOut(), qt.IsFalse)
|
||||
c.Assert(nn[1].Prefs.WantRunning(), qt.IsTrue)
|
||||
c.Assert(ipn.Starting, qt.Equals, *nn[2].State)
|
||||
}
|
||||
|
||||
@@ -853,8 +854,8 @@ func TestStateMachine(t *testing.T) {
|
||||
nn := notifies.drain(1)
|
||||
cc.assertCalls()
|
||||
c.Assert(nn[0].Prefs, qt.IsNotNil)
|
||||
c.Assert(nn[0].Prefs.LoggedOut, qt.IsFalse)
|
||||
c.Assert(nn[0].Prefs.WantRunning, qt.IsTrue)
|
||||
c.Assert(nn[0].Prefs.LoggedOut(), qt.IsFalse)
|
||||
c.Assert(nn[0].Prefs.WantRunning(), qt.IsTrue)
|
||||
c.Assert(ipn.NoState, qt.Equals, b.State())
|
||||
}
|
||||
|
||||
@@ -918,6 +919,60 @@ func TestStateMachine(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestEditPrefsHasNoKeys(t *testing.T) {
|
||||
logf := t.Logf
|
||||
store := new(testStateStorage)
|
||||
e, err := wgengine.NewFakeUserspaceEngine(logf, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("NewFakeUserspaceEngine: %v", err)
|
||||
}
|
||||
t.Cleanup(e.Close)
|
||||
|
||||
b, err := NewLocalBackend(logf, "logid", store, nil, e, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("NewLocalBackend: %v", err)
|
||||
}
|
||||
b.hostinfo = &tailcfg.Hostinfo{OS: "testos"}
|
||||
b.prefs = (&ipn.Prefs{
|
||||
Persist: &persist.Persist{
|
||||
PrivateNodeKey: key.NewNode(),
|
||||
OldPrivateNodeKey: key.NewNode(),
|
||||
|
||||
LegacyFrontendPrivateMachineKey: key.NewMachine(),
|
||||
},
|
||||
}).View()
|
||||
if b.prefs.Persist().PrivateNodeKey.IsZero() {
|
||||
t.Fatalf("PrivateNodeKey not set")
|
||||
}
|
||||
p, err := b.EditPrefs(&ipn.MaskedPrefs{
|
||||
Prefs: ipn.Prefs{
|
||||
Hostname: "foo",
|
||||
},
|
||||
HostnameSet: true,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("EditPrefs: %v", err)
|
||||
}
|
||||
if p.Hostname() != "foo" {
|
||||
t.Errorf("Hostname = %q; want foo", p.Hostname())
|
||||
}
|
||||
|
||||
// Test that we can't see the PrivateNodeKey.
|
||||
if !p.Persist().PrivateNodeKey.IsZero() {
|
||||
t.Errorf("PrivateNodeKey = %v; want zero", p.Persist().PrivateNodeKey)
|
||||
}
|
||||
|
||||
// Test that we can't see the PrivateNodeKey.
|
||||
if !p.Persist().OldPrivateNodeKey.IsZero() {
|
||||
t.Errorf("OldPrivateNodeKey = %v; want zero", p.Persist().OldPrivateNodeKey)
|
||||
}
|
||||
|
||||
// Test that we can't see the PrivateNodeKey.
|
||||
if !p.Persist().LegacyFrontendPrivateMachineKey.IsZero() {
|
||||
t.Errorf("LegacyFrontendPrivateMachineKey = %v; want zero", p.Persist().LegacyFrontendPrivateMachineKey)
|
||||
}
|
||||
}
|
||||
|
||||
type testStateStorage struct {
|
||||
mem mem.Store
|
||||
written atomic.Bool
|
||||
|
||||
@@ -27,6 +27,7 @@ import (
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
"unicode"
|
||||
|
||||
"go4.org/mem"
|
||||
"inet.af/peercred"
|
||||
@@ -57,7 +58,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
|
||||
@@ -1072,7 +1073,20 @@ func (s *Server) localhostHandler(ci connIdentity) http.Handler {
|
||||
})
|
||||
}
|
||||
|
||||
// ServeHTMLStatus serves an HTML status page at http://localhost:41112/ for
|
||||
// Windows and via $DEBUG_LISTENER/debug/ipn when tailscaled's --debug flag
|
||||
// is used to run a debug server.
|
||||
func (s *Server) ServeHTMLStatus(w http.ResponseWriter, r *http.Request) {
|
||||
// As this is only meant for debug, verify there's no DNS name being used to
|
||||
// access this.
|
||||
if !strings.HasPrefix(r.Host, "localhost:") && strings.IndexFunc(r.Host, unicode.IsLetter) != -1 {
|
||||
http.Error(w, "invalid host", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Security-Policy", `default-src 'none'; frame-ancestors 'none'; script-src 'none'; script-src-elem 'none'; script-src-attr 'none'`)
|
||||
w.Header().Set("X-Frame-Options", "DENY")
|
||||
w.Header().Set("X-Content-Type-Options", "nosniff")
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
st := s.b.Status()
|
||||
// TODO(bradfitz): add LogID and opts to st?
|
||||
|
||||
@@ -26,18 +26,61 @@ 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"
|
||||
"tailscale.com/net/netutil"
|
||||
"tailscale.com/safesocket"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tka"
|
||||
"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)
|
||||
@@ -87,7 +130,14 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "server has no local backend", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if r.Referer() != "" || r.Header.Get("Origin") != "" || !validHost(r.Host) {
|
||||
http.Error(w, "invalid localapi request", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Tailscale-Version", version.Long)
|
||||
w.Header().Set("Content-Security-Policy", `default-src 'none'; frame-ancestors 'none'; script-src 'none'; script-src-elem 'none'; script-src-attr 'none'`)
|
||||
w.Header().Set("X-Frame-Options", "DENY")
|
||||
w.Header().Set("X-Content-Type-Options", "nosniff")
|
||||
if h.RequiredPassword != "" {
|
||||
_, pass, ok := r.BasicAuth()
|
||||
if !ok {
|
||||
@@ -99,68 +149,74 @@ 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
|
||||
}
|
||||
|
||||
// validHost reports whether h is a valid Host header value for a LocalAPI request.
|
||||
func validHost(h string) bool {
|
||||
// The client code sends a hostname of "local-tailscaled.sock".
|
||||
switch h {
|
||||
case "", apitype.LocalAPIHost:
|
||||
return true
|
||||
}
|
||||
if strings.HasPrefix(r.URL.Path, "/localapi/v0/cert/") {
|
||||
h.serveCert(w, r)
|
||||
return
|
||||
// Allow either localhost or loopback IP hosts.
|
||||
host, portStr, err := net.SplitHostPort(h)
|
||||
if err != nil {
|
||||
return 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)
|
||||
port, err := strconv.ParseUint(portStr, 10, 16)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if runtime.GOOS == "windows" && port != safesocket.WindowsLocalPort {
|
||||
return false
|
||||
}
|
||||
if host == "localhost" {
|
||||
return true
|
||||
}
|
||||
addr, err := netip.ParseAddr(h)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return addr.IsLoopback()
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
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
|
||||
}
|
||||
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.
|
||||
@@ -212,20 +268,86 @@ func (h *Handler) serveBugReport(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "bugreport access denied", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
logMarker := fmt.Sprintf("BUG-%v-%v-%v", h.backendLogID, time.Now().UTC().Format("20060102150405Z"), randHex(8))
|
||||
if envknob.NoLogsNoSupport() {
|
||||
logMarker = "BUG-NO-LOGS-NO-SUPPORT-this-node-has-had-its-logging-disabled"
|
||||
if r.Method != "POST" {
|
||||
http.Error(w, "only POST allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
h.logf("user bugreport: %s", logMarker)
|
||||
if note := r.FormValue("note"); len(note) > 0 {
|
||||
|
||||
logMarker := func() string {
|
||||
return fmt.Sprintf("BUG-%v-%v-%v", h.backendLogID, time.Now().UTC().Format("20060102150405Z"), randHex(8))
|
||||
}
|
||||
if envknob.NoLogsNoSupport() {
|
||||
logMarker = func() string { return "BUG-NO-LOGS-NO-SUPPORT-this-node-has-had-its-logging-disabled" }
|
||||
}
|
||||
|
||||
startMarker := logMarker()
|
||||
h.logf("user bugreport: %s", startMarker)
|
||||
if note := r.URL.Query().Get("note"); len(note) > 0 {
|
||||
h.logf("user bugreport note: %s", note)
|
||||
}
|
||||
if defBool(r.FormValue("diagnose"), false) {
|
||||
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.URL.Query().Get("diagnose"), false) {
|
||||
h.b.Doctor(r.Context(), logger.WithPrefix(h.logf, "diag: "))
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
fmt.Fprintln(w, logMarker)
|
||||
fmt.Fprintln(w, startMarker)
|
||||
|
||||
// Nothing else to do if we're not in record mode; we wrote the marker
|
||||
// above, so we can just finish our response now.
|
||||
if !defBool(r.URL.Query().Get("record"), false) {
|
||||
return
|
||||
}
|
||||
|
||||
until := time.Now().Add(12 * time.Hour)
|
||||
|
||||
var changed map[string]bool
|
||||
for _, component := range []string{"magicsock"} {
|
||||
if h.b.GetComponentDebugLogging(component).IsZero() {
|
||||
if err := h.b.SetComponentDebugLogging(component, until); err != nil {
|
||||
h.logf("bugreport: error setting component %q logging: %v", component, err)
|
||||
continue
|
||||
}
|
||||
|
||||
mak.Set(&changed, component, true)
|
||||
}
|
||||
}
|
||||
defer func() {
|
||||
for component := range changed {
|
||||
h.b.SetComponentDebugLogging(component, time.Time{})
|
||||
}
|
||||
}()
|
||||
|
||||
// NOTE(andrew): if we have anything else we want to do while recording
|
||||
// a bugreport, we can add it here.
|
||||
|
||||
// Read from the client; this will also return when the client closes
|
||||
// the connection.
|
||||
var buf [1]byte
|
||||
_, err := r.Body.Read(buf[:])
|
||||
|
||||
switch {
|
||||
case err == nil:
|
||||
// good
|
||||
case errors.Is(err, io.EOF):
|
||||
// good
|
||||
case errors.Is(err, io.ErrUnexpectedEOF):
|
||||
// this happens when Ctrl-C'ing the tailscale client; don't
|
||||
// bother logging an error
|
||||
default:
|
||||
// Log but continue anyway.
|
||||
h.logf("user bugreport: error reading body: %v", err)
|
||||
}
|
||||
|
||||
// Generate another log marker and return it to the client.
|
||||
endMarker := logMarker()
|
||||
h.logf("user bugreport end: %s", endMarker)
|
||||
fmt.Fprintln(w, endMarker)
|
||||
}
|
||||
|
||||
func (h *Handler) serveWhoIs(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -318,6 +440,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)
|
||||
@@ -406,7 +546,7 @@ func (h *Handler) servePrefs(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "prefs access denied", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
var prefs *ipn.Prefs
|
||||
var prefs ipn.PrefsView
|
||||
switch r.Method {
|
||||
case "PATCH":
|
||||
if !h.PermitWrite {
|
||||
@@ -505,6 +645,7 @@ func (h *Handler) serveFiles(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
defer rc.Close()
|
||||
w.Header().Set("Content-Length", fmt.Sprint(size))
|
||||
w.Header().Set("Content-Type", "application/octet-stream")
|
||||
io.Copy(w, rc)
|
||||
}
|
||||
|
||||
@@ -803,13 +944,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
|
||||
}
|
||||
|
||||
@@ -822,7 +963,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
|
||||
@@ -855,6 +996,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
|
||||
|
||||
@@ -6,12 +6,8 @@ package ipn
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tstest"
|
||||
)
|
||||
|
||||
@@ -61,133 +57,6 @@ func TestReadWrite(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientServer(t *testing.T) {
|
||||
tstest.PanicOnLog()
|
||||
tstest.ResourceCheck(t)
|
||||
|
||||
b := &FakeBackend{}
|
||||
var bs *BackendServer
|
||||
var bc *BackendClient
|
||||
serverToClientCh := make(chan []byte, 16)
|
||||
defer close(serverToClientCh)
|
||||
go func() {
|
||||
for b := range serverToClientCh {
|
||||
bc.GotNotifyMsg(b)
|
||||
}
|
||||
}()
|
||||
serverToClient := func(n Notify) {
|
||||
b, err := json.Marshal(n)
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
serverToClientCh <- append([]byte{}, b...)
|
||||
}
|
||||
clientToServer := func(b []byte) {
|
||||
bs.GotCommandMsg(context.TODO(), b)
|
||||
}
|
||||
slogf := func(fmt string, args ...any) {
|
||||
t.Logf("s: "+fmt, args...)
|
||||
}
|
||||
clogf := func(fmt string, args ...any) {
|
||||
t.Logf("c: "+fmt, args...)
|
||||
}
|
||||
bs = NewBackendServer(slogf, b, serverToClient)
|
||||
// Verify that this doesn't break bs's callback:
|
||||
NewBackendServer(slogf, b, nil)
|
||||
bc = NewBackendClient(clogf, clientToServer)
|
||||
|
||||
ch := make(chan Notify, 256)
|
||||
notify := func(n Notify) { ch <- n }
|
||||
h, err := NewHandle(bc, clogf, notify, Options{
|
||||
Prefs: &Prefs{
|
||||
ControlURL: "http://example.com/fake",
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("NewHandle error: %v\n", err)
|
||||
}
|
||||
|
||||
notes := Notify{}
|
||||
nn := []Notify{}
|
||||
processNote := func(n Notify) {
|
||||
nn = append(nn, n)
|
||||
if n.State != nil {
|
||||
t.Logf("state change: %v", *n.State)
|
||||
notes.State = n.State
|
||||
}
|
||||
if n.Prefs != nil {
|
||||
notes.Prefs = n.Prefs
|
||||
}
|
||||
if n.NetMap != nil {
|
||||
notes.NetMap = n.NetMap
|
||||
}
|
||||
if n.Engine != nil {
|
||||
notes.Engine = n.Engine
|
||||
}
|
||||
if n.BrowseToURL != nil {
|
||||
notes.BrowseToURL = n.BrowseToURL
|
||||
}
|
||||
}
|
||||
notesState := func() State {
|
||||
if notes.State != nil {
|
||||
return *notes.State
|
||||
}
|
||||
return NoState
|
||||
}
|
||||
|
||||
flushUntil := func(wantFlush State) {
|
||||
t.Helper()
|
||||
timer := time.NewTimer(1 * time.Second)
|
||||
loop:
|
||||
for {
|
||||
select {
|
||||
case n := <-ch:
|
||||
processNote(n)
|
||||
if notesState() == wantFlush {
|
||||
break loop
|
||||
}
|
||||
case <-timer.C:
|
||||
t.Fatalf("timeout waiting for state %v, got %v", wantFlush, notes.State)
|
||||
}
|
||||
}
|
||||
timer.Stop()
|
||||
loop2:
|
||||
for {
|
||||
select {
|
||||
case n := <-ch:
|
||||
processNote(n)
|
||||
default:
|
||||
break loop2
|
||||
}
|
||||
}
|
||||
if got, want := h.State(), notesState(); got != want {
|
||||
t.Errorf("h.State()=%v, notes.State=%v (on flush until %v)\n", got, want, wantFlush)
|
||||
}
|
||||
}
|
||||
|
||||
flushUntil(NeedsLogin)
|
||||
|
||||
h.StartLoginInteractive()
|
||||
flushUntil(Running)
|
||||
if notes.NetMap == nil && h.NetMap() != nil {
|
||||
t.Errorf("notes.NetMap == nil while h.NetMap != nil\nnotes:\n%v", nn)
|
||||
}
|
||||
|
||||
h.UpdatePrefs(func(p *Prefs) {
|
||||
p.WantRunning = false
|
||||
})
|
||||
flushUntil(Stopped)
|
||||
|
||||
h.Logout()
|
||||
flushUntil(NeedsLogin)
|
||||
|
||||
h.Login(&tailcfg.Oauth2Token{
|
||||
AccessToken: "google_id_token",
|
||||
TokenType: GoogleIDTokenType,
|
||||
})
|
||||
flushUntil(Running)
|
||||
}
|
||||
|
||||
func TestNilBackend(t *testing.T) {
|
||||
var called *Notify
|
||||
bs := NewBackendServer(t.Logf, nil, func(n Notify) {
|
||||
|
||||
26
ipn/prefs.go
26
ipn/prefs.go
@@ -27,7 +27,7 @@ import (
|
||||
"tailscale.com/util/dnsname"
|
||||
)
|
||||
|
||||
//go:generate go run tailscale.com/cmd/cloner -type=Prefs
|
||||
//go:generate go run tailscale.com/cmd/viewer -type=Prefs
|
||||
|
||||
// DefaultControlURL is the URL base of the control plane
|
||||
// ("coordination server") for use when no explicit one is configured.
|
||||
@@ -288,6 +288,8 @@ func (m *MaskedPrefs) Pretty() string {
|
||||
// IsEmpty reports whether p is nil or pointing to a Prefs zero value.
|
||||
func (p *Prefs) IsEmpty() bool { return p == nil || p.Equals(&Prefs{}) }
|
||||
|
||||
func (p PrefsView) Pretty() string { return p.ж.Pretty() }
|
||||
|
||||
func (p *Prefs) Pretty() string { return p.pretty(runtime.GOOS) }
|
||||
func (p *Prefs) pretty(goos string) string {
|
||||
var sb strings.Builder
|
||||
@@ -347,6 +349,10 @@ func (p *Prefs) pretty(goos string) string {
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func (p PrefsView) ToBytes() []byte {
|
||||
return p.ж.ToBytes()
|
||||
}
|
||||
|
||||
func (p *Prefs) ToBytes() []byte {
|
||||
data, err := json.MarshalIndent(p, "", "\t")
|
||||
if err != nil {
|
||||
@@ -355,6 +361,10 @@ func (p *Prefs) ToBytes() []byte {
|
||||
return data
|
||||
}
|
||||
|
||||
func (p PrefsView) Equals(p2 PrefsView) bool {
|
||||
return p.ж.Equals(p2.ж)
|
||||
}
|
||||
|
||||
func (p *Prefs) Equals(p2 *Prefs) bool {
|
||||
if p == nil && p2 == nil {
|
||||
return true
|
||||
@@ -431,6 +441,14 @@ func NewPrefs() *Prefs {
|
||||
}
|
||||
}
|
||||
|
||||
// ControlURLOrDefault returns the coordination server's URL base.
|
||||
//
|
||||
// If not configured, or if the configured value is a legacy name equivalent to
|
||||
// the default, then DefaultControlURL is returned instead.
|
||||
func (p PrefsView) ControlURLOrDefault() string {
|
||||
return p.ж.ControlURLOrDefault()
|
||||
}
|
||||
|
||||
// ControlURLOrDefault returns the coordination server's URL base.
|
||||
//
|
||||
// If not configured, or if the configured value is a legacy name equivalent to
|
||||
@@ -588,6 +606,12 @@ func (p *Prefs) SetExitNodeIP(s string, st *ipnstate.Status) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// ShouldSSHBeRunning reports whether the SSH server should be running based on
|
||||
// the prefs.
|
||||
func (p PrefsView) ShouldSSHBeRunning() bool {
|
||||
return p.Valid() && p.ж.ShouldSSHBeRunning()
|
||||
}
|
||||
|
||||
// ShouldSSHBeRunning reports whether the SSH server should be running based on
|
||||
// the prefs.
|
||||
func (p *Prefs) ShouldSSHBeRunning() bool {
|
||||
|
||||
@@ -470,7 +470,7 @@ 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 := os.CreateTemp("", "TestLoadPrefsFileWithZeroInIt")
|
||||
@@ -826,3 +826,22 @@ func TestControlURLOrDefault(t *testing.T) {
|
||||
t.Errorf("got %q; want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNotifyPrefsJSONRoundtrip(t *testing.T) {
|
||||
var n Notify
|
||||
if n.Prefs != nil && n.Prefs.Valid() {
|
||||
t.Fatal("Prefs should not be valid at start")
|
||||
}
|
||||
b, err := json.Marshal(n)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var n2 Notify
|
||||
if err := json.Unmarshal(b, &n2); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if n2.Prefs != nil && n2.Prefs.Valid() {
|
||||
t.Fatal("Prefs should not be valid after deserialization")
|
||||
}
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
|
||||
@@ -100,7 +100,9 @@ func (c *Client) secretURL(name string) string {
|
||||
}
|
||||
|
||||
func getError(resp *http.Response) error {
|
||||
if resp.StatusCode == 200 {
|
||||
if resp.StatusCode == 200 || resp.StatusCode == 201 {
|
||||
// These are the only success codes returned by the Kubernetes API.
|
||||
// https://github.com/kubernetes/community/blob/master/contributors/devel/sig-architecture/api-conventions.md#http-status-codes
|
||||
return nil
|
||||
}
|
||||
st := &Status{}
|
||||
|
||||
@@ -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/777337dba4cf/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))
|
||||
|
||||
@@ -27,7 +27,7 @@ and [iOS][]. See also the dependencies in the [Tailscale CLI][].
|
||||
- [github.com/mdlayher/sdnotify](https://pkg.go.dev/github.com/mdlayher/sdnotify) ([MIT](https://github.com/mdlayher/sdnotify/blob/v1.0.0/LICENSE.md))
|
||||
- [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/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/777337dba4cf/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/tcnksm/go-httpstat](https://pkg.go.dev/github.com/tcnksm/go-httpstat) ([MIT](https://github.com/tcnksm/go-httpstat/blob/v0.2.0/LICENSE))
|
||||
@@ -39,9 +39,9 @@ and [iOS][]. See also the dependencies in the [Tailscale CLI][].
|
||||
- [go4.org/netipx](https://pkg.go.dev/go4.org/netipx) ([BSD-3-Clause](https://github.com/go4org/netipx/blob/7e7bdc8411bf/LICENSE))
|
||||
- [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) ([BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/6f7dac96:LICENSE))
|
||||
- [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/net](https://pkg.go.dev/golang.org/x/net) ([BSD-3-Clause](https://cs.opensource.google/go/x/net/+/c690dde0: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/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/+/886fb937: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/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))
|
||||
- [golang.zx2c4.com/wireguard](https://pkg.go.dev/golang.zx2c4.com/wireguard) ([MIT](https://git.zx2c4.com/wireguard-go/tree/LICENSE?id=b51010ba13f0))
|
||||
|
||||
@@ -58,7 +58,7 @@ Some packages may only be included on certain architectures or operating systems
|
||||
- [github.com/pkg/sftp](https://pkg.go.dev/github.com/pkg/sftp) ([BSD-2-Clause](https://github.com/pkg/sftp/blob/v1.13.4/LICENSE))
|
||||
- [github.com/skip2/go-qrcode](https://pkg.go.dev/github.com/skip2/go-qrcode) ([MIT](https://github.com/skip2/go-qrcode/blob/da1b6568686e/LICENSE))
|
||||
- [github.com/tailscale/certstore](https://pkg.go.dev/github.com/tailscale/certstore) ([MIT](https://github.com/tailscale/certstore/blob/78d6e1c49d8d/LICENSE.md))
|
||||
- [github.com/tailscale/golang-x-crypto](https://pkg.go.dev/github.com/tailscale/golang-x-crypto) ([BSD-3-Clause](https://github.com/tailscale/golang-x-crypto/blob/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/netlink](https://pkg.go.dev/github.com/tailscale/netlink) ([Apache-2.0](https://github.com/tailscale/netlink/blob/cabfb018fe85/LICENSE))
|
||||
- [github.com/tcnksm/go-httpstat](https://pkg.go.dev/github.com/tcnksm/go-httpstat) ([MIT](https://github.com/tcnksm/go-httpstat/blob/v0.2.0/LICENSE))
|
||||
- [github.com/toqueteos/webbrowser](https://pkg.go.dev/github.com/toqueteos/webbrowser) ([MIT](https://github.com/toqueteos/webbrowser/blob/v1.2.0/LICENSE.md))
|
||||
@@ -71,9 +71,9 @@ Some packages may only be included on certain architectures or operating systems
|
||||
- [go4.org/netipx](https://pkg.go.dev/go4.org/netipx) ([BSD-3-Clause](https://github.com/go4org/netipx/blob/7e7bdc8411bf/LICENSE))
|
||||
- [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) ([BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/eb4f295c:LICENSE))
|
||||
- [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/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))
|
||||
|
||||
@@ -31,9 +31,9 @@ Windows][]. See also the dependencies in the [Tailscale CLI][].
|
||||
- [go4.org/mem](https://pkg.go.dev/go4.org/mem) ([Apache-2.0](https://github.com/go4org/mem/blob/4f986261bf13/LICENSE))
|
||||
- [go4.org/netipx](https://pkg.go.dev/go4.org/netipx) ([BSD-3-Clause](https://github.com/go4org/netipx/blob/7e7bdc8411bf/LICENSE))
|
||||
- [golang.org/x/crypto](https://pkg.go.dev/golang.org/x/crypto) ([BSD-3-Clause](https://cs.opensource.google/go/x/crypto/+/6f7dac96: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/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/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/+/886fb937: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.zx2c4.com/wintun](https://pkg.go.dev/golang.zx2c4.com/wintun) ([MIT](https://git.zx2c4.com/wintun-go/tree/LICENSE?id=415007cec224))
|
||||
- [golang.zx2c4.com/wireguard/windows/tunnel/winipcfg](https://pkg.go.dev/golang.zx2c4.com/wireguard/windows/tunnel/winipcfg) ([MIT](https://git.zx2c4.com/wireguard-windows/tree/COPYING?h=v0.5.3))
|
||||
|
||||
@@ -70,11 +70,24 @@ func getLogTarget() string {
|
||||
return getLogTargetOnce.v
|
||||
}
|
||||
|
||||
// LogURL is the base URL for the configured logtail server, or the default.
|
||||
// It is guaranteed to not terminate with any forward slashes.
|
||||
func LogURL() string {
|
||||
if v := getLogTarget(); v != "" {
|
||||
return strings.TrimRight(v, "/")
|
||||
}
|
||||
return "https://" + logtail.DefaultHost
|
||||
}
|
||||
|
||||
// LogHost returns the hostname only (without port) of the configured
|
||||
// logtail server, or the default.
|
||||
//
|
||||
// Deprecated: Use LogURL instead.
|
||||
func LogHost() string {
|
||||
if v := getLogTarget(); v != "" {
|
||||
return v
|
||||
if u, err := url.Parse(v); err == nil {
|
||||
return u.Hostname()
|
||||
}
|
||||
}
|
||||
return logtail.DefaultHost
|
||||
}
|
||||
@@ -596,7 +609,7 @@ func NewWithConfigPath(collection, dir, cmdName string) *Policy {
|
||||
}
|
||||
}
|
||||
|
||||
log.SetFlags(0) // other logflags are set on console, not here
|
||||
log.SetFlags(0) // other log flags are set on console, not here
|
||||
log.SetOutput(logOutput)
|
||||
|
||||
log.Printf("Program starting: v%v, Go %v: %#v",
|
||||
|
||||
37
logpolicy/logpolicy_test.go
Normal file
37
logpolicy/logpolicy_test.go
Normal file
@@ -0,0 +1,37 @@
|
||||
// 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 logpolicy
|
||||
|
||||
import (
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestLogHost(t *testing.T) {
|
||||
v := reflect.ValueOf(&getLogTargetOnce).Elem()
|
||||
reset := func() {
|
||||
v.Set(reflect.Zero(v.Type()))
|
||||
}
|
||||
defer reset()
|
||||
|
||||
tests := []struct {
|
||||
env string
|
||||
want string
|
||||
}{
|
||||
{"", "log.tailscale.io"},
|
||||
{"http://foo.com", "foo.com"},
|
||||
{"https://foo.com", "foo.com"},
|
||||
{"https://foo.com/", "foo.com"},
|
||||
{"https://foo.com:123/", "foo.com"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
reset()
|
||||
os.Setenv("TS_LOG_TARGET", tt.env)
|
||||
if got := LogHost(); got != tt.want {
|
||||
t.Errorf("for env %q, got %q, want %q", tt.env, got, tt.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -34,7 +34,7 @@ func NewPrivateID() (id PrivateID, err error) {
|
||||
func (id PrivateID) MarshalText() ([]byte, error) {
|
||||
b := make([]byte, hex.EncodedLen(len(id)))
|
||||
if i := hex.Encode(b, id[:]); i != len(b) {
|
||||
return nil, fmt.Errorf("logtail.PrivateID.MarhsalText: i=%d", i)
|
||||
return nil, fmt.Errorf("logtail.PrivateID.MarshalText: i=%d", i)
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
@@ -122,7 +122,7 @@ func MustParsePublicID(s string) PublicID {
|
||||
func (id PublicID) MarshalText() ([]byte, error) {
|
||||
b := make([]byte, hex.EncodedLen(len(id)))
|
||||
if i := hex.Encode(b, id[:]); i != len(b) {
|
||||
return nil, fmt.Errorf("logtail.PublicID.MarhsalText: i=%d", i)
|
||||
return nil, fmt.Errorf("logtail.PublicID.MarshalText: i=%d", i)
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
|
||||
@@ -44,12 +44,13 @@ type Encoder interface {
|
||||
|
||||
type Config struct {
|
||||
Collection string // collection name, a domain name
|
||||
PrivateID PrivateID // machine-specific private identifier
|
||||
PrivateID PrivateID // private ID for the primary log stream
|
||||
CopyPrivateID PrivateID // private ID for a log stream that is a superset of this log stream
|
||||
BaseURL string // if empty defaults to "https://log.tailscale.io"
|
||||
HTTPC *http.Client // if empty defaults to http.DefaultClient
|
||||
SkipClientTime bool // if true, client_time is not written to logs
|
||||
LowMemory bool // if true, logtail minimizes memory use
|
||||
TimeNow func() time.Time // if set, subsitutes uses of time.Now
|
||||
TimeNow func() time.Time // if set, substitutes uses of time.Now
|
||||
Stderr io.Writer // if set, logs are sent here instead of os.Stderr
|
||||
StderrLevel int // max verbosity level to write to stderr; 0 means the non-verbose messages only
|
||||
Buffer Buffer // temp storage, if nil a MemoryBuffer
|
||||
@@ -73,7 +74,7 @@ type Config struct {
|
||||
|
||||
// IncludeProcSequence, if true, results in an ephemeral sequence number
|
||||
// being included in the logs. The sequence number is incremented for each
|
||||
// log message sent, but is not peristed across process restarts.
|
||||
// log message sent, but is not persisted across process restarts.
|
||||
IncludeProcSequence bool
|
||||
}
|
||||
|
||||
@@ -112,12 +113,16 @@ func NewLogger(cfg Config, logf tslogger.Logf) *Logger {
|
||||
stdLogf := func(f string, a ...any) {
|
||||
fmt.Fprintf(cfg.Stderr, strings.TrimSuffix(f, "\n")+"\n", a...)
|
||||
}
|
||||
var urlSuffix string
|
||||
if !cfg.CopyPrivateID.IsZero() {
|
||||
urlSuffix = "?copyId=" + cfg.CopyPrivateID.String()
|
||||
}
|
||||
l := &Logger{
|
||||
privateID: cfg.PrivateID,
|
||||
stderr: cfg.Stderr,
|
||||
stderrLevel: int64(cfg.StderrLevel),
|
||||
httpc: cfg.HTTPC,
|
||||
url: cfg.BaseURL + "/c/" + cfg.Collection + "/" + cfg.PrivateID.String(),
|
||||
url: cfg.BaseURL + "/c/" + cfg.Collection + "/" + cfg.PrivateID.String() + urlSuffix,
|
||||
lowMem: cfg.LowMemory,
|
||||
buffer: cfg.Buffer,
|
||||
skipClientTime: cfg.SkipClientTime,
|
||||
@@ -519,7 +524,7 @@ func (l *Logger) encodeText(buf []byte, skipClientTime bool, procID uint32, proc
|
||||
b = append(b, `"logtail": {`...)
|
||||
if !skipClientTime {
|
||||
b = append(b, `"client_time": "`...)
|
||||
b = now.AppendFormat(b, time.RFC3339Nano)
|
||||
b = now.UTC().AppendFormat(b, time.RFC3339Nano)
|
||||
b = append(b, `",`...)
|
||||
}
|
||||
if procID != 0 {
|
||||
@@ -612,7 +617,7 @@ func (l *Logger) encodeLocked(buf []byte, level int) []byte {
|
||||
if !l.skipClientTime || l.procID != 0 || l.procSequence != 0 {
|
||||
logtail := map[string]any{}
|
||||
if !l.skipClientTime {
|
||||
logtail["client_time"] = now.Format(time.RFC3339Nano)
|
||||
logtail["client_time"] = now.UTC().Format(time.RFC3339Nano)
|
||||
}
|
||||
if l.procID != 0 {
|
||||
logtail["proc_id"] = l.procID
|
||||
|
||||
@@ -381,7 +381,7 @@ func (m *Manager) NextPacket() ([]byte, error) {
|
||||
return buf, nil
|
||||
}
|
||||
|
||||
// Query executes a DNS query recieved from the given address. The query is
|
||||
// Query executes a DNS query received from the given address. The query is
|
||||
// provided in bs as a wire-encoded DNS query without any transport header.
|
||||
// This method is called for requests arriving over UDP and TCP.
|
||||
func (m *Manager) Query(ctx context.Context, bs []byte, from netip.AddrPort) ([]byte, error) {
|
||||
@@ -540,7 +540,7 @@ func Cleanup(logf logger.Logf, interfaceName string) {
|
||||
logf("creating dns cleanup: %v", err)
|
||||
return
|
||||
}
|
||||
dns := NewManager(logf, oscfg, nil, new(tsdial.Dialer), nil)
|
||||
dns := NewManager(logf, oscfg, nil, &tsdial.Dialer{Logf: logf}, nil)
|
||||
if err := dns.Down(); err != nil {
|
||||
logf("dns down: %v", err)
|
||||
}
|
||||
|
||||
@@ -274,7 +274,7 @@ func runTest(t *testing.T, isLocal bool) {
|
||||
runCase := func(n int) {
|
||||
t.Logf("Test case: %d domains\n", n)
|
||||
if !isLocal {
|
||||
// When !isLocal, we want to check that a GP notification occured for
|
||||
// When !isLocal, we want to check that a GP notification occurred for
|
||||
// every single test case.
|
||||
trk, err = newGPNotificationTracker()
|
||||
if err != nil {
|
||||
|
||||
@@ -302,7 +302,7 @@ func (m *nmManager) GetBaseConfig() (OSConfig, error) {
|
||||
for _, cfg := range cfgs {
|
||||
if name, ok := cfg["interface"]; ok {
|
||||
if s, ok := name.Value().(string); ok && s == m.interfaceName {
|
||||
// Config for the taislcale interface, skip.
|
||||
// Config for the tailscale interface, skip.
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ var (
|
||||
|
||||
const _RP_FORCE = 1 // Flag for RefreshPolicyEx
|
||||
|
||||
// nrptRuleDatabase ensapsulates access to the Windows Name Resolution Policy
|
||||
// nrptRuleDatabase encapsulates access to the Windows Name Resolution Policy
|
||||
// Table (NRPT).
|
||||
type nrptRuleDatabase struct {
|
||||
logf logger.Logf
|
||||
|
||||
@@ -37,15 +37,15 @@ func DoHEndpointFromIP(ip netip.Addr) (dohBase string, dohOnly bool, ok bool) {
|
||||
}
|
||||
|
||||
// NextDNS DoH URLs are of the form "https://dns.nextdns.io/c3a884"
|
||||
// where the path component is the lower 8 bytes of the IPv6 address
|
||||
// where the path component is the lower 12 bytes of the IPv6 address
|
||||
// in lowercase hex without any zero padding.
|
||||
if nextDNSv6RangeA.Contains(ip) || nextDNSv6RangeB.Contains(ip) {
|
||||
a := ip.As16()
|
||||
var sb strings.Builder
|
||||
const base = "https://dns.nextdns.io/"
|
||||
sb.Grow(len(base) + 8)
|
||||
sb.Grow(len(base) + 12)
|
||||
sb.WriteString(base)
|
||||
for _, b := range bytes.TrimLeft(a[8:], "\x00") {
|
||||
for _, b := range bytes.TrimLeft(a[4:], "\x00") {
|
||||
fmt.Fprintf(&sb, "%02x", b)
|
||||
}
|
||||
return sb.String(), true, true
|
||||
@@ -100,7 +100,7 @@ func DoHIPsOfBase(dohBase string) []netip.Addr {
|
||||
// conventional for them and not required (it'll already be in the DoH path).
|
||||
// (Really we shouldn't use either IPv4 or IPv6 anycast for DoH once we
|
||||
// resolve "dns.nextdns.io".)
|
||||
if b, err := hex.DecodeString(hexStr); err == nil && len(b) <= 8 && len(b) > 0 {
|
||||
if b, err := hex.DecodeString(hexStr); err == nil && len(b) <= 12 && len(b) > 0 {
|
||||
return []netip.Addr{
|
||||
nextDNSv4One,
|
||||
nextDNSv4Two,
|
||||
@@ -215,7 +215,7 @@ var (
|
||||
// nextDNSv6Gen generates a NextDNS IPv6 address from the upper 8 bytes in the
|
||||
// provided ip and using id as the lowest 0-8 bytes.
|
||||
func nextDNSv6Gen(ip netip.Addr, id []byte) netip.Addr {
|
||||
if len(id) > 8 {
|
||||
if len(id) > 12 {
|
||||
return netip.Addr{}
|
||||
}
|
||||
a := ip.As16()
|
||||
|
||||
@@ -86,6 +86,19 @@ func TestDoHIPsOfBase(t *testing.T) {
|
||||
"2a07:a8c1::c3:a884",
|
||||
),
|
||||
},
|
||||
{
|
||||
base: "https://dns.nextdns.io/112233445566778899aabbcc",
|
||||
want: ips(
|
||||
"45.90.28.0",
|
||||
"45.90.30.0",
|
||||
"2a07:a8c0:1122:3344:5566:7788:99aa:bbcc",
|
||||
"2a07:a8c1:1122:3344:5566:7788:99aa:bbcc",
|
||||
),
|
||||
},
|
||||
{
|
||||
base: "https://dns.nextdns.io/112233445566778899aabbccdd",
|
||||
want: ips(), // nothing; profile length is over 12 bytes
|
||||
},
|
||||
{
|
||||
base: "https://dns.nextdns.io/c3a884/with/more/stuff",
|
||||
want: ips(
|
||||
|
||||
@@ -180,7 +180,7 @@ type resolverAndDelay struct {
|
||||
type forwarder struct {
|
||||
logf logger.Logf
|
||||
linkMon *monitor.Mon
|
||||
linkSel ForwardLinkSelector // TODO(bradfitz): remove this when tsdial.Dialer absords it
|
||||
linkSel ForwardLinkSelector // TODO(bradfitz): remove this when tsdial.Dialer absorbs it
|
||||
dialer *tsdial.Dialer
|
||||
dohSem chan struct{}
|
||||
|
||||
@@ -502,7 +502,7 @@ func (f *forwarder) send(ctx context.Context, fq *forwardQuery, rr resolverAndDe
|
||||
// Only known DoH providers are supported currently. Specifically, we
|
||||
// only support DoH providers where we can TCP connect to them on port
|
||||
// 443 at the same IP address they serve normal UDP DNS from (1.1.1.1,
|
||||
// 8.8.8.8, 9.9.9.9, etc.) That's why OpenDNS and custon DoH providers
|
||||
// 8.8.8.8, 9.9.9.9, etc.) That's why OpenDNS and custom DoH providers
|
||||
// aren't currently supported. There's no backup DNS resolution path for
|
||||
// them.
|
||||
urlBase := rr.name.Addr
|
||||
|
||||
@@ -609,7 +609,7 @@ func (r *Resolver) resolveLocal(domain dnsname.FQDN, typ dns.Type) (netip.Addr,
|
||||
metricDNSResolveLocalOKAll.Add(1)
|
||||
return addrs[0], dns.RCodeSuccess
|
||||
|
||||
// Leave some some record types explicitly unimplemented.
|
||||
// Leave some record types explicitly unimplemented.
|
||||
// These types relate to recursive resolution or special
|
||||
// DNS semantics and might be implemented in the future.
|
||||
case dns.TypeNS, dns.TypeSOA, dns.TypeAXFR, dns.TypeHINFO:
|
||||
|
||||
@@ -276,6 +276,11 @@ func (r *Resolver) lookupIP(host string) (ip, ip6 netip.Addr, allIPs []netip.Add
|
||||
return netip.Addr{}, netip.Addr{}, nil, fmt.Errorf("no IPs for %q found", host)
|
||||
}
|
||||
|
||||
// Unmap everything; LookupNetIP can return mapped addresses (see #5698)
|
||||
for i := range ips {
|
||||
ips[i] = ips[i].Unmap()
|
||||
}
|
||||
|
||||
have4 := false
|
||||
for _, ipa := range ips {
|
||||
if ipa.Is4() {
|
||||
|
||||
@@ -99,7 +99,7 @@ type msgResource struct {
|
||||
}
|
||||
|
||||
// ErrCacheMiss is a sentinel error returned by MessageCache.ReplyFromCache
|
||||
// when the request can not be satisified from cache.
|
||||
// when the request can not be satisfied from cache.
|
||||
var ErrCacheMiss = errors.New("cache miss")
|
||||
|
||||
var parserPool = &sync.Pool{
|
||||
@@ -264,7 +264,7 @@ func asciiLowerName(n dnsmessage.Name) dnsmessage.Name {
|
||||
}
|
||||
|
||||
// packDNSResponse builds a DNS response for the given question and
|
||||
// transaction ID. The response resource records will have have the
|
||||
// transaction ID. The response resource records will have the
|
||||
// same provided TTL.
|
||||
func packDNSResponse(q msgQ, txID uint16, ttl uint32, answers []msgResource) ([]byte, error) {
|
||||
var baseMem []byte // TODO: guess a max size based on looping over answers?
|
||||
|
||||
@@ -20,9 +20,9 @@ import (
|
||||
|
||||
// Tuple is a 5-tuple of proto, source and destination IP and port.
|
||||
type Tuple struct {
|
||||
Proto ipproto.Proto
|
||||
Src netip.AddrPort
|
||||
Dst netip.AddrPort
|
||||
Proto ipproto.Proto `json:"proto"`
|
||||
Src netip.AddrPort `json:"src"`
|
||||
Dst netip.AddrPort `json:"dst"`
|
||||
}
|
||||
|
||||
func (t Tuple) String() string {
|
||||
|
||||
@@ -441,13 +441,13 @@ func prefixesEqual(a, b []netip.Prefix) bool {
|
||||
|
||||
// UseInterestingInterfaces is an InterfaceFilter that reports whether i is an interesting interface.
|
||||
// An interesting interface if it is (a) not owned by Tailscale and (b) routes interesting IP addresses.
|
||||
// See UseInterestingIPs for the defition of an interesting IP address.
|
||||
// See UseInterestingIPs for the definition of an interesting IP address.
|
||||
func UseInterestingInterfaces(i Interface, ips []netip.Prefix) bool {
|
||||
return !isTailscaleInterface(i.Name, ips) && anyInterestingIP(ips)
|
||||
}
|
||||
|
||||
// UseInterestingIPs is an IPFilter that reports whether ip is an interesting IP address.
|
||||
// An IP address is interesting if it is neither a lopback not a link local unicast IP address.
|
||||
// An IP address is interesting if it is neither a loopback nor a link local unicast IP address.
|
||||
func UseInterestingIPs(ip netip.Addr) bool {
|
||||
return isInterestingIP(ip)
|
||||
}
|
||||
@@ -455,7 +455,7 @@ func UseInterestingIPs(ip netip.Addr) bool {
|
||||
// UseAllInterfaces is an InterfaceFilter that includes all interfaces.
|
||||
func UseAllInterfaces(i Interface, ips []netip.Prefix) bool { return true }
|
||||
|
||||
// UseAllIPs is an IPFilter that includes all all IPs.
|
||||
// UseAllIPs is an IPFilter that includes all IPs.
|
||||
func UseAllIPs(ips netip.Addr) bool { return true }
|
||||
|
||||
func (s *State) HasPAC() bool { return s != nil && s.PAC != "" }
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// This might work on other BSDs, but only tested on FreeBSD.
|
||||
// Originally a fork of interfaces_darwin.go with slightly different flags.
|
||||
// Common code for FreeBSD and Darwin. This might also work on other
|
||||
// BSD systems (e.g. OpenBSD) but has not been tested.
|
||||
|
||||
//go:build freebsd
|
||||
// +build freebsd
|
||||
//go:build darwin || freebsd
|
||||
// +build darwin freebsd
|
||||
|
||||
package interfaces
|
||||
|
||||
@@ -37,11 +37,6 @@ func defaultRoute() (d DefaultRouteDetails, err error) {
|
||||
return d, nil
|
||||
}
|
||||
|
||||
// fetchRoutingTable calls route.FetchRIB, fetching NET_RT_DUMP.
|
||||
func fetchRoutingTable() (rib []byte, err error) {
|
||||
return route.FetchRIB(syscall.AF_UNSPEC, unix.NET_RT_DUMP, 0)
|
||||
}
|
||||
|
||||
func DefaultRouteInterfaceIndex() (int, error) {
|
||||
// $ netstat -nr
|
||||
// Routing tables
|
||||
@@ -61,35 +56,20 @@ func DefaultRouteInterfaceIndex() (int, error) {
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("route.FetchRIB: %w", err)
|
||||
}
|
||||
msgs, err := route.ParseRIB(unix.NET_RT_IFLIST, rib)
|
||||
msgs, err := parseRoutingTable(rib)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("route.ParseRIB: %w", err)
|
||||
}
|
||||
indexSeen := map[int]int{} // index => count
|
||||
for _, m := range msgs {
|
||||
rm, ok := m.(*route.RouteMessage)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
const RTF_GATEWAY = 0x2
|
||||
const RTF_IFSCOPE = 0x1000000
|
||||
if rm.Flags&RTF_GATEWAY == 0 {
|
||||
continue
|
||||
}
|
||||
if rm.Flags&RTF_IFSCOPE != 0 {
|
||||
continue
|
||||
}
|
||||
indexSeen[rm.Index]++
|
||||
}
|
||||
if len(indexSeen) == 0 {
|
||||
return 0, errors.New("no gateway index found")
|
||||
}
|
||||
if len(indexSeen) == 1 {
|
||||
for idx := range indexSeen {
|
||||
return idx, nil
|
||||
if isDefaultGateway(rm) {
|
||||
return rm.Index, nil
|
||||
}
|
||||
}
|
||||
return 0, fmt.Errorf("ambiguous gateway interfaces found: %v", indexSeen)
|
||||
return 0, errors.New("no gateway index found")
|
||||
}
|
||||
|
||||
func init() {
|
||||
@@ -102,7 +82,7 @@ func likelyHomeRouterIPBSDFetchRIB() (ret netip.Addr, ok bool) {
|
||||
log.Printf("routerIP/FetchRIB: %v", err)
|
||||
return ret, false
|
||||
}
|
||||
msgs, err := route.ParseRIB(unix.NET_RT_IFLIST, rib)
|
||||
msgs, err := parseRoutingTable(rib)
|
||||
if err != nil {
|
||||
log.Printf("routerIP/ParseRIB: %v", err)
|
||||
return ret, false
|
||||
@@ -112,26 +92,59 @@ func likelyHomeRouterIPBSDFetchRIB() (ret netip.Addr, ok bool) {
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
const RTF_IFSCOPE = 0x1000000
|
||||
if rm.Flags&unix.RTF_GATEWAY == 0 {
|
||||
if !isDefaultGateway(rm) {
|
||||
continue
|
||||
}
|
||||
if rm.Flags&RTF_IFSCOPE != 0 {
|
||||
|
||||
gw, ok := rm.Addrs[unix.RTAX_GATEWAY].(*route.Inet4Addr)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if len(rm.Addrs) > unix.RTAX_GATEWAY {
|
||||
dst4, ok := rm.Addrs[unix.RTAX_DST].(*route.Inet4Addr)
|
||||
if !ok || dst4.IP != ([4]byte{0, 0, 0, 0}) {
|
||||
// Expect 0.0.0.0 as DST field.
|
||||
continue
|
||||
}
|
||||
gw, ok := rm.Addrs[unix.RTAX_GATEWAY].(*route.Inet4Addr)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
return netaddr.IPv4(gw.IP[0], gw.IP[1], gw.IP[2], gw.IP[3]), true
|
||||
}
|
||||
return netaddr.IPv4(gw.IP[0], gw.IP[1], gw.IP[2], gw.IP[3]), true
|
||||
}
|
||||
|
||||
return ret, false
|
||||
}
|
||||
|
||||
var v4default = [4]byte{0, 0, 0, 0}
|
||||
var v6default = [16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
|
||||
|
||||
func isDefaultGateway(rm *route.RouteMessage) bool {
|
||||
if rm.Flags&unix.RTF_GATEWAY == 0 {
|
||||
return false
|
||||
}
|
||||
// Defined locally because FreeBSD does not have unix.RTF_IFSCOPE.
|
||||
const RTF_IFSCOPE = 0x1000000
|
||||
if rm.Flags&RTF_IFSCOPE != 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
// Addrs is [RTAX_DST, RTAX_GATEWAY, RTAX_NETMASK, ...]
|
||||
if len(rm.Addrs) <= unix.RTAX_NETMASK {
|
||||
return false
|
||||
}
|
||||
|
||||
dst := rm.Addrs[unix.RTAX_DST]
|
||||
netmask := rm.Addrs[unix.RTAX_NETMASK]
|
||||
if dst == nil || netmask == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if dst.Family() == syscall.AF_INET && netmask.Family() == syscall.AF_INET {
|
||||
dstAddr, dstOk := dst.(*route.Inet4Addr)
|
||||
nmAddr, nmOk := netmask.(*route.Inet4Addr)
|
||||
if dstOk && nmOk && dstAddr.IP == v4default && nmAddr.IP == v4default {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
if dst.Family() == syscall.AF_INET6 && netmask.Family() == syscall.AF_INET6 {
|
||||
dstAddr, dstOk := dst.(*route.Inet6Addr)
|
||||
nmAddr, nmOk := netmask.(*route.Inet6Addr)
|
||||
if dstOk && nmOk && dstAddr.IP == v6default && nmAddr.IP == v6default {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -5,128 +5,16 @@
|
||||
package interfaces
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"net/netip"
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/net/route"
|
||||
"golang.org/x/sys/unix"
|
||||
"tailscale.com/net/netaddr"
|
||||
)
|
||||
|
||||
func defaultRoute() (d DefaultRouteDetails, err error) {
|
||||
idx, err := DefaultRouteInterfaceIndex()
|
||||
if err != nil {
|
||||
return d, err
|
||||
}
|
||||
iface, err := net.InterfaceByIndex(idx)
|
||||
if err != nil {
|
||||
return d, err
|
||||
}
|
||||
d.InterfaceName = iface.Name
|
||||
d.InterfaceIndex = idx
|
||||
return d, nil
|
||||
}
|
||||
|
||||
// fetchRoutingTable calls route.FetchRIB, fetching NET_RT_DUMP2.
|
||||
func fetchRoutingTable() (rib []byte, err error) {
|
||||
return route.FetchRIB(syscall.AF_UNSPEC, syscall.NET_RT_DUMP2, 0)
|
||||
}
|
||||
|
||||
func DefaultRouteInterfaceIndex() (int, error) {
|
||||
// $ netstat -nr
|
||||
// Routing tables
|
||||
// Internet:
|
||||
// Destination Gateway Flags Netif Expire
|
||||
// default 10.0.0.1 UGSc en0 <-- want this one
|
||||
// default 10.0.0.1 UGScI en1
|
||||
|
||||
// From man netstat:
|
||||
// U RTF_UP Route usable
|
||||
// G RTF_GATEWAY Destination requires forwarding by intermediary
|
||||
// S RTF_STATIC Manually added
|
||||
// c RTF_PRCLONING Protocol-specified generate new routes on use
|
||||
// I RTF_IFSCOPE Route is associated with an interface scope
|
||||
|
||||
rib, err := fetchRoutingTable()
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("route.FetchRIB: %w", err)
|
||||
}
|
||||
msgs, err := route.ParseRIB(syscall.NET_RT_IFLIST2, rib)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("route.ParseRIB: %w", err)
|
||||
}
|
||||
indexSeen := map[int]int{} // index => count
|
||||
for _, m := range msgs {
|
||||
rm, ok := m.(*route.RouteMessage)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
const RTF_GATEWAY = 0x2
|
||||
const RTF_IFSCOPE = 0x1000000
|
||||
if rm.Flags&RTF_GATEWAY == 0 {
|
||||
continue
|
||||
}
|
||||
if rm.Flags&RTF_IFSCOPE != 0 {
|
||||
continue
|
||||
}
|
||||
indexSeen[rm.Index]++
|
||||
}
|
||||
if len(indexSeen) == 0 {
|
||||
return 0, errors.New("no gateway index found")
|
||||
}
|
||||
if len(indexSeen) == 1 {
|
||||
for idx := range indexSeen {
|
||||
return idx, nil
|
||||
}
|
||||
}
|
||||
return 0, fmt.Errorf("ambiguous gateway interfaces found: %v", indexSeen)
|
||||
}
|
||||
|
||||
func init() {
|
||||
likelyHomeRouterIP = likelyHomeRouterIPDarwinFetchRIB
|
||||
}
|
||||
|
||||
func likelyHomeRouterIPDarwinFetchRIB() (ret netip.Addr, ok bool) {
|
||||
rib, err := fetchRoutingTable()
|
||||
if err != nil {
|
||||
log.Printf("routerIP/FetchRIB: %v", err)
|
||||
return ret, false
|
||||
}
|
||||
msgs, err := route.ParseRIB(syscall.NET_RT_IFLIST2, rib)
|
||||
if err != nil {
|
||||
log.Printf("routerIP/ParseRIB: %v", err)
|
||||
return ret, false
|
||||
}
|
||||
for _, m := range msgs {
|
||||
rm, ok := m.(*route.RouteMessage)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
const RTF_GATEWAY = 0x2
|
||||
const RTF_IFSCOPE = 0x1000000
|
||||
if rm.Flags&RTF_GATEWAY == 0 {
|
||||
continue
|
||||
}
|
||||
if rm.Flags&RTF_IFSCOPE != 0 {
|
||||
continue
|
||||
}
|
||||
if len(rm.Addrs) > unix.RTAX_GATEWAY {
|
||||
dst4, ok := rm.Addrs[unix.RTAX_DST].(*route.Inet4Addr)
|
||||
if !ok || dst4.IP != ([4]byte{0, 0, 0, 0}) {
|
||||
// Expect 0.0.0.0 as DST field.
|
||||
continue
|
||||
}
|
||||
gw, ok := rm.Addrs[unix.RTAX_GATEWAY].(*route.Inet4Addr)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
return netaddr.IPv4(gw.IP[0], gw.IP[1], gw.IP[2], gw.IP[3]), true
|
||||
}
|
||||
}
|
||||
|
||||
return ret, false
|
||||
func parseRoutingTable(rib []byte) ([]route.Message, error) {
|
||||
return route.ParseRIB(syscall.NET_RT_IFLIST2, rib)
|
||||
}
|
||||
|
||||
@@ -16,18 +16,32 @@ import (
|
||||
)
|
||||
|
||||
func TestLikelyHomeRouterIPSyscallExec(t *testing.T) {
|
||||
syscallIP, syscallOK := likelyHomeRouterIPDarwinFetchRIB()
|
||||
netstatIP, netstatOK := likelyHomeRouterIPDarwinExec()
|
||||
syscallIP, syscallOK := likelyHomeRouterIPBSDFetchRIB()
|
||||
netstatIP, netstatIf, netstatOK := likelyHomeRouterIPDarwinExec()
|
||||
|
||||
if syscallOK != netstatOK || syscallIP != netstatIP {
|
||||
t.Errorf("syscall() = %v, %v, netstat = %v, %v",
|
||||
syscallIP, syscallOK,
|
||||
netstatIP, netstatOK,
|
||||
)
|
||||
}
|
||||
|
||||
if !syscallOK {
|
||||
return
|
||||
}
|
||||
|
||||
def, err := defaultRoute()
|
||||
if err != nil {
|
||||
t.Errorf("defaultRoute() error: %v", err)
|
||||
}
|
||||
|
||||
if def.InterfaceName != netstatIf {
|
||||
t.Errorf("syscall default route interface %s differs from netstat %s", def.InterfaceName, netstatIf)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Parse out 10.0.0.1 from:
|
||||
Parse out 10.0.0.1 and en0 from:
|
||||
|
||||
$ netstat -r -n -f inet
|
||||
Routing tables
|
||||
@@ -40,12 +54,12 @@ default link#14 UCSI utun2
|
||||
10.0.0.1/32 link#4 UCS en0 !
|
||||
...
|
||||
*/
|
||||
func likelyHomeRouterIPDarwinExec() (ret netip.Addr, ok bool) {
|
||||
func likelyHomeRouterIPDarwinExec() (ret netip.Addr, netif string, ok bool) {
|
||||
if version.IsMobile() {
|
||||
// Don't try to do subprocesses on iOS. Ends up with log spam like:
|
||||
// kernel: "Sandbox: IPNExtension(86580) deny(1) process-fork"
|
||||
// This is why we have likelyHomeRouterIPDarwinSyscall.
|
||||
return ret, false
|
||||
return ret, "", false
|
||||
}
|
||||
cmd := exec.Command("/usr/sbin/netstat", "-r", "-n", "-f", "inet")
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
@@ -64,22 +78,26 @@ func likelyHomeRouterIPDarwinExec() (ret netip.Addr, ok bool) {
|
||||
return nil
|
||||
}
|
||||
f = mem.AppendFields(f[:0], line)
|
||||
if len(f) < 3 || !f[0].EqualString("default") {
|
||||
if len(f) < 4 || !f[0].EqualString("default") {
|
||||
return nil
|
||||
}
|
||||
ipm, flagsm := f[1], f[2]
|
||||
ipm, flagsm, netifm := f[1], f[2], f[3]
|
||||
if !mem.Contains(flagsm, mem.S("G")) {
|
||||
return nil
|
||||
}
|
||||
if mem.Contains(flagsm, mem.S("I")) {
|
||||
return nil
|
||||
}
|
||||
ip, err := netip.ParseAddr(string(mem.Append(nil, ipm)))
|
||||
if err == nil && ip.IsPrivate() {
|
||||
ret = ip
|
||||
netif = netifm.StringCopy()
|
||||
// We've found what we're looking for.
|
||||
return errStopReadingNetstatTable
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return ret, ret.IsValid()
|
||||
return ret, netif, ret.IsValid()
|
||||
}
|
||||
|
||||
func TestFetchRoutingTable(t *testing.T) {
|
||||
|
||||
26
net/interfaces/interfaces_freebsd.go
Normal file
26
net/interfaces/interfaces_freebsd.go
Normal file
@@ -0,0 +1,26 @@
|
||||
// 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.
|
||||
|
||||
// This might work on other BSDs, but only tested on FreeBSD.
|
||||
|
||||
//go:build freebsd
|
||||
// +build freebsd
|
||||
|
||||
package interfaces
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/net/route"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
// fetchRoutingTable calls route.FetchRIB, fetching NET_RT_DUMP.
|
||||
func fetchRoutingTable() (rib []byte, err error) {
|
||||
return route.FetchRIB(syscall.AF_UNSPEC, unix.NET_RT_DUMP, 0)
|
||||
}
|
||||
|
||||
func parseRoutingTable(rib []byte) ([]route.Message, error) {
|
||||
return route.ParseRIB(syscall.NET_RT_IFLIST, rib)
|
||||
}
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
"net/netip"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -161,7 +162,7 @@ type Client struct {
|
||||
|
||||
// GetSTUNConn4 optionally provides a func to return the
|
||||
// connection to use for sending & receiving IPv4 packets. If
|
||||
// nil, an emphemeral one is created as needed.
|
||||
// nil, an ephemeral one is created as needed.
|
||||
GetSTUNConn4 func() STUNConn
|
||||
|
||||
// GetSTUNConn6 is like GetSTUNConn4, but for IPv6.
|
||||
@@ -1105,21 +1106,39 @@ func (c *Client) checkCaptivePortal(ctx context.Context, dm *tailcfg.DERPMap, pr
|
||||
}
|
||||
rids = append(rids, id)
|
||||
}
|
||||
if len(rids) == 0 {
|
||||
return false, nil
|
||||
}
|
||||
preferredDERP = rids[rand.Intn(len(rids))]
|
||||
}
|
||||
|
||||
node := dm.Regions[preferredDERP].Nodes[0]
|
||||
|
||||
if strings.HasSuffix(node.HostName, tailcfg.DotInvalid) {
|
||||
// Don't try to connect to invalid hostnames. This occurred in tests:
|
||||
// https://github.com/tailscale/tailscale/issues/6207
|
||||
// TODO(bradfitz,andrew-d): how to actually handle this nicely?
|
||||
return false, nil
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", "http://"+node.HostName+"/generate_204", nil)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
chal := "tailscale " + node.HostName
|
||||
req.Header.Set("X-Tailscale-Challenge", chal)
|
||||
r, err := noRedirectClient.Do(req)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
c.logf("[v2] checkCaptivePortal url=%q status_code=%d", req.URL.String(), r.StatusCode)
|
||||
defer r.Body.Close()
|
||||
|
||||
return r.StatusCode != 204, nil
|
||||
expectedResponse := "response " + chal
|
||||
validResponse := r.Header.Get("X-Tailscale-Response") == expectedResponse
|
||||
|
||||
c.logf("[v2] checkCaptivePortal url=%q status_code=%d valid_response=%v", req.URL.String(), r.StatusCode, validResponse)
|
||||
return r.StatusCode != 204 || !validResponse, nil
|
||||
}
|
||||
|
||||
// runHTTPOnlyChecks is the netcheck done by environments that can
|
||||
|
||||
@@ -6,6 +6,7 @@ package nettest
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/netip"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -32,20 +33,38 @@ func NewConn(name string, maxBuf int) (Conn, Conn) {
|
||||
return &connHalf{r: r, w: w}, &connHalf{r: w, w: r}
|
||||
}
|
||||
|
||||
// NewTCPConn creates a pair of Conns that are wired together by pipes.
|
||||
func NewTCPConn(src, dst netip.AddrPort, maxBuf int) (local Conn, remote Conn) {
|
||||
r := NewPipe(src.String(), maxBuf)
|
||||
w := NewPipe(dst.String(), maxBuf)
|
||||
|
||||
lAddr := net.TCPAddrFromAddrPort(src)
|
||||
rAddr := net.TCPAddrFromAddrPort(dst)
|
||||
|
||||
return &connHalf{r: r, w: w, remote: rAddr, local: lAddr}, &connHalf{r: w, w: r, remote: lAddr, local: rAddr}
|
||||
}
|
||||
|
||||
type connAddr string
|
||||
|
||||
func (a connAddr) Network() string { return "mem" }
|
||||
func (a connAddr) String() string { return string(a) }
|
||||
|
||||
type connHalf struct {
|
||||
r, w *Pipe
|
||||
local, remote net.Addr
|
||||
r, w *Pipe
|
||||
}
|
||||
|
||||
func (c *connHalf) LocalAddr() net.Addr {
|
||||
if c.local != nil {
|
||||
return c.local
|
||||
}
|
||||
return connAddr(c.r.name)
|
||||
}
|
||||
|
||||
func (c *connHalf) RemoteAddr() net.Addr {
|
||||
if c.remote != nil {
|
||||
return c.remote
|
||||
}
|
||||
return connAddr(c.w.name)
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ const (
|
||||
bufferSize = 256 * 1024
|
||||
)
|
||||
|
||||
// Listener is a net.Listener using using NewConn to create pairs of network
|
||||
// Listener is a net.Listener using NewConn to create pairs of network
|
||||
// connections connected in memory using a buffered pipe. It also provides a
|
||||
// Dial method to establish new connections.
|
||||
type Listener struct {
|
||||
|
||||
@@ -195,7 +195,7 @@ const (
|
||||
// given interface.
|
||||
// The iface param determines which interface to check against, "" means to check
|
||||
// global config.
|
||||
// It tries to lookup the value directly from `/proc/sys`, and fallsback to
|
||||
// It tries to lookup the value directly from `/proc/sys`, and falls back to
|
||||
// using `sysctl` on failure.
|
||||
func ipForwardingEnabledLinux(p protocol, iface string) (bool, error) {
|
||||
k := ipForwardSysctlKey(slashFormat, p, iface)
|
||||
|
||||
@@ -40,7 +40,7 @@ type Header interface {
|
||||
}
|
||||
|
||||
// HeaderChecksummer is implemented by Header implementations that
|
||||
// need to do a checksum over their paylods.
|
||||
// need to do a checksum over their payloads.
|
||||
type HeaderChecksummer interface {
|
||||
Header
|
||||
|
||||
|
||||
@@ -202,7 +202,7 @@ func TestPingerMismatch(t *testing.T) {
|
||||
|
||||
func mockPinger(t *testing.T, clock *tstest.Clock) (*Pinger, func()) {
|
||||
// In tests, we use UDP so that we can test without being root; this
|
||||
// doesn't matter becuase we mock out the ICMP reply below to be a real
|
||||
// doesn't matter because we mock out the ICMP reply below to be a real
|
||||
// ICMP echo reply packet.
|
||||
conn, err := net.ListenPacket("udp4", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
|
||||
@@ -19,7 +19,7 @@ import (
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
// TestIGD is an IGD (Intenet Gateway Device) for testing. It supports fake
|
||||
// TestIGD is an IGD (Internet Gateway Device) for testing. It supports fake
|
||||
// implementations of NAT-PMP, PCP, and/or UPnP to test clients against.
|
||||
type TestIGD struct {
|
||||
upnpConn net.PacketConn // for UPnP discovery
|
||||
|
||||
@@ -122,7 +122,7 @@ func DERPMapOf(stun ...string) *tailcfg.DERPMap {
|
||||
node := &tailcfg.DERPNode{
|
||||
Name: fmt.Sprint(regionID) + "a",
|
||||
RegionID: regionID,
|
||||
HostName: fmt.Sprintf("d%d.invalid", regionID),
|
||||
HostName: fmt.Sprintf("d%d%s", regionID, tailcfg.DotInvalid),
|
||||
IPv4: ipv4,
|
||||
IPv6: ipv6,
|
||||
STUNPort: port,
|
||||
|
||||
@@ -56,10 +56,15 @@ func TailscaleServiceIP() netip.Addr {
|
||||
//
|
||||
// For IPv4, use TailscaleServiceIP.
|
||||
func TailscaleServiceIPv6() netip.Addr {
|
||||
serviceIPv6.Do(func() { mustPrefix(&serviceIPv6.v, "fd7a:115c:a1e0::53/128") })
|
||||
serviceIPv6.Do(func() { mustPrefix(&serviceIPv6.v, TailscaleServiceIPv6String+"/128") })
|
||||
return serviceIPv6.v.Addr()
|
||||
}
|
||||
|
||||
const (
|
||||
TailscaleServiceIPString = "100.100.100.100"
|
||||
TailscaleServiceIPv6String = "fd7a:115c:a1e0::53"
|
||||
)
|
||||
|
||||
// IsTailscaleIP reports whether ip is an IP address in a range that
|
||||
// Tailscale assigns from.
|
||||
func IsTailscaleIP(ip netip.Addr) bool {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user