Compare commits

...

50 Commits

Author SHA1 Message Date
Denton Gentry 9dd89b8c26
VERSION.txt: this is v1.32.3
Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2022-11-21 08:10:58 -08:00
Tom DNetto b7d0a67f5e
ipn/{localapi,ipnserver}: set a CSP for ServeHTMLStatus, refactor host check
Signed-off-by: Tom DNetto <tom@tailscale.com>
(cherry picked from commit 2a991a3541)
2022-11-18 20:30:00 -08:00
Brad Fitzpatrick 7045359322
net/netcheck: deflake (maybe) magicsock's TestNewConn
Updates #6207

Change-Id: I51d200d0b42b9a1ef799d0abfc8d4bd871c50cf2
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
(cherry picked from commit 036334e913)
2022-11-18 09:25:35 -08:00
Brad Fitzpatrick 49ae82e8bd
ipn/ipnserver: validate Host header on debug ServeHTMLStatus status
Updates tailscale/corp#7948

Change-Id: I3a8c64f353af1eeae620812b2700ce4af4fbbc88
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
(cherry picked from commit f18dde6ad1)
2022-11-18 08:23:22 -08:00
Brad Fitzpatrick 7077adc475
ipn/localapi: require POST to add a bugreport marker
The LocalClient.BugReport method already sends it via POST.

Updates tailscale/corp#7948

Change-Id: I98dbd558c99d4296d934baa5ebc97052c7413073
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
(cherry picked from commit a13753ae1e)
2022-11-18 08:23:09 -08:00
Adrian Dewhurst 5e65717c7f ipn/ipnlocal: fix integration test failure in tkaSyncIfNeeded
In main, some of the prefs handling was reworked and some of those
changes were cherry picked to 1.32. This prevents failures for the
internal integration test for TKA that was failing due to an
uninitialized prefs.Persist.

Signed-off-by: Adrian Dewhurst <adrian@tailscale.com>
2022-11-18 11:21:38 -05:00
Mihai Parparita 39d73f9fae ipn/ipnlocal: fix a log line having function pointers instead of values
Followup to using ipn.PrefsView (#6031).

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
2022-11-17 17:40:50 -08:00
Andrew Dunham 6dbfafd75f cmd/tailscale, util/quarantine: set quarantine flags on files from Taildrop
This sets the "com.apple.quarantine" flag on macOS, and the
"Zone.Identifier" alternate data stream on Windows.

Change-Id: If14f805467b0e2963067937d7f34e08ba1d1fa85
Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
(cherry picked from commit 0af61f7c40)
2022-11-17 17:40:50 -08:00
Andrew Dunham f99a3e5fd9 ipn/localapi: set security headers
Change-Id: I028b6ab91229e2f824e5a69856ca9e1844f7486e
Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
2022-11-17 17:40:50 -08:00
Mihai Parparita c68ba18d43 ipn/localapi: also allow localhost as the LocalAPI host
The Mac and iOS LocalAPI clients make requests to it.

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
(cherry picked from commit b3da5de10f)
2022-11-17 17:40:50 -08:00
Maisem Ali 6cee582f67 ipn/ipnlocal: move selfNode from peerAPIServer to peerAPIHandler
The peerAPIHandler is instantiated per PeerAPI call so it is
guaranteed to have the latest selfNode.

Signed-off-by: Maisem Ali <maisem@tailscale.com>
(cherry picked from commit b0736fe6f7)
2022-11-17 17:40:50 -08:00
Brad Fitzpatrick 0cd803e174 ipn/ipnlocal: also accept service IP IPv6 literal in brackets for quad100
The fix in 4fc8538e2 was sufficient for IPv6. Browsers (can?) send the
IPv6 literal, even without a port number, in brackets.

Updates tailscale/corp#7948

Change-Id: I0e429d3de4df8429152c12f251ab140b0c8f6b77
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
(cherry picked from commit e9c851b04b)
2022-11-17 17:40:50 -08:00
Denton Gentry 6a7e66b666 Fix cherry-pick related build breaks.
ipn/ipnlocal/local.go: must include net/url.
ipn/ipnlocal/peerapi_test.go: remove tailscale.com/util/must

Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2022-11-17 17:40:50 -08:00
Maisem Ali 7a3a1e3e68 tailcfg: add CapabilityDebug
Updates tailscale/corp#7948

Signed-off-by: Maisem Ali <maisem@tailscale.com>
(cherry picked from commit 296e712591)
2022-11-17 17:40:50 -08:00
Maisem Ali 637b4b72c0 ipn/ipnlocal: add some validation to PeerAPI
Updates tailscale/corp#7948

Signed-off-by: Maisem Ali <maisem@tailscale.com>
(cherry picked from commit 1e78fc462c)
2022-11-17 17:40:50 -08:00
Brad Fitzpatrick af0d2bd407 ipn/ipnlocal: move LocalBackend.validPopBrowserURL empty check from caller
I was too late with review feedback to 513780f4f8.

Updates tailscale/corp#7948

Change-Id: I8fa3b4eba4efaff591a2d0bfe6ab4795638b7c3a
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
(cherry picked from commit 1b56acf513)
2022-11-17 17:40:50 -08:00
Maisem Ali ca827b9a04 ipn/ipnlocal: move URL validation to LocalBackend
Updates tailscale/corp#7948

Signed-off-by: Maisem Ali <maisem@tailscale.com>
(cherry picked from commit 513780f4f8)
2022-11-17 17:40:50 -08:00
Andrew Dunham 5cb9db9950 ipn/localapi: serve files with application/octet-stream Content-Type
Updates tailscale/corp#7948

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: I5f570c04974598c7abae4017e4a7a0f63492c87c
(cherry picked from commit 4caca8619e)
2022-11-17 17:40:50 -08:00
Brad Fitzpatrick df91c8f153 ipn/ipnlocal: check quad100 Host header in info page
Updates tailscale/corp#7948

Change-Id: I0ab61c764bff9ba8afaf9070db73e971eb018477
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
(cherry picked from commit 4fc8538e2f)
2022-11-17 17:40:50 -08:00
Brad Fitzpatrick 7c1ba770be client/tailscale/apitype: add LocalAPIHost const, use it
Removes duplication.

Updates tailcale/corp#7948

Change-Id: I564c912ecfde31ba2293124bb1316e433c2a10f1
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
(cherry picked from commit 976e88d430)
2022-11-17 17:40:50 -08:00
Brad Fitzpatrick c1522858bb control/controlclient: filter PopBrowserURL values to https schemes
No need for http://, etc. In case a control server sends a bogus value
and GUIs don't also validate.

Updates tailscale/corp#7948

Change-Id: I0b7dd86aa396bdabd88f0c4fe51831fb2ec4175a
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
(cherry picked from commit 97319a1970)
2022-11-17 17:40:50 -08:00
Maisem Ali 1251e128da ipn: make Notify.Prefs be a *ipn.PrefsView
It is currently a `ipn.PrefsView` which means when we do a JSON roundtrip,
we go from an invalid Prefs to a valid one.

This makes it a pointer, which fixes the JSON roundtrip.

This was introduced in 0957bc5af2.

Signed-off-by: Maisem Ali <maisem@tailscale.com>
(cherry picked from commit 6afe26575c)
2022-11-17 17:40:50 -08:00
Maisem Ali 5ab91f8b99 ipn/ipnlocal: make EditPrefs strip private keys before returning
Signed-off-by: Maisem Ali <maisem@tailscale.com>
(cherry picked from commit 9f39c3b10f)
2022-11-17 17:40:50 -08:00
Maisem Ali 3344521e88 types/persist: add PublicNodeKey helper
Signed-off-by: Maisem Ali <maisem@tailscale.com>
(cherry picked from commit a2d15924fb)
2022-11-17 17:40:50 -08:00
Maisem Ali 7dc95765f7 ipn/ipnlocal: use ipn.PrefsView
Signed-off-by: Maisem Ali <maisem@tailscale.com>
(cherry picked from commit 0957bc5af2)
2022-11-17 17:40:50 -08:00
Maisem Ali c7ddb42083 ipn/prefs: add views
Signed-off-by: Maisem Ali <maisem@tailscale.com>
(cherry picked from commit 20324eeebc)
2022-11-17 17:40:50 -08:00
Brad Fitzpatrick 77ba681a7b ipn: remove handle.go
It was unused in this repo. The Windows client used it, but it can move there.

Change-Id: I572816fd80cbbf1b8db734879b6280857d5bd2a7
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
(cherry picked from commit afce773aae)
2022-11-17 17:40:50 -08:00
Denton Gentry 54e8fa172b
VERSION.txt: this is v1.32.2
Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2022-10-26 14:02:53 -07:00
Andrew Dunham 8a9888aea9
net/interfaces: don't dereference null pointer if no destination/netmask
Fixes #6065

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: I7159b8cbb8d5f47c0668cf83e59167f182f1defd
(cherry picked from commit 95f3dd1346)
2022-10-26 13:52:49 -07:00
Jordan Whited 5bdf8e21c8
wgengine/netstack: enable TCP SACK (#6066)
TCP selective acknowledgement can improve throughput by an order
of magnitude in the presence of loss.

Signed-off-by: Jordan Whited <jordan@tailscale.com>
(cherry picked from commit a471681e28)
2022-10-26 13:52:43 -07:00
Peter Cai 78c60b49f7
net/dnscache: Handle 4-in-6 addresses in DNS responses
On Android, the system resolver can return IPv4 addresses as IPv6-mapped
addresses (i.e. `::ffff:a.b.c.d`). After the switch to `net/netip`
(19008a3), this case is no longer handled and a response like this will
be seen as failure to resolve any IPv4 addresses.

Handle this case by simply calling `Unmap()` on the returned IPs. Fixes #5698.

Signed-off-by: Peter Cai <peter@typeblog.net>
(cherry picked from commit 4597ec1037)
2022-10-26 13:52:35 -07:00
Denton Gentry f8497daa68
VERSION.txt: this is v1.32.1
Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2022-10-21 11:37:06 -07:00
Maisem Ali 8023971bff wgengine/router: [linux] add before deleting interface addrs
Deleting may temporarily result in no addrs on the interface, which results in
all other rules (like routes) to get dropped by the OS.

I verified this fixes the problem.

Signed-off-by: Maisem Ali <maisem@tailscale.com>
(cherry picked from commit 74637f2c15)
2022-10-21 08:16:18 -07:00
Andrew Dunham 0cc397e96d cmd/derper, net/netcheck: add challenge/response to generate_204 endpoint
The Lufthansa in-flight wifi generates a synthetic 204 response to the
DERP server's /generate_204 endpoint. This PR adds a basic
challenge/response to the endpoint; something sufficiently complicated
that it's unlikely to be implemented by a captive portal. We can then
check for the expected response to verify whether we're being MITM'd.

Follow-up to #5601

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: I94a68c9a16a7be7290200eea6a549b64f02ff48f
(cherry picked from commit 223126fe5b)
2022-10-21 08:16:18 -07:00
Anton Tolchanov 46235b790d net/interfaces: improve default route detection
Instead of treating any interface with a non-ifscope route as a
potential default gateway, now verify that a given route is
actually a default route (0.0.0.0/0 or ::/0).

Fixes #5879

Signed-off-by: Anton Tolchanov <anton@tailscale.com>
(cherry picked from commit d499afac78)
2022-10-21 08:16:18 -07:00
Anton Tolchanov b6ce364bf7 net/interfaces: deduplicate route table parsing on Darwin and FreeBSD
Signed-off-by: Anton Tolchanov <anton@tailscale.com>
(cherry picked from commit 9c2ad7086c)
2022-10-21 08:16:18 -07:00
Mihai Parparita 78dec82736 net/wsconn: add back custom wrapper for turning a websocket.Conn into a net.Conn
We removed it in #4806 in favor of the built-in functionality from the
nhooyr.io/websocket package. However, it has an issue with deadlines
that has not been fixed yet (see nhooyr/websocket#350). Temporarily
go back to using a custom wrapper (using the fix from our fork) so that
derpers will stop closing connections too aggressively.

Updates #5921

Change-Id: I1597644e8ba47b413e33f2201eab935145566c0e
Signed-off-by: Mihai Parparita <mihai@tailscale.com>
(cherry picked from commit 9d04ffc782)
2022-10-21 08:16:18 -07:00
Brad Fitzpatrick 7c2fdcd028 ipn/ipnlocal: fix E.G.G. port number accounting
Change-Id: Id35461fdde79448372271ba54f6e6af586f2304d
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
(cherry picked from commit 9475801ebe)
2022-10-21 08:16:18 -07:00
Xe Iaso 613d624bea tsnet/examples/tshello: update example for LocalClient method (#5966)
Before this would silently fail if this program was running on a machine
that was not already running Tailscale. This patch changes the WhoIs
call to use the tsnet.Server LocalClient instead of the global tailscale
LocalClient.

Signed-off-by: Xe <xe@tailscale.com>

Change-Id: Ieb830fbce81292acc4c3b4d1b675aa10766a18dc
Signed-off-by: Xe <xe@tailscale.com>
(cherry picked from commit 86c5bddce2)
2022-10-21 08:16:18 -07:00
Andrew Dunham d982963e0b control/controlhttp: try to avoid flakes in TestDialPlan
Updates tailscale/corp#7446

Signed-off-by: Andrew Dunham <andrew@du.nham.ca>
Change-Id: Ifcf3b5176f065c2e67cbb8943f6356dea720a9c5
(cherry picked from commit a4e707bcf0)
2022-10-21 08:16:18 -07:00
Maisem Ali cdf7ae8066 kube: handle 201 as a valid status code.
Fixes tailscale/corp#7478

Signed-off-by: Maisem Ali <maisem@tailscale.com>
(cherry picked from commit af966391c7)
2022-10-21 08:16:18 -07:00
Denton Gentry 30afe38cb9 cmd/tailscale: correct --cpu-profile help text
Signed-off-by: Denton Gentry <dgentry@tailscale.com>
(cherry picked from commit 19dfdeb1bb)
2022-10-21 08:16:18 -07:00
Andrew Dunham 2a6afafc76 cmd/tailscale, ipn: enable debug logs when --report flag is passed to bugreport (#5830)
Change-Id: Id22e9f4a2dcf35cecb9cd19dd844389e38c922ec
Signed-off-by: Andrew Dunham <andrew@tailscale.com>
(cherry picked from commit c32f9f5865)
2022-10-21 08:16:18 -07:00
Tom DNetto 23a664325e ipn/ipnlocal: make tkaSyncIfNeeded exclusive with a mutex
Running corp/ipn#TestNetworkLockE2E has a 1/300 chance of failing, and
deskchecking suggests thats whats happening are two netmaps are racing each
other to be processed through tkaSyncIfNeededLocked. This happens in the
first place because we release b.mu during network RPCs.

To fix this, we make the tka sync logic an exclusive section, so two
netmaps will need to wait for tka sync to complete serially (which is what
we would want anyway, as the second run through probably wont need to
sync).

Signed-off-by: Tom DNetto <tom@tailscale.com>
(cherry picked from commit a515fc517b)
2022-10-21 08:16:18 -07:00
Brad Fitzpatrick b9e1c18578 net/netcheck: fix crash in checkCaptivePortal
If netcheck happens before there's a derpmap.

This seems to only affect Headscale because it doesn't send a derpmap
as early?

Change-Id: I51e0dfca8e40623e04702bc9cc471770ca20d2c2
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
(cherry picked from commit 9a264dac01)
2022-10-21 08:16:18 -07:00
James Tucker a5340a07cf wgengine/router: fix MTU configuration on Windows
Always set the MTU to the Tailscale default MTU. In practice we are
missing applying an MTU for IPv6 on Windows prior to this patch.

This is the simplest patch to fix the problem, the code in here needs
some more refactoring.

Fixes #5914

Signed-off-by: James Tucker <james@tailscale.com>
(cherry picked from commit 4ec6d41682)
2022-10-21 08:16:18 -07:00
Joe Tsai ccca9faaf8 wgengine: fix typo in Engine.PeerForIP (#5912)
Signed-off-by: Joe Tsai <joetsai@digital-static.net>
(cherry picked from commit 49bae7fd5c)
2022-10-21 08:16:18 -07:00
Sonia Appasamy f7c15dd0b0 types/view: add ContainsNonExitSubnetRoutes func
Signed-off-by: Sonia Appasamy <sonia@tailscale.com>
(cherry picked from commit 5363a90272)
2022-10-21 08:16:18 -07:00
Mihai Parparita a780929391 derp/derphttp: fix nil pointer dereference when closing a netcheck client
NewNetcheckClient only initializes a subset of fields of derphttp.Client,
and the Close() call added by #5707 was result in a nil pointer dereference.
Make Close() safe to call when using NewNetcheckClient() too.

Fixes #5919

Signed-off-by: Mihai Parparita <mihai@tailscale.com>
(cherry picked from commit b2855cfd86)
2022-10-13 11:50:39 -07:00
Denton Gentry fc688fe024
VERSION.txt: this is v1.32.0
Signed-off-by: Denton Gentry <dgentry@tailscale.com>
2022-10-12 09:28:49 -07:00
63 changed files with 1674 additions and 791 deletions

View File

@ -1 +1 @@
1.31.0
1.32.3

View File

@ -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

View File

@ -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
}
@ -405,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
}
@ -442,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
}
@ -565,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
}

View File

@ -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+

View File

@ -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) {

View File

@ -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)
}
})
}
}

View File

@ -13,6 +13,7 @@ import (
"nhooyr.io/websocket"
"tailscale.com/derp"
"tailscale.com/net/wsconn"
)
var counterWebSocketAccepts = expvar.NewInt("derp_websocket_accepts")
@ -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)
})

View File

@ -41,29 +41,44 @@ func runBugReport(ctx context.Context, args []string) error {
default:
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
}
if bugReportArgs.record {
outln("The initial bugreport is below; please reproduce your issue and then press Enter...")
}
outln(logMarker)
if bugReportArgs.record {
fmt.Scanln()
logMarker, err := localClient.BugReportWithOpts(ctx, tailscale.BugReportOpts{})
if !bugReportArgs.record {
// Simple, non-record case
logMarker, err := localClient.BugReportWithOpts(ctx, opts)
if err != nil {
return err
}
outln(logMarker)
outln("Please provide both bugreport markers above to the support team or GitHub issue.")
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
}

View File

@ -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

View File

@ -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()

View File

@ -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
}

View File

@ -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) {

View File

@ -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+
@ -169,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+

View File

@ -241,6 +241,7 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de
💣 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+

View File

@ -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,

View File

@ -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()

View File

@ -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() {

View File

@ -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()

View File

@ -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}
@ -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()

View File

@ -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
}

View File

@ -67,7 +67,7 @@ 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 current wireguard stats
BrowseToURL *string // if non-nil, UI should open a browser right now
@ -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 {

View File

@ -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)

View File

@ -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 100644
View 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
}{})

View File

@ -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")

View File

@ -12,6 +12,7 @@ import (
"net"
"net/http"
"net/netip"
"net/url"
"os"
"os/user"
"path/filepath"
@ -147,7 +148,7 @@ type LocalBackend struct {
ccAuto *controlclient.Auto // if cc is of type *controlclient.Auto
stateKey ipn.StateKey // computed in part from user-provided value
userID string // current controlling user ID (for Windows, primarily)
prefs *ipn.Prefs
prefs ipn.PrefsView // may not be Valid.
inServerMode bool
machinePrivKey key.MachinePrivate
nlPrivKey key.NLPrivate
@ -198,6 +199,14 @@ type LocalBackend struct {
// dialPlan is any dial plan that we've received from the control
// server during a previous connection; it is cleared on logout.
dialPlan atomic.Pointer[tailcfg.ControlDialPlan]
// tkaSyncLock is used to make tkaSyncIfNeeded an exclusive
// section. This is needed to stop two map-responses in quick succession
// from racing each other through TKA sync logic / RPCs.
//
// tkaSyncLock MUST be taken before mu (or inversely, mu must not be held
// at the moment that tkaSyncLock is taken).
tkaSyncLock sync.Mutex
}
// clientGen is a func that creates a control plane client.
@ -355,6 +364,21 @@ func (b *LocalBackend) SetComponentDebugLogging(component string, until time.Tim
return nil
}
// GetComponentDebugLogging gets the time that component's debug logging is
// enabled until, or the zero time if component's time is not currently
// enabled.
func (b *LocalBackend) GetComponentDebugLogging(component string) time.Time {
b.mu.Lock()
defer b.mu.Unlock()
now := time.Now()
ls := b.componentLogUntil[component]
if ls.until.IsZero() || ls.until.Before(now) {
return time.Time{}
}
return ls.until
}
// Dialer returns the backend's dialer.
func (b *LocalBackend) Dialer() *tsdial.Dialer {
return b.dialer
@ -469,17 +493,23 @@ func (b *LocalBackend) Shutdown() {
b.e.Wait()
}
func stripKeysFromPrefs(p ipn.PrefsView) ipn.PrefsView {
if !p.Valid() || p.Persist() == nil {
return p
}
p2 := p.AsStruct()
p2.Persist.LegacyFrontendPrivateMachineKey = key.MachinePrivate{}
p2.Persist.PrivateNodeKey = key.NodePrivate{}
p2.Persist.OldPrivateNodeKey = key.NodePrivate{}
return p2.View()
}
// Prefs returns a copy of b's current prefs, with any private keys removed.
func (b *LocalBackend) Prefs() *ipn.Prefs {
func (b *LocalBackend) Prefs() ipn.PrefsView {
b.mu.Lock()
defer b.mu.Unlock()
p := b.prefs.Clone()
if p != nil && p.Persist != nil {
p.Persist.LegacyFrontendPrivateMachineKey = key.MachinePrivate{}
p.Persist.PrivateNodeKey = key.NodePrivate{}
p.Persist.OldPrivateNodeKey = key.NodePrivate{}
}
return p
return stripKeysFromPrefs(b.prefs)
}
// Status returns the latest status of the backend and its
@ -536,14 +566,14 @@ func (b *LocalBackend) updateStatus(sb *ipnstate.StatusBuilder, extraLocked func
s.CurrentTailnet.MagicDNSSuffix = b.netMap.MagicDNSSuffix()
s.CurrentTailnet.MagicDNSEnabled = b.netMap.DNS.Proxied
s.CurrentTailnet.Name = b.netMap.Domain
if b.prefs != nil && !b.prefs.ExitNodeID.IsZero() {
if exitPeer, ok := b.netMap.PeerWithStableID(b.prefs.ExitNodeID); ok {
if b.prefs.Valid() && !b.prefs.ExitNodeID().IsZero() {
if exitPeer, ok := b.netMap.PeerWithStableID(b.prefs.ExitNodeID()); ok {
var online = false
if exitPeer.Online != nil {
online = *exitPeer.Online
}
s.ExitNodeStatus = &ipnstate.ExitNodeStatus{
ID: b.prefs.ExitNodeID,
ID: b.prefs.ExitNodeID(),
Online: online,
TailscaleIPs: exitPeer.Addresses,
}
@ -625,7 +655,7 @@ func (b *LocalBackend) populatePeerStatusLocked(sb *ipnstate.StatusBuilder) {
LastSeen: lastSeen,
Online: p.Online != nil && *p.Online,
ShareeNode: p.Hostinfo.ShareeNode(),
ExitNode: p.StableID != "" && p.StableID == b.prefs.ExitNodeID,
ExitNode: p.StableID != "" && p.StableID == b.prefs.ExitNodeID(),
ExitNodeOption: exitNodeOption,
SSH_HostKeys: p.Hostinfo.SSH_HostKeys().AsSlice(),
})
@ -754,7 +784,7 @@ func (b *LocalBackend) setClientStatus(st controlclient.Status) {
b.e.SetNetworkMap(new(netmap.NetworkMap))
}
prefs := b.prefs
prefs := b.prefs.AsStruct()
stateKey := b.stateKey
netMap := b.netMap
interact := b.interact
@ -769,19 +799,22 @@ func (b *LocalBackend) setClientStatus(st controlclient.Status) {
prefsChanged = true
}
if st.Persist != nil {
if !b.prefs.Persist.Equals(st.Persist) {
if !prefs.Persist.Equals(st.Persist) {
prefsChanged = true
b.prefs.Persist = st.Persist.Clone()
prefs.Persist = st.Persist.Clone()
}
}
if st.NetMap != nil {
if err := b.tkaSyncIfNeededLocked(st.NetMap); err != nil {
b.mu.Unlock() // respect locking rules for tkaSyncIfNeeded
if err := b.tkaSyncIfNeeded(st.NetMap, prefs.View()); err != nil {
b.logf("[v1] TKA sync error: %v", err)
}
b.mu.Lock()
if !envknob.TKASkipSignatureCheck() {
b.tkaFilterNetmapLocked(st.NetMap)
}
if b.findExitNodeIDLocked(st.NetMap) {
if findExitNodeIDLocked(prefs, st.NetMap) {
prefsChanged = true
}
b.setNetMapLocked(st.NetMap)
@ -794,18 +827,18 @@ func (b *LocalBackend) setClientStatus(st controlclient.Status) {
// Interactive login finished successfully (URL visited).
// After an interactive login, the user always wants
// WantRunning.
if !b.prefs.WantRunning || b.prefs.LoggedOut {
if !prefs.WantRunning || prefs.LoggedOut {
prefsChanged = true
}
b.prefs.WantRunning = true
b.prefs.LoggedOut = false
prefs.WantRunning = true
prefs.LoggedOut = false
}
// Prefs will be written out; this is not safe unless locked or cloned.
if prefsChanged {
prefs = b.prefs.Clone()
b.prefs = prefs.View()
}
if st.NetMap != nil {
b.updateFilterLocked(st.NetMap, prefs)
b.updateFilterLocked(st.NetMap, b.prefs)
}
b.mu.Unlock()
@ -816,7 +849,8 @@ func (b *LocalBackend) setClientStatus(st controlclient.Status) {
b.logf("Failed to save new controlclient state: %v", err)
}
}
b.send(ipn.Notify{Prefs: prefs})
p := prefs.View()
b.send(ipn.Notify{Prefs: &p})
}
if st.NetMap != nil {
if netMap != nil {
@ -848,9 +882,9 @@ func (b *LocalBackend) setClientStatus(st controlclient.Status) {
b.authReconfig()
}
// findExitNodeIDLocked updates b.prefs to reference an exit node by ID,
// rather than by IP. It returns whether prefs was mutated.
func (b *LocalBackend) findExitNodeIDLocked(nm *netmap.NetworkMap) (prefsChanged bool) {
// findExitNodeIDLocked updates prefs to reference an exit node by ID, rather
// than by IP. It returns whether prefs was mutated.
func findExitNodeIDLocked(prefs *ipn.Prefs, nm *netmap.NetworkMap) (prefsChanged bool) {
if nm == nil {
// No netmap, can't resolve anything.
return false
@ -858,30 +892,30 @@ func (b *LocalBackend) findExitNodeIDLocked(nm *netmap.NetworkMap) (prefsChanged
// If we have a desired IP on file, try to find the corresponding
// node.
if !b.prefs.ExitNodeIP.IsValid() {
if !prefs.ExitNodeIP.IsValid() {
return false
}
// IP takes precedence over ID, so if both are set, clear ID.
if b.prefs.ExitNodeID != "" {
b.prefs.ExitNodeID = ""
if prefs.ExitNodeID != "" {
prefs.ExitNodeID = ""
prefsChanged = true
}
for _, peer := range nm.Peers {
for _, addr := range peer.Addresses {
if !addr.IsSingleIP() || addr.Addr() != b.prefs.ExitNodeIP {
if !addr.IsSingleIP() || addr.Addr() != prefs.ExitNodeIP {
continue
}
// Found the node being referenced, upgrade prefs to
// reference it directly for next time.
b.prefs.ExitNodeID = peer.StableID
b.prefs.ExitNodeIP = netip.Addr{}
prefs.ExitNodeID = peer.StableID
prefs.ExitNodeIP = netip.Addr{}
return true
}
}
return false
return prefsChanged
}
// setWgengineStatus is the callback by the wireguard engine whenever it posts a new status.
@ -1046,10 +1080,11 @@ func (b *LocalBackend) Start(opts ipn.Options) error {
nm := b.netMap
state := b.state
b.mu.Unlock()
p := b.prefs
b.send(ipn.Notify{
State: &state,
NetMap: nm,
Prefs: b.prefs,
Prefs: &p,
LoginFinished: new(empty.Message),
})
return nil
@ -1088,8 +1123,8 @@ func (b *LocalBackend) Start(opts ipn.Options) error {
if opts.UpdatePrefs != nil {
newPrefs := opts.UpdatePrefs
newPrefs.Persist = b.prefs.Persist
b.prefs = newPrefs
newPrefs.Persist = b.prefs.Persist()
b.prefs = newPrefs.View()
if opts.StateKey != "" {
if err := b.store.WriteState(opts.StateKey, b.prefs.ToBytes()); err != nil {
@ -1099,7 +1134,7 @@ func (b *LocalBackend) Start(opts ipn.Options) error {
b.setAtomicValuesFromPrefs(b.prefs)
}
wantRunning := b.prefs.WantRunning
wantRunning := b.prefs.WantRunning()
if wantRunning {
if err := b.initMachineKeyLocked(); err != nil {
return fmt.Errorf("initMachineKeyLocked: %w", err)
@ -1109,9 +1144,9 @@ func (b *LocalBackend) Start(opts ipn.Options) error {
return fmt.Errorf("initNLKeyLocked: %w", err)
}
loggedOut := b.prefs.LoggedOut
loggedOut := b.prefs.LoggedOut()
b.inServerMode = b.prefs.ForceDaemon
b.inServerMode = b.prefs.ForceDaemon()
b.serverURL = b.prefs.ControlURLOrDefault()
if b.inServerMode || runtime.GOOS == "windows" {
b.logf("Start: serverMode=%v", b.inServerMode)
@ -1119,8 +1154,8 @@ func (b *LocalBackend) Start(opts ipn.Options) error {
b.applyPrefsToHostinfo(hostinfo, b.prefs)
b.setNetMapLocked(nil)
persistv := b.prefs.Persist
b.updateFilterLocked(nil, nil)
persistv := b.prefs.Persist()
b.updateFilterLocked(nil, ipn.PrefsView{})
b.mu.Unlock()
if b.portpoll != nil {
@ -1204,13 +1239,13 @@ func (b *LocalBackend) Start(opts ipn.Options) error {
b.e.SetNetInfoCallback(b.setNetInfo)
b.mu.Lock()
prefs := b.prefs.Clone()
prefs := b.prefs
b.mu.Unlock()
blid := b.backendLogID
b.logf("Backend: logs: be:%v fe:%v", blid, opts.FrontendLogID)
b.send(ipn.Notify{BackendLogID: &blid})
b.send(ipn.Notify{Prefs: prefs})
b.send(ipn.Notify{Prefs: &prefs})
if !loggedOut && b.hasNodeKey() {
// Even if !WantRunning, we should verify our key, if there
@ -1226,7 +1261,7 @@ func (b *LocalBackend) Start(opts ipn.Options) error {
// given netMap and user preferences.
//
// b.mu must be held.
func (b *LocalBackend) updateFilterLocked(netMap *netmap.NetworkMap, prefs *ipn.Prefs) {
func (b *LocalBackend) updateFilterLocked(netMap *netmap.NetworkMap, prefs ipn.PrefsView) {
// NOTE(danderson): keep change detection as the first thing in
// this function. Don't try to optimize by returning early, more
// likely than not you'll just end up breaking the change
@ -1238,7 +1273,7 @@ func (b *LocalBackend) updateFilterLocked(netMap *netmap.NetworkMap, prefs *ipn.
packetFilter []filter.Match
localNetsB netipx.IPSetBuilder
logNetsB netipx.IPSetBuilder
shieldsUp = prefs == nil || prefs.ShieldsUp // Be conservative when not ready
shieldsUp = !prefs.Valid() || prefs.ShieldsUp() // Be conservative when not ready
)
// Log traffic for Tailscale IPs.
logNetsB.AddPrefix(tsaddr.CGNATRange())
@ -1251,8 +1286,10 @@ func (b *LocalBackend) updateFilterLocked(netMap *netmap.NetworkMap, prefs *ipn.
}
packetFilter = netMap.PacketFilter
}
if prefs != nil {
for _, r := range prefs.AdvertiseRoutes {
if prefs.Valid() {
ar := prefs.AdvertiseRoutes()
for i := 0; i < ar.Len(); i++ {
r := ar.At(i)
if r.Bits() == 0 {
// When offering a default route to the world, we
// filter out locally reachable LANs, so that the
@ -1603,8 +1640,34 @@ func (b *LocalBackend) popBrowserAuthNow() {
}
}
// validPopBrowserURL reports whether urlStr is a valid value for a
// control server to send in a *URL field.
//
// b.mu must *not* be held.
func (b *LocalBackend) validPopBrowserURL(urlStr string) bool {
if urlStr == "" {
return false
}
u, err := url.Parse(urlStr)
if err != nil {
return false
}
switch u.Scheme {
case "https":
return true
case "http":
b.mu.Lock()
serverURL := b.serverURL
b.mu.Unlock()
// If the control server is using plain HTTP (likely a dev server),
// then permit http://.
return strings.HasPrefix(serverURL, "http://")
}
return false
}
func (b *LocalBackend) tellClientToBrowseToURL(url string) {
if url != "" {
if b.validPopBrowserURL(url) {
b.send(ipn.Notify{BrowseToURL: &url})
}
}
@ -1661,8 +1724,8 @@ func (b *LocalBackend) initMachineKeyLocked() (err error) {
}
var legacyMachineKey key.MachinePrivate
if b.prefs.Persist != nil {
legacyMachineKey = b.prefs.Persist.LegacyFrontendPrivateMachineKey
if b.prefs.Persist() != nil {
legacyMachineKey = b.prefs.Persist().LegacyFrontendPrivateMachineKey
}
keyText, err := b.store.ReadState(ipn.MachineKeyStateKey)
@ -1710,6 +1773,7 @@ func (b *LocalBackend) initMachineKeyLocked() (err error) {
// initNLKeyLocked is called to initialize b.nlPrivKey.
//
// b.prefs must already be initialized.
//
// b.stateKey should be set too, but just for nicer log messages.
// b.mu must be held.
func (b *LocalBackend) initNLKeyLocked() (err error) {
@ -1750,12 +1814,12 @@ func (b *LocalBackend) initNLKeyLocked() (err error) {
// user and prefs. If userID is blank or prefs is blank, no work is done.
//
// b.mu may either be held or not.
func (b *LocalBackend) writeServerModeStartState(userID string, prefs *ipn.Prefs) {
if userID == "" || prefs == nil {
func (b *LocalBackend) writeServerModeStartState(userID string, prefs ipn.PrefsView) {
if userID == "" || !prefs.Valid() {
return
}
if prefs.ForceDaemon {
if prefs.ForceDaemon() {
stateKey := ipn.StateKey("user-" + userID)
if err := b.store.WriteState(ipn.ServerModeStartKey, []byte(stateKey)); err != nil {
b.logf("WriteState error: %v", err)
@ -1800,7 +1864,7 @@ func (b *LocalBackend) loadStateLocked(key ipn.StateKey, prefs *ipn.Prefs) (err
// optional/legacy machine key then it's used as the
// value instead of making up a new one.
b.logf("using frontend prefs: %s", prefs.Pretty())
b.prefs = prefs.Clone()
b.prefs = prefs.Clone().View()
b.writeServerModeStartState(b.userID, b.prefs)
return nil
}
@ -1817,14 +1881,15 @@ func (b *LocalBackend) loadStateLocked(key ipn.StateKey, prefs *ipn.Prefs) (err
bs, err := b.store.ReadState(key)
switch {
case errors.Is(err, ipn.ErrStateNotExist):
b.prefs = ipn.NewPrefs()
b.prefs.WantRunning = false
b.logf("using backend prefs; created empty state for %q: %s", key, b.prefs.Pretty())
prefs := ipn.NewPrefs()
prefs.WantRunning = false
b.logf("using backend prefs; created empty state for %q: %s", key, prefs.Pretty())
b.prefs = prefs.View()
return nil
case err != nil:
return fmt.Errorf("backend prefs: store.ReadState(%q): %v", key, err)
}
b.prefs, err = ipn.PrefsFromBytes(bs)
prefs, err = ipn.PrefsFromBytes(bs)
if err != nil {
b.logf("using backend prefs for %q", key)
return fmt.Errorf("PrefsFromBytes: %v", err)
@ -1836,13 +1901,14 @@ func (b *LocalBackend) loadStateLocked(key ipn.StateKey, prefs *ipn.Prefs) (err
// This makes sure that mobile clients go through the new
// frontends where we're (2021-10-02) doing battery
// optimization work ahead of turning down the old backends.
if b.prefs != nil && b.prefs.ControlURL != "" &&
b.prefs.ControlURL != ipn.DefaultControlURL &&
ipn.IsLoginServerSynonym(b.prefs.ControlURL) {
b.prefs.ControlURL = ""
if prefs != nil && prefs.ControlURL != "" &&
prefs.ControlURL != ipn.DefaultControlURL &&
ipn.IsLoginServerSynonym(prefs.ControlURL) {
prefs.ControlURL = ""
}
b.logf("using backend prefs for %q: %s", key, b.prefs.Pretty())
b.logf("using backend prefs for %q: %s", key, prefs.Pretty())
b.prefs = prefs.View()
b.setAtomicValuesFromPrefs(b.prefs)
@ -1851,13 +1917,13 @@ func (b *LocalBackend) loadStateLocked(key ipn.StateKey, prefs *ipn.Prefs) (err
// setAtomicValuesFromPrefs populates sshAtomicBool and containsViaIPFuncAtomic
// from the prefs p, which may be nil.
func (b *LocalBackend) setAtomicValuesFromPrefs(p *ipn.Prefs) {
b.sshAtomicBool.Store(p != nil && p.RunSSH && envknob.CanSSHD())
func (b *LocalBackend) setAtomicValuesFromPrefs(p ipn.PrefsView) {
b.sshAtomicBool.Store(p.Valid() && p.RunSSH() && envknob.CanSSHD())
if p == nil {
if !p.Valid() {
b.containsViaIPFuncAtomic.Store(tsaddr.NewContainsIPFunc(nil))
} else {
b.containsViaIPFuncAtomic.Store(tsaddr.NewContainsIPFunc(tsaddr.FilterPrefixesCopy(p.AdvertiseRoutes, tsaddr.IsViaPrefix)))
b.containsViaIPFuncAtomic.Store(tsaddr.NewContainsIPFunc(p.AdvertiseRoutes().Filter(tsaddr.IsViaPrefix)))
}
}
@ -2013,10 +2079,10 @@ func (b *LocalBackend) shouldUploadServices() bool {
b.mu.Lock()
defer b.mu.Unlock()
if b.prefs == nil || b.netMap == nil {
if !b.prefs.Valid() || b.netMap == nil {
return false // default to safest setting
}
return !b.prefs.ShieldsUp && b.netMap.CollectServices
return !b.prefs.ShieldsUp() && b.netMap.CollectServices
}
func (b *LocalBackend) SetCurrentUserID(uid string) {
@ -2085,7 +2151,7 @@ func (b *LocalBackend) checkSSHPrefsLocked(p *ipn.Prefs) error {
}
func (b *LocalBackend) sshOnButUnusableHealthCheckMessageLocked() (healthMessage string) {
if b.prefs == nil || !b.prefs.RunSSH {
if !b.prefs.Valid() || !b.prefs.RunSSH() {
return ""
}
if envknob.SSHIgnoreTailnetPolicy() || envknob.SSHPolicyFile() != "" {
@ -2111,43 +2177,45 @@ func (b *LocalBackend) sshOnButUnusableHealthCheckMessageLocked() (healthMessage
}
func (b *LocalBackend) isDefaultServerLocked() bool {
if b.prefs == nil {
if !b.prefs.Valid() {
return true // assume true until set otherwise
}
return b.prefs.ControlURLOrDefault() == ipn.DefaultControlURL
}
func (b *LocalBackend) EditPrefs(mp *ipn.MaskedPrefs) (*ipn.Prefs, error) {
func (b *LocalBackend) EditPrefs(mp *ipn.MaskedPrefs) (ipn.PrefsView, error) {
b.mu.Lock()
if mp.EggSet {
mp.EggSet = false
b.egg = true
go b.doSetHostinfoFilterServices(b.hostinfo.Clone())
}
p0 := b.prefs.Clone()
p1 := b.prefs.Clone()
p0 := b.prefs
p1 := b.prefs.AsStruct()
p1.ApplyEdits(mp)
if err := b.checkPrefsLocked(p1); err != nil {
b.mu.Unlock()
b.logf("EditPrefs check error: %v", err)
return nil, err
return ipn.PrefsView{}, err
}
if p1.RunSSH && !envknob.CanSSHD() {
b.mu.Unlock()
b.logf("EditPrefs requests SSH, but disabled by envknob; returning error")
return nil, errors.New("Tailscale SSH server administratively disabled.")
return ipn.PrefsView{}, errors.New("Tailscale SSH server administratively disabled.")
}
if p1.Equals(p0) {
if p1.View().Equals(p0) {
b.mu.Unlock()
return p1, nil
return stripKeysFromPrefs(p0), nil
}
b.logf("EditPrefs: %v", mp.Pretty())
b.setPrefsLockedOnEntry("EditPrefs", p1) // does a b.mu.Unlock
newPrefs := b.setPrefsLockedOnEntry("EditPrefs", p1) // does a b.mu.Unlock
// Note: don't perform any actions for the new prefs here. Not
// every prefs change goes through EditPrefs. Put your actions
// in setPrefsLocksOnEntry instead.
return p1, nil
// This should return the public prefs, not the private ones.
return stripKeysFromPrefs(newPrefs), nil
}
// SetPrefs saves new user preferences and propagates them throughout
@ -2162,26 +2230,26 @@ func (b *LocalBackend) SetPrefs(newp *ipn.Prefs) {
// setPrefsLockedOnEntry requires b.mu be held to call it, but it
// unlocks b.mu when done. newp ownership passes to this function.
func (b *LocalBackend) setPrefsLockedOnEntry(caller string, newp *ipn.Prefs) {
// It returns a readonly copy of the new prefs.
func (b *LocalBackend) setPrefsLockedOnEntry(caller string, newp *ipn.Prefs) ipn.PrefsView {
netMap := b.netMap
stateKey := b.stateKey
b.setAtomicValuesFromPrefs(newp)
oldp := b.prefs
newp.Persist = oldp.Persist // caller isn't allowed to override this
b.prefs = newp
newp.Persist = oldp.Persist() // caller isn't allowed to override this
// findExitNodeIDLocked returns whether it updated b.prefs, but
// everything in this function treats b.prefs as completely new
// anyway. No-op if no exit node resolution is needed.
b.findExitNodeIDLocked(netMap)
b.inServerMode = newp.ForceDaemon
findExitNodeIDLocked(newp, netMap)
b.prefs = newp.View()
b.setAtomicValuesFromPrefs(b.prefs)
b.inServerMode = b.prefs.ForceDaemon()
// We do this to avoid holding the lock while doing everything else.
newp = b.prefs.Clone()
oldHi := b.hostinfo
newHi := oldHi.Clone()
b.applyPrefsToHostinfo(newHi, newp)
b.applyPrefsToHostinfo(newHi, b.prefs)
b.hostinfo = newHi
hostInfoChanged := !oldHi.Equal(newHi)
userID := b.userID
@ -2189,41 +2257,42 @@ func (b *LocalBackend) setPrefsLockedOnEntry(caller string, newp *ipn.Prefs) {
// [GRINDER STATS LINE] - please don't remove (used for log parsing)
if caller == "SetPrefs" {
b.logf("SetPrefs: %v", newp.Pretty())
b.logf("SetPrefs: %v", b.prefs.Pretty())
}
b.updateFilterLocked(netMap, newp)
b.updateFilterLocked(netMap, b.prefs)
if oldp.ShouldSSHBeRunning() && !newp.ShouldSSHBeRunning() {
if oldp.ShouldSSHBeRunning() && !b.prefs.ShouldSSHBeRunning() {
if b.sshServer != nil {
go b.sshServer.Shutdown()
b.sshServer = nil
}
}
prefs := b.prefs // We can grab the view before unlocking. It can't be mutated.
b.mu.Unlock()
if stateKey != "" {
if err := b.store.WriteState(stateKey, newp.ToBytes()); err != nil {
if err := b.store.WriteState(stateKey, prefs.ToBytes()); err != nil {
b.logf("failed to save new controlclient state: %v", err)
}
}
b.writeServerModeStartState(userID, newp)
b.writeServerModeStartState(userID, prefs)
if netMap != nil {
if login := netMap.UserProfiles[netMap.User].LoginName; login != "" {
if newp.Persist == nil {
if prefs.Persist() == nil {
b.logf("active login: %s", login)
} else if newp.Persist.LoginName != login {
} else if prefs.Persist().LoginName != login {
// Corp issue 461: sometimes the wrong prefs are
// logged; the frontend isn't always getting
// notified (to update its prefs/persist) on
// account switch. Log this while we figure it
// out.
b.logf("active login: %q ([unexpected] corp#461, not %q)", newp.Persist.LoginName, login)
b.logf("active login: %q ([unexpected] corp#461, not %q)", prefs.Persist().LoginName, login)
}
}
}
if oldp.ShieldsUp != newp.ShieldsUp || hostInfoChanged {
if oldp.ShieldsUp() != prefs.ShieldsUp() || hostInfoChanged {
b.doSetHostinfoFilterServices(newHi)
}
@ -2231,18 +2300,19 @@ func (b *LocalBackend) setPrefsLockedOnEntry(caller string, newp *ipn.Prefs) {
b.e.SetDERPMap(netMap.DERPMap)
}
if !oldp.WantRunning && newp.WantRunning {
if !oldp.WantRunning() && prefs.WantRunning() {
b.logf("transitioning to running; doing Login...")
cc.Login(nil, controlclient.LoginDefault)
}
if oldp.WantRunning != newp.WantRunning {
if oldp.WantRunning() != prefs.WantRunning() {
b.stateMachine()
} else {
b.authReconfig()
}
b.send(ipn.Notify{Prefs: newp})
b.send(ipn.Notify{Prefs: &prefs})
return prefs
}
// GetPeerAPIPort returns the port number for the peerapi server
@ -2321,7 +2391,7 @@ func (b *LocalBackend) doSetHostinfoFilterServices(hi *tailcfg.Hostinfo) {
}
peerAPIServices := b.peerAPIServicesLocked()
if b.egg {
peerAPIServices = append(peerAPIServices, tailcfg.Service{Proto: "egg"})
peerAPIServices = append(peerAPIServices, tailcfg.Service{Proto: "egg", Port: 1})
}
b.mu.Unlock()
@ -2383,16 +2453,16 @@ func (b *LocalBackend) authReconfig() {
b.logf("[v1] authReconfig: netmap not yet valid. Skipping.")
return
}
if !prefs.WantRunning {
if !prefs.WantRunning() {
b.logf("[v1] authReconfig: skipping because !WantRunning.")
return
}
var flags netmap.WGConfigFlags
if prefs.RouteAll {
if prefs.RouteAll() {
flags |= netmap.AllowSubnetRoutes
}
if prefs.AllowSingleHosts {
if prefs.AllowSingleHosts() {
flags |= netmap.AllowSingleHosts
}
if hasPAC && disableSubnetsIfPAC {
@ -2405,13 +2475,13 @@ func (b *LocalBackend) authReconfig() {
// Keep the dialer updated about whether we're supposed to use
// an exit node's DNS server (so SOCKS5/HTTP outgoing dials
// can use it for name resolution)
if dohURL, ok := exitNodeCanProxyDNS(nm, prefs.ExitNodeID); ok {
if dohURL, ok := exitNodeCanProxyDNS(nm, prefs.ExitNodeID()); ok {
b.dialer.SetExitDNSDoH(dohURL)
} else {
b.dialer.SetExitDNSDoH("")
}
cfg, err := nmcfg.WGCfg(nm, b.logf, flags, prefs.ExitNodeID)
cfg, err := nmcfg.WGCfg(nm, b.logf, flags, prefs.ExitNodeID())
if err != nil {
b.logf("wgcfg: %v", err)
return
@ -2425,7 +2495,7 @@ func (b *LocalBackend) authReconfig() {
if err == wgengine.ErrNoChanges {
return
}
b.logf("[v1] authReconfig: ra=%v dns=%v 0x%02x: %v", prefs.RouteAll, prefs.CorpDNS, flags, err)
b.logf("[v1] authReconfig: ra=%v dns=%v 0x%02x: %v", prefs.RouteAll(), prefs.CorpDNS(), flags, err)
b.initPeerAPIListener()
}
@ -2467,7 +2537,7 @@ func shouldUseOneCGNATRoute(nm *netmap.NetworkMap, logf logger.Logf, versionOS s
//
// The versionOS is a Tailscale-style version ("iOS", "macOS") and not
// a runtime.GOOS.
func dnsConfigForNetmap(nm *netmap.NetworkMap, prefs *ipn.Prefs, logf logger.Logf, versionOS string) *dns.Config {
func dnsConfigForNetmap(nm *netmap.NetworkMap, prefs ipn.PrefsView, logf logger.Logf, versionOS string) *dns.Config {
dcfg := &dns.Config{
Routes: map[dnsname.FQDN][]*dnstype.Resolver{},
Hosts: map[dnsname.FQDN][]netip.Addr{},
@ -2539,7 +2609,7 @@ func dnsConfigForNetmap(nm *netmap.NetworkMap, prefs *ipn.Prefs, logf logger.Log
dcfg.Hosts[fqdn] = append(dcfg.Hosts[fqdn], ip)
}
if !prefs.CorpDNS {
if !prefs.CorpDNS() {
return dcfg
}
@ -2564,7 +2634,7 @@ func dnsConfigForNetmap(nm *netmap.NetworkMap, prefs *ipn.Prefs, logf logger.Log
// If we're using an exit node and that exit node is new enough (1.19.x+)
// to run a DoH DNS proxy, then send all our DNS traffic through it.
if dohURL, ok := exitNodeCanProxyDNS(nm, prefs.ExitNodeID); ok {
if dohURL, ok := exitNodeCanProxyDNS(nm, prefs.ExitNodeID()); ok {
addDefault([]*dnstype.Resolver{{Addr: dohURL}})
return dcfg
}
@ -2598,7 +2668,7 @@ func dnsConfigForNetmap(nm *netmap.NetworkMap, prefs *ipn.Prefs, logf logger.Log
switch {
case len(dcfg.DefaultResolvers) != 0:
// Default resolvers already set.
case !prefs.ExitNodeID.IsZero():
case !prefs.ExitNodeID().IsZero():
// When using exit nodes, it's very likely the LAN
// resolvers will become unreachable. So, force use of the
// fallback resolvers until we implement DNS forwarding to
@ -2739,7 +2809,6 @@ func (b *LocalBackend) initPeerAPIListener() {
ps := &peerAPIServer{
b: b,
rootDir: fileRoot,
selfNode: selfNode,
directFileMode: b.directFileRoot != "",
directFileDoFinalRename: b.directFileDoFinalRename,
}
@ -2864,16 +2933,16 @@ func ipPrefixLess(ri, rj netip.Prefix) bool {
}
// routerConfig produces a router.Config from a wireguard config and IPN prefs.
func (b *LocalBackend) routerConfig(cfg *wgcfg.Config, prefs *ipn.Prefs, oneCGNATRoute bool) *router.Config {
func (b *LocalBackend) routerConfig(cfg *wgcfg.Config, prefs ipn.PrefsView, oneCGNATRoute bool) *router.Config {
singleRouteThreshold := 10_000
if oneCGNATRoute {
singleRouteThreshold = 1
}
rs := &router.Config{
LocalAddrs: unmapIPPrefixes(cfg.Addresses),
SubnetRoutes: unmapIPPrefixes(prefs.AdvertiseRoutes),
SNATSubnetRoutes: !prefs.NoSNAT,
NetfilterMode: prefs.NetfilterMode,
SubnetRoutes: unmapIPPrefixes(prefs.AdvertiseRoutes().AsSlice()),
SNATSubnetRoutes: !prefs.NoSNAT(),
NetfilterMode: prefs.NetfilterMode(),
Routes: peerRoutes(cfg.Peers, singleRouteThreshold),
}
@ -2888,7 +2957,7 @@ func (b *LocalBackend) routerConfig(cfg *wgcfg.Config, prefs *ipn.Prefs, oneCGNA
// likely to break some functionality, but if the user expressed a
// preference for routing remotely, we want to avoid leaking
// traffic at the expense of functionality.
if prefs.ExitNodeID != "" || prefs.ExitNodeIP.IsValid() {
if prefs.ExitNodeID() != "" || prefs.ExitNodeIP().IsValid() {
var default4, default6 bool
for _, route := range rs.Routes {
switch route {
@ -2913,7 +2982,7 @@ func (b *LocalBackend) routerConfig(cfg *wgcfg.Config, prefs *ipn.Prefs, oneCGNA
}
if runtime.GOOS == "linux" || runtime.GOOS == "darwin" || runtime.GOOS == "windows" {
rs.LocalRoutes = internalIPs // unconditionally allow access to guest VM networks
if prefs.ExitNodeAllowLANAccess {
if prefs.ExitNodeAllowLANAccess() {
rs.LocalRoutes = append(rs.LocalRoutes, externalIPs...)
} else {
// Explicitly add routes to the local network so that we do not
@ -2945,16 +3014,16 @@ func unmapIPPrefixes(ippsList ...[]netip.Prefix) (ret []netip.Prefix) {
}
// Warning: b.mu might be held. Currently (2022-02-17) both callers hold it.
func (b *LocalBackend) applyPrefsToHostinfo(hi *tailcfg.Hostinfo, prefs *ipn.Prefs) {
if h := prefs.Hostname; h != "" {
func (b *LocalBackend) applyPrefsToHostinfo(hi *tailcfg.Hostinfo, prefs ipn.PrefsView) {
if h := prefs.Hostname(); h != "" {
hi.Hostname = h
}
hi.RoutableIPs = append(prefs.AdvertiseRoutes[:0:0], prefs.AdvertiseRoutes...)
hi.RequestTags = append(prefs.AdvertiseTags[:0:0], prefs.AdvertiseTags...)
hi.ShieldsUp = prefs.ShieldsUp
hi.RoutableIPs = prefs.AdvertiseRoutes().AsSlice()
hi.RequestTags = prefs.AdvertiseTags().AsSlice()
hi.ShieldsUp = prefs.ShieldsUp()
var sshHostKeys []string
if prefs.RunSSH && envknob.CanSSHD() {
if prefs.RunSSH() && envknob.CanSSHD() {
// TODO(bradfitz): this is called with b.mu held. Not ideal.
// If the filesystem gets wedged or something we could block for
// a long time. But probably fine.
@ -2990,7 +3059,7 @@ func (b *LocalBackend) enterState(newState ipn.State) {
// prefs may change irrespective of state; WantRunning should be explicitly
// set before potential early return even if the state is unchanged.
health.SetIPNState(newState.String(), prefs.WantRunning)
health.SetIPNState(newState.String(), prefs.WantRunning())
if oldState == newState {
return
}
@ -3031,10 +3100,9 @@ func (b *LocalBackend) enterState(newState ipn.State) {
func (b *LocalBackend) hasNodeKey() bool {
// we can't use b.Prefs(), because it strips the keys, oops!
b.mu.Lock()
p := b.prefs
b.mu.Unlock()
defer b.mu.Unlock()
return p.Persist != nil && !p.Persist.PrivateNodeKey.IsZero()
return b.prefs.Valid() && b.prefs.Persist() != nil && !b.prefs.Persist().PrivateNodeKey.IsZero()
}
// nextState returns the state the backend seems to be in, based on
@ -3047,8 +3115,8 @@ func (b *LocalBackend) nextState() ipn.State {
netMap = b.netMap
state = b.state
blocked = b.blocked
wantRunning = b.prefs.WantRunning
loggedOut = b.prefs.LoggedOut
wantRunning = b.prefs.WantRunning()
loggedOut = b.prefs.LoggedOut()
st = b.engineStatus
keyExpired = b.keyExpired
)
@ -3165,12 +3233,12 @@ func (b *LocalBackend) ResetForClientDisconnect() {
b.stateKey = ""
b.userID = ""
b.setNetMapLocked(nil)
b.prefs = new(ipn.Prefs)
b.prefs = new(ipn.Prefs).View()
b.keyExpired = false
b.authURL = ""
b.authURLSticky = ""
b.activeLogin = ""
b.setAtomicValuesFromPrefs(nil)
b.setAtomicValuesFromPrefs(b.prefs)
}
func (b *LocalBackend) ShouldRunSSH() bool { return b.sshAtomicBool.Load() && envknob.CanSSHD() }
@ -3331,22 +3399,16 @@ func (b *LocalBackend) setNetMapLocked(nm *netmap.NetworkMap) {
func (b *LocalBackend) operatorUserName() string {
b.mu.Lock()
defer b.mu.Unlock()
if b.prefs == nil {
if !b.prefs.Valid() {
return ""
}
return b.prefs.OperatorUser
return b.prefs.OperatorUser()
}
// OperatorUserID returns the current pref's OperatorUser's ID (in
// os/user.User.Uid string form), or the empty string if none.
func (b *LocalBackend) OperatorUserID() string {
b.mu.Lock()
if b.prefs == nil {
b.mu.Unlock()
return ""
}
opUserName := b.prefs.OperatorUser
b.mu.Unlock()
opUserName := b.operatorUserName()
if opUserName == "" {
return ""
}
@ -3367,12 +3429,12 @@ func (b *LocalBackend) TestOnlyPublicKeys() (machineKey key.MachinePublic, nodeK
machinePrivKey := b.machinePrivKey
b.mu.Unlock()
if prefs == nil || machinePrivKey.IsZero() {
if !prefs.Valid() || machinePrivKey.IsZero() {
return
}
mk := machinePrivKey.Public()
nk := prefs.Persist.PrivateNodeKey.Public()
nk := prefs.Persist().PublicNodeKey()
return mk, nk
}
@ -3481,8 +3543,8 @@ func (b *LocalBackend) SetDNS(ctx context.Context, name, value string) error {
b.mu.Lock()
cc := b.ccAuto
if prefs := b.prefs; prefs != nil {
req.NodeKey = prefs.Persist.PrivateNodeKey.Public()
if b.prefs.Valid() {
req.NodeKey = b.prefs.Persist().PublicNodeKey()
}
b.mu.Unlock()
if cc == nil {
@ -3594,11 +3656,13 @@ func (b *LocalBackend) DERPMap() *tailcfg.DERPMap {
func (b *LocalBackend) OfferingExitNode() bool {
b.mu.Lock()
defer b.mu.Unlock()
if b.prefs == nil {
if !b.prefs.Valid() {
return false
}
var def4, def6 bool
for _, r := range b.prefs.AdvertiseRoutes {
ar := b.prefs.AdvertiseRoutes()
for i := 0; i < ar.Len(); i++ {
r := ar.At(i)
if r.Bits() != 0 {
continue
}
@ -3727,10 +3791,7 @@ func (b *LocalBackend) DoNoiseRequest(req *http.Request) (*http.Response, error)
func (b *LocalBackend) tailscaleSSHEnabled() bool {
b.mu.Lock()
defer b.mu.Unlock()
if b.prefs == nil {
return false
}
return b.prefs.RunSSH
return b.prefs.Valid() && b.prefs.RunSSH()
}
func (b *LocalBackend) sshServerOrInit() (_ SSHServer, err error) {
@ -3765,6 +3826,17 @@ func (b *LocalBackend) HandleQuad100Port80Conn(c net.Conn) {
s.Serve(netutil.NewOneConnListener(c, nil))
}
func validQuad100Host(h string) bool {
switch h {
case "",
tsaddr.TailscaleServiceIPString,
tsaddr.TailscaleServiceIPv6String,
"[" + tsaddr.TailscaleServiceIPv6String + "]":
return true
}
return false
}
func (b *LocalBackend) handleQuad100Port80Conn(w http.ResponseWriter, r *http.Request) {
w.Header().Set("X-Frame-Options", "DENY")
w.Header().Set("Content-Security-Policy", "default-src 'self';")
@ -3772,6 +3844,10 @@ func (b *LocalBackend) handleQuad100Port80Conn(w http.ResponseWriter, r *http.Re
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
if !validQuad100Host(r.Host) {
http.Error(w, "bad request", http.StatusBadRequest)
return
}
b.mu.Lock()
defer b.mu.Unlock()

View File

@ -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

View File

@ -17,6 +17,7 @@ import (
"time"
"tailscale.com/envknob"
"tailscale.com/ipn"
"tailscale.com/ipn/ipnstate"
"tailscale.com/tailcfg"
"tailscale.com/tka"
@ -39,6 +40,8 @@ type tkaState struct {
// 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.
@ -70,7 +73,7 @@ func (b *LocalBackend) tkaFilterNetmapLocked(nm *netmap.NetworkMap) {
nm.Peers = peers
}
// tkaSyncIfNeededLocked examines TKA info reported from the control plane,
// tkaSyncIfNeeded examines TKA info reported from the control plane,
// performing the steps necessary to synchronize local tka state.
//
// There are 4 scenarios handled here:
@ -85,14 +88,20 @@ func (b *LocalBackend) tkaFilterNetmapLocked(nm *netmap.NetworkMap) {
// - 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 {
// 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
}
ourNodeKey := b.prefs.Persist.PrivateNodeKey.Public()
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
@ -158,6 +167,8 @@ func toSyncOffer(head string, ancestors []string) (tka.SyncOffer, error) {
// 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 {
@ -331,8 +342,8 @@ func (b *LocalBackend) NetworkLockInit(keys []tka.Key) error {
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() {
@ -443,7 +454,7 @@ func (b *LocalBackend) NetworkLockModify(addKeys, removeKeys []tka.Key) (err err
return nil
}
ourNodeKey := b.prefs.Persist.PrivateNodeKey.Public()
ourNodeKey := b.prefs.Persist().PublicNodeKey()
b.mu.Unlock()
resp, err := b.tkaDoSyncSend(ourNodeKey, aums, true)
b.mu.Lock()

View File

@ -122,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: a1.Head(),
})
b.mu.Unlock()
}, b.prefs)
if err != nil {
t.Errorf("tkaSyncIfNeededLocked() failed: %v", err)
}
@ -221,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)
}
@ -243,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)
}
@ -462,18 +456,16 @@ func TestTKASync(t *testing.T) {
authority: nodeAuthority,
storage: nodeStorage,
},
prefs: &ipn.Prefs{
prefs: (&ipn.Prefs{
Persist: &persist.Persist{PrivateNodeKey: nodePriv},
},
}).View(),
}
// Finally, lets trigger a sync.
b.mu.Lock()
err = b.tkaSyncIfNeededLocked(&netmap.NetworkMap{
err = b.tkaSyncIfNeeded(&netmap.NetworkMap{
TKAEnabled: true,
TKAHead: controlAuthority.Head(),
})
b.mu.Unlock()
}, b.prefs)
if err != nil {
t.Errorf("tkaSyncIfNeededLocked() failed: %v", err)
}

View File

@ -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
@ -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)
}

View File

@ -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 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")
}

View File

@ -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

View File

@ -27,6 +27,7 @@ import (
"sync"
"syscall"
"time"
"unicode"
"go4.org/mem"
"inet.af/peercred"
@ -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?

View File

@ -32,6 +32,7 @@ import (
"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"
@ -129,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 {
@ -148,6 +156,35 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
}
// 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
}
// Allow either localhost or loopback IP hosts.
host, portStr, err := net.SplitHostPort(h)
if err != nil {
return false
}
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) {
@ -231,13 +268,21 @@ 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)
}
hi, _ := json.Marshal(hostinfo.New())
@ -247,11 +292,62 @@ func (h *Handler) serveBugReport(w http.ResponseWriter, r *http.Request) {
} else {
h.logf("user bugreport health: ok")
}
if defBool(r.FormValue("diagnose"), false) {
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) {
@ -450,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 {
@ -549,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)
}

View File

@ -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) {

View File

@ -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 {

View File

@ -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")
}
}

View File

@ -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{}

View File

@ -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() {

View File

@ -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
}

View File

@ -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)
}

View File

@ -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) {

View 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)
}

View File

@ -19,6 +19,7 @@ import (
"net/netip"
"runtime"
"sort"
"strings"
"sync"
"time"
@ -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

View File

@ -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,

View File

@ -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 {

View File

@ -32,12 +32,26 @@ func TestInCrostiniRange(t *testing.T) {
}
}
func TestTailscaleServiceIP(t *testing.T) {
got := TailscaleServiceIP().String()
want := "100.100.100.100"
if got != want {
t.Errorf("got %q; want %q", got, want)
}
if TailscaleServiceIPString != want {
t.Error("TailscaleServiceIPString is not consistent")
}
}
func TestTailscaleServiceIPv6(t *testing.T) {
got := TailscaleServiceIPv6().String()
want := "fd7a:115c:a1e0::53"
if got != want {
t.Errorf("got %q; want %q", got, want)
}
if TailscaleServiceIPv6String != want {
t.Error("TailscaleServiceIPv6String is not consistent")
}
}
func TestChromeOSVMRange(t *testing.T) {

View File

@ -0,0 +1,213 @@
// 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 wsconn contains an adapter type that turns
// a websocket connection into a net.Conn. It a temporary fork of the
// netconn.go file from the nhooyr.io/websocket package while we wait for
// https://github.com/nhooyr/websocket/pull/350 to be merged.
package wsconn
import (
"context"
"fmt"
"io"
"math"
"net"
"os"
"sync"
"sync/atomic"
"time"
"nhooyr.io/websocket"
)
// NetConn converts a *websocket.Conn into a net.Conn.
//
// It's for tunneling arbitrary protocols over WebSockets.
// Few users of the library will need this but it's tricky to implement
// correctly and so provided in the library.
// See https://github.com/nhooyr/websocket/issues/100.
//
// Every Write to the net.Conn will correspond to a message write of
// the given type on *websocket.Conn.
//
// The passed ctx bounds the lifetime of the net.Conn. If cancelled,
// all reads and writes on the net.Conn will be cancelled.
//
// If a message is read that is not of the correct type, the connection
// will be closed with StatusUnsupportedData and an error will be returned.
//
// Close will close the *websocket.Conn with StatusNormalClosure.
//
// When a deadline is hit, the connection will be closed. This is
// different from most net.Conn implementations where only the
// reading/writing goroutines are interrupted but the connection is kept alive.
//
// The Addr methods will return a mock net.Addr that returns "websocket" for Network
// and "websocket/unknown-addr" for String.
//
// A received StatusNormalClosure or StatusGoingAway close frame will be translated to
// io.EOF when reading.
func NetConn(ctx context.Context, c *websocket.Conn, msgType websocket.MessageType) net.Conn {
nc := &netConn{
c: c,
msgType: msgType,
}
var writeCancel context.CancelFunc
nc.writeContext, writeCancel = context.WithCancel(ctx)
nc.writeTimer = time.AfterFunc(math.MaxInt64, func() {
nc.afterWriteDeadline.Store(true)
if nc.writing.Load() {
writeCancel()
}
})
if !nc.writeTimer.Stop() {
<-nc.writeTimer.C
}
var readCancel context.CancelFunc
nc.readContext, readCancel = context.WithCancel(ctx)
nc.readTimer = time.AfterFunc(math.MaxInt64, func() {
nc.afterReadDeadline.Store(true)
if nc.reading.Load() {
readCancel()
}
})
if !nc.readTimer.Stop() {
<-nc.readTimer.C
}
return nc
}
type netConn struct {
c *websocket.Conn
msgType websocket.MessageType
writeTimer *time.Timer
writeContext context.Context
writing atomic.Bool
afterWriteDeadline atomic.Bool
readTimer *time.Timer
readContext context.Context
reading atomic.Bool
afterReadDeadline atomic.Bool
readMu sync.Mutex
eofed bool
reader io.Reader
}
var _ net.Conn = &netConn{}
func (c *netConn) Close() error {
return c.c.Close(websocket.StatusNormalClosure, "")
}
func (c *netConn) Write(p []byte) (int, error) {
if c.afterWriteDeadline.Load() {
return 0, os.ErrDeadlineExceeded
}
if swapped := c.writing.CompareAndSwap(false, true); !swapped {
panic("Concurrent writes not allowed")
}
defer c.writing.Store(false)
err := c.c.Write(c.writeContext, c.msgType, p)
if err != nil {
return 0, err
}
return len(p), nil
}
func (c *netConn) Read(p []byte) (int, error) {
if c.afterReadDeadline.Load() {
return 0, os.ErrDeadlineExceeded
}
c.readMu.Lock()
defer c.readMu.Unlock()
if swapped := c.reading.CompareAndSwap(false, true); !swapped {
panic("Concurrent reads not allowed")
}
defer c.reading.Store(false)
if c.eofed {
return 0, io.EOF
}
if c.reader == nil {
typ, r, err := c.c.Reader(c.readContext)
if err != nil {
switch websocket.CloseStatus(err) {
case websocket.StatusNormalClosure, websocket.StatusGoingAway:
c.eofed = true
return 0, io.EOF
}
return 0, err
}
if typ != c.msgType {
err := fmt.Errorf("unexpected frame type read (expected %v): %v", c.msgType, typ)
c.c.Close(websocket.StatusUnsupportedData, err.Error())
return 0, err
}
c.reader = r
}
n, err := c.reader.Read(p)
if err == io.EOF {
c.reader = nil
err = nil
}
return n, err
}
type websocketAddr struct {
}
func (a websocketAddr) Network() string {
return "websocket"
}
func (a websocketAddr) String() string {
return "websocket/unknown-addr"
}
func (c *netConn) RemoteAddr() net.Addr {
return websocketAddr{}
}
func (c *netConn) LocalAddr() net.Addr {
return websocketAddr{}
}
func (c *netConn) SetDeadline(t time.Time) error {
c.SetWriteDeadline(t)
c.SetReadDeadline(t)
return nil
}
func (c *netConn) SetWriteDeadline(t time.Time) error {
if t.IsZero() {
c.writeTimer.Stop()
} else {
c.writeTimer.Reset(time.Until(t))
}
c.afterWriteDeadline.Store(false)
return nil
}
func (c *netConn) SetReadDeadline(t time.Time) error {
if t.IsZero() {
c.readTimer.Stop()
} else {
c.readTimer.Reset(time.Until(t))
}
c.afterReadDeadline.Store(false)
return nil
}

View File

@ -144,3 +144,6 @@ type DERPNode struct {
// If empty, it's assumed to be the same as the DERP server.
STUNTestIP string `json:",omitempty"`
}
// DotInvalid is a fake DNS TLD used in tests for an invalid hostname.
const DotInvalid = ".invalid"

View File

@ -1627,12 +1627,16 @@ type Oauth2Token struct {
const (
// These are the capabilities that the self node has as listed in
// MapResponse.Node.Capabilities.
//
// We've since started referring to these as "Node Attributes" ("nodeAttrs"
// in the ACL policy file).
CapabilityFileSharing = "https://tailscale.com/cap/file-sharing"
CapabilityAdmin = "https://tailscale.com/cap/is-admin"
CapabilitySSH = "https://tailscale.com/cap/ssh" // feature enabled/available
CapabilitySSHRuleIn = "https://tailscale.com/cap/ssh-rule-in" // some SSH rule reach this node
CapabilityDataPlaneAuditLogs = "https://tailscale.com/cap/data-plane-audit-logs" // feature enabled
CapabilityDebug = "https://tailscale.com/cap/debug" // exposes debug endpoints over the PeerAPI
// Inter-node capabilities as specified in the MapResponse.PacketFilter[].CapGrants.

View File

@ -29,13 +29,20 @@ func main() {
if err != nil {
log.Fatal(err)
}
defer ln.Close()
lc, err := s.LocalClient()
if err != nil {
log.Fatal(err)
}
if *addr == ":443" {
ln = tls.NewListener(ln, &tls.Config{
GetCertificate: tailscale.GetCertificate,
})
}
log.Fatal(http.Serve(ln, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
who, err := tailscale.WhoIs(r.Context(), r.RemoteAddr)
who, err := lc.WhoIs(r.Context(), r.RemoteAddr)
if err != nil {
http.Error(w, err.Error(), 500)
return

View File

@ -12,7 +12,7 @@ import (
"tailscale.com/types/structs"
)
//go:generate go run tailscale.com/cmd/cloner -type=Persist
//go:generate go run tailscale.com/cmd/viewer -type=Persist
// Persist is the JSON type stored on disk on nodes to remember their
// settings between runs.
@ -36,6 +36,11 @@ type Persist struct {
LoginName string
}
// PublicNodeKey returns the public key for the node key.
func (p *Persist) PublicNodeKey() key.NodePublic {
return p.PrivateNodeKey.Public()
}
func (p *Persist) Equals(p2 *Persist) bool {
if p == nil && p2 == nil {
return true
@ -63,7 +68,7 @@ func (p *Persist) Pretty() string {
ok = p.OldPrivateNodeKey.Public()
}
if !p.PrivateNodeKey.IsZero() {
nk = p.PrivateNodeKey.Public()
nk = p.PublicNodeKey()
}
return fmt.Sprintf("Persist{lm=%v, o=%v, n=%v u=%#v}",
mk.ShortString(), ok.ShortString(), nk.ShortString(), p.LoginName)

View File

@ -0,0 +1,80 @@
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Code generated by tailscale/cmd/viewer; DO NOT EDIT.
package persist
import (
"encoding/json"
"errors"
"tailscale.com/types/key"
"tailscale.com/types/structs"
)
//go:generate go run tailscale.com/cmd/cloner -clonefunc=false -type=Persist
// View returns a readonly view of Persist.
func (p *Persist) View() PersistView {
return PersistView{ж: p}
}
// PersistView provides a read-only view over Persist.
//
// Its methods should only be called if `Valid()` returns true.
type PersistView 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.
ж *Persist
}
// Valid reports whether underlying value is non-nil.
func (v PersistView) Valid() bool { return v.ж != nil }
// AsStruct returns a clone of the underlying value which aliases no memory with
// the original.
func (v PersistView) AsStruct() *Persist {
if v.ж == nil {
return nil
}
return v.ж.Clone()
}
func (v PersistView) MarshalJSON() ([]byte, error) { return json.Marshal(v.ж) }
func (v *PersistView) UnmarshalJSON(b []byte) error {
if v.ж != nil {
return errors.New("already initialized")
}
if len(b) == 0 {
return nil
}
var x Persist
if err := json.Unmarshal(b, &x); err != nil {
return err
}
v.ж = &x
return nil
}
func (v PersistView) LegacyFrontendPrivateMachineKey() key.MachinePrivate {
return v.ж.LegacyFrontendPrivateMachineKey
}
func (v PersistView) PrivateNodeKey() key.NodePrivate { return v.ж.PrivateNodeKey }
func (v PersistView) OldPrivateNodeKey() key.NodePrivate { return v.ж.OldPrivateNodeKey }
func (v PersistView) Provider() string { return v.ж.Provider }
func (v PersistView) LoginName() string { return v.ж.LoginName }
// A compilation failure here means this code must be regenerated, with the command at the top of this file.
var _PersistViewNeedsRegeneration = Persist(struct {
_ structs.Incomparable
LegacyFrontendPrivateMachineKey key.MachinePrivate
PrivateNodeKey key.NodePrivate
OldPrivateNodeKey key.NodePrivate
Provider string
LoginName string
}{})

View File

@ -201,6 +201,11 @@ func (v IPPrefixSlice) AsSlice() []netip.Prefix {
return v.ж.AsSlice()
}
// Filter returns a new slice, containing elements of v that match f.
func (v IPPrefixSlice) Filter(f func(netip.Prefix) bool) []netip.Prefix {
return tsaddr.FilterPrefixesCopy(v.ж.ж, f)
}
// PrefixesContainsIP reports whether any IPPrefix contains IP.
func (v IPPrefixSlice) ContainsIP(ip netip.Addr) bool {
return tsaddr.PrefixesContainsIP(v.ж.ж, ip)
@ -216,6 +221,17 @@ func (v IPPrefixSlice) ContainsExitRoutes() bool {
return tsaddr.ContainsExitRoutes(v.ж.ж)
}
// ContainsNonExitSubnetRoutes reports whether v contains Subnet
// Routes other than ExitNode Routes.
func (v IPPrefixSlice) ContainsNonExitSubnetRoutes() bool {
for i := 0; i < v.Len(); i++ {
if v.At(i).Bits() != 0 {
return true
}
}
return false
}
// MarshalJSON implements json.Marshaler.
func (v IPPrefixSlice) MarshalJSON() ([]byte, error) {
return v.ж.MarshalJSON()

View File

@ -0,0 +1,15 @@
// 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 quarantine sets platform specific "quarantine" attributes on files
// that are received from other hosts.
package quarantine
import "os"
// SetOnFile sets the platform-specific quarantine attribute (if any) on the
// provided file.
func SetOnFile(f *os.File) error {
return setQuarantineAttr(f)
}

View File

@ -0,0 +1,57 @@
// 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 quarantine
import (
"fmt"
"os"
"strings"
"time"
"github.com/google/uuid"
"golang.org/x/sys/unix"
)
func setQuarantineAttr(f *os.File) error {
sc, err := f.SyscallConn()
if err != nil {
return err
}
now := time.Now()
// We uppercase the UUID to match what other applications on macOS do
id := strings.ToUpper(uuid.New().String())
// kLSQuarantineTypeOtherDownload; this matches what AirDrop sets when
// receiving a file.
quarantineType := "0001"
// This format is under-documented, but the following links contain a
// reasonably comprehensive overview:
// https://eclecticlight.co/2020/10/29/quarantine-and-the-quarantine-flag/
// https://nixhacker.com/security-protection-in-macos-1/
// https://ilostmynotes.blogspot.com/2012/06/gatekeeper-xprotect-and-quarantine.html
attrData := fmt.Sprintf("%s;%x;%s;%s",
quarantineType, // quarantine value
now.Unix(), // time in hex
"Tailscale", // application
id, // UUID
)
var innerErr error
err = sc.Control(func(fd uintptr) {
innerErr = unix.Fsetxattr(
int(fd),
"com.apple.quarantine", // attr
[]byte(attrData),
0,
)
})
if err != nil {
return err
}
return innerErr
}

View File

@ -0,0 +1,15 @@
// 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.
//go:build !darwin && !windows
package quarantine
import (
"os"
)
func setQuarantineAttr(f *os.File) error {
return nil
}

View File

@ -0,0 +1,30 @@
// 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 quarantine
import (
"os"
"strings"
)
func setQuarantineAttr(f *os.File) error {
// Documentation on this can be found here:
// https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-fscc/6e3f7352-d11c-4d76-8c39-2516a9df36e8
//
// Additional information can be found at:
// https://www.digital-detective.net/forensic-analysis-of-zone-identifier-stream/
// https://bugzilla.mozilla.org/show_bug.cgi?id=1433179
content := strings.Join([]string{
"[ZoneTransfer]",
// "URLZONE_INTERNET"
// https://docs.microsoft.com/en-us/previous-versions/windows/internet-explorer/ie-developer/platform-apis/ms537175(v=vs.85)
"ZoneId=3",
// TODO(andrew): should/could we add ReferrerUrl or HostUrl?
}, "\r\n")
return os.WriteFile(f.Name()+":Zone.Identifier", []byte(content), 0)
}

View File

@ -160,6 +160,11 @@ func Create(logf logger.Logf, tundev *tstun.Wrapper, e wgengine.Engine, mc *magi
NetworkProtocols: []stack.NetworkProtocolFactory{ipv4.NewProtocol, ipv6.NewProtocol},
TransportProtocols: []stack.TransportProtocolFactory{tcp.NewProtocol, udp.NewProtocol, icmp.NewProtocol4, icmp.NewProtocol6},
})
sackEnabledOpt := tcpip.TCPSACKEnabled(true) // TCP SACK is disabled by default
tcpipErr := ipstack.SetTransportProtocolOption(tcp.ProtocolNumber, &sackEnabledOpt)
if tcpipErr != nil {
return nil, fmt.Errorf("could not enable TCP SACK: %v", tcpipErr)
}
linkEP := channel.New(512, mtu, "")
if tcpipProblem := ipstack.CreateNIC(nicID, linkEP); tcpipProblem != nil {
return nil, fmt.Errorf("could not create netstack NIC: %v", tcpipProblem)

View File

@ -24,6 +24,7 @@ import (
"tailscale.com/health"
"tailscale.com/net/interfaces"
"tailscale.com/net/tsaddr"
"tailscale.com/net/tstun"
"tailscale.com/util/multierr"
"tailscale.com/wgengine/winnet"
)
@ -247,7 +248,7 @@ func interfaceFromLUID(luid winipcfg.LUID, flags winipcfg.GAAFlags) (*winipcfg.I
}
func configureInterface(cfg *Config, tun *tun.NativeTun) (retErr error) {
const mtu = 0
const mtu = tstun.DefaultMTU
luid := winipcfg.LUID(tun.LUID())
iface, err := interfaceFromLUID(luid,
// Issue 474: on early boot, when the network is still

View File

@ -1532,25 +1532,10 @@ func cidrDiff(kind string, old map[netip.Prefix]bool, new []netip.Prefix, add, d
ret[cidr] = true
}
var delFail []error
for cidr := range old {
if newMap[cidr] {
continue
}
if err := del(cidr); err != nil {
logf("%s del failed: %v", kind, err)
delFail = append(delFail, err)
} else {
delete(ret, cidr)
}
}
if len(delFail) == 1 {
return ret, delFail[0]
}
if len(delFail) > 0 {
return ret, fmt.Errorf("%d delete %s failures; first was: %w", len(delFail), kind, delFail[0])
}
// We want to add before we delete, so that if there is no overlap, we don't
// end up in a state where we have no addresses on an interface as that
// results in other kernel entities (like routes) pointing to that interface
// to also be deleted.
var addFail []error
for cidr := range newMap {
if old[cidr] {
@ -1571,6 +1556,25 @@ func cidrDiff(kind string, old map[netip.Prefix]bool, new []netip.Prefix, add, d
return ret, fmt.Errorf("%d add %s failures; first was: %w", len(addFail), kind, addFail[0])
}
var delFail []error
for cidr := range old {
if newMap[cidr] {
continue
}
if err := del(cidr); err != nil {
logf("%s del failed: %v", kind, err)
delFail = append(delFail, err)
} else {
delete(ret, cidr)
}
}
if len(delFail) == 1 {
return ret, delFail[0]
}
if len(delFail) > 0 {
return ret, fmt.Errorf("%d delete %s failures; first was: %w", len(delFail), kind, delFail[0])
}
return ret, nil
}

View File

@ -10,6 +10,7 @@ import (
"math/rand"
"net/netip"
"os"
"reflect"
"sort"
"strings"
"sync/atomic"
@ -17,6 +18,7 @@ import (
"github.com/google/go-cmp/cmp"
"github.com/vishvananda/netlink"
"golang.org/x/exp/slices"
"golang.zx2c4.com/wireguard/tun"
"tailscale.com/tstest"
"tailscale.com/types/logger"
@ -839,3 +841,84 @@ Usage: busybox [function [arguments]...]
t.Errorf("version = %q, want %q", got, want)
}
}
func TestCIDRDiff(t *testing.T) {
pfx := func(p ...string) []netip.Prefix {
var ret []netip.Prefix
for _, s := range p {
ret = append(ret, netip.MustParsePrefix(s))
}
return ret
}
tests := []struct {
old []netip.Prefix
new []netip.Prefix
wantAdd []netip.Prefix
wantDel []netip.Prefix
final []netip.Prefix
}{
{
old: nil,
new: pfx("1.1.1.1/32"),
wantAdd: pfx("1.1.1.1/32"),
final: pfx("1.1.1.1/32"),
},
{
old: pfx("1.1.1.1/32"),
new: pfx("1.1.1.1/32"),
final: pfx("1.1.1.1/32"),
},
{
old: pfx("1.1.1.1/32", "2.3.4.5/32"),
new: pfx("1.1.1.1/32"),
wantDel: pfx("2.3.4.5/32"),
final: pfx("1.1.1.1/32"),
},
{
old: pfx("1.1.1.1/32", "2.3.4.5/32"),
new: pfx("1.0.0.0/32", "3.4.5.6/32"),
wantDel: pfx("1.1.1.1/32", "2.3.4.5/32"),
wantAdd: pfx("1.0.0.0/32", "3.4.5.6/32"),
final: pfx("1.0.0.0/32", "3.4.5.6/32"),
},
}
for _, tc := range tests {
om := make(map[netip.Prefix]bool)
for _, p := range tc.old {
om[p] = true
}
var added []netip.Prefix
var deleted []netip.Prefix
fm, err := cidrDiff("test", om, tc.new, func(p netip.Prefix) error {
if len(deleted) > 0 {
t.Error("delete called before add")
}
added = append(added, p)
return nil
}, func(p netip.Prefix) error {
deleted = append(deleted, p)
return nil
}, t.Logf)
if err != nil {
t.Fatal(err)
}
slices.SortFunc(added, func(a, b netip.Prefix) bool { return a.Addr().Less(b.Addr()) })
slices.SortFunc(deleted, func(a, b netip.Prefix) bool { return a.Addr().Less(b.Addr()) })
if !reflect.DeepEqual(added, tc.wantAdd) {
t.Errorf("added = %v, want %v", added, tc.wantAdd)
}
if !reflect.DeepEqual(deleted, tc.wantDel) {
t.Errorf("deleted = %v, want %v", deleted, tc.wantDel)
}
// Check that the final state is correct.
if len(fm) != len(tc.final) {
t.Fatalf("final state = %v, want %v", fm, tc.final)
}
for _, p := range tc.final {
if !fm[p] {
t.Errorf("final state = %v, want %v", fm, tc.final)
}
}
}
}

View File

@ -79,7 +79,7 @@ type Engine interface {
Reconfig(*wgcfg.Config, *router.Config, *dns.Config, *tailcfg.Debug) error
// PeerForIP returns the node to which the provided IP routes,
// if any. If none is found, (nil, nil) is returned.
// if any. If none is found, (nil, false) is returned.
PeerForIP(netip.Addr) (_ PeerForIP, ok bool)
// GetFilter returns the current packet filter, if any.