Compare commits
50 Commits
main
...
release-br
Author | SHA1 | Date |
---|---|---|
![]() |
9dd89b8c26 | |
![]() |
b7d0a67f5e | |
![]() |
7045359322 | |
![]() |
49ae82e8bd | |
![]() |
7077adc475 | |
![]() |
5e65717c7f | |
![]() |
39d73f9fae | |
![]() |
6dbfafd75f | |
![]() |
f99a3e5fd9 | |
![]() |
c68ba18d43 | |
![]() |
6cee582f67 | |
![]() |
0cd803e174 | |
![]() |
6a7e66b666 | |
![]() |
7a3a1e3e68 | |
![]() |
637b4b72c0 | |
![]() |
af0d2bd407 | |
![]() |
ca827b9a04 | |
![]() |
5cb9db9950 | |
![]() |
df91c8f153 | |
![]() |
7c1ba770be | |
![]() |
c1522858bb | |
![]() |
1251e128da | |
![]() |
5ab91f8b99 | |
![]() |
3344521e88 | |
![]() |
7dc95765f7 | |
![]() |
c7ddb42083 | |
![]() |
77ba681a7b | |
![]() |
54e8fa172b | |
![]() |
8a9888aea9 | |
![]() |
5bdf8e21c8 | |
![]() |
78c60b49f7 | |
![]() |
f8497daa68 | |
![]() |
8023971bff | |
![]() |
0cc397e96d | |
![]() |
46235b790d | |
![]() |
b6ce364bf7 | |
![]() |
78dec82736 | |
![]() |
7c2fdcd028 | |
![]() |
613d624bea | |
![]() |
d982963e0b | |
![]() |
cdf7ae8066 | |
![]() |
30afe38cb9 | |
![]() |
2a6afafc76 | |
![]() |
23a664325e | |
![]() |
b9e1c18578 | |
![]() |
a5340a07cf | |
![]() |
ccca9faaf8 | |
![]() |
f7c15dd0b0 | |
![]() |
a780929391 | |
![]() |
fc688fe024 |
|
@ -1 +1 @@
|
|||
1.31.0
|
||||
1.32.3
|
||||
|
|
|
@ -7,6 +7,9 @@ package apitype
|
|||
|
||||
import "tailscale.com/tailcfg"
|
||||
|
||||
// LocalAPIHost is the Host header value used by the LocalAPI.
|
||||
const LocalAPIHost = "local-tailscaled.sock"
|
||||
|
||||
// WhoIsResponse is the JSON type returned by tailscaled debug server's /whois?ip=$IP handler.
|
||||
type WhoIsResponse struct {
|
||||
Node *tailcfg.Node
|
||||
|
|
|
@ -197,7 +197,7 @@ func SetVersionMismatchHandler(f func(clientVer, serverVer string)) {
|
|||
}
|
||||
|
||||
func (lc *LocalClient) send(ctx context.Context, method, path string, wantStatus int, body io.Reader) ([]byte, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, method, "http://local-tailscaled.sock"+path, body)
|
||||
req, err := http.NewRequestWithContext(ctx, method, "http://"+apitype.LocalAPIHost+path, body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -276,6 +276,12 @@ type BugReportOpts struct {
|
|||
// Diagnose specifies whether to print additional diagnostic information to
|
||||
// the logs when generating this bugreport.
|
||||
Diagnose bool
|
||||
|
||||
// Record specifies, if non-nil, whether to perform a bugreport
|
||||
// "recording"–generating an initial log marker, then waiting for
|
||||
// this channel to be closed before finishing the request, which
|
||||
// generates another log marker.
|
||||
Record <-chan struct{}
|
||||
}
|
||||
|
||||
// BugReportWithOpts logs and returns a log marker that can be shared by the
|
||||
|
@ -284,16 +290,40 @@ type BugReportOpts struct {
|
|||
// The opts type specifies options to pass to the Tailscale daemon when
|
||||
// generating this bug report.
|
||||
func (lc *LocalClient) BugReportWithOpts(ctx context.Context, opts BugReportOpts) (string, error) {
|
||||
var qparams url.Values
|
||||
qparams := make(url.Values)
|
||||
if opts.Note != "" {
|
||||
qparams.Set("note", opts.Note)
|
||||
}
|
||||
if opts.Diagnose {
|
||||
qparams.Set("diagnose", "true")
|
||||
}
|
||||
if opts.Record != nil {
|
||||
qparams.Set("record", "true")
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
|
||||
var requestBody io.Reader
|
||||
if opts.Record != nil {
|
||||
pr, pw := io.Pipe()
|
||||
requestBody = pr
|
||||
|
||||
// This goroutine waits for the 'Record' channel to be closed,
|
||||
// and then closes the write end of our pipe to unblock the
|
||||
// reader.
|
||||
go func() {
|
||||
defer pw.Close()
|
||||
select {
|
||||
case <-opts.Record:
|
||||
case <-ctx.Done():
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// lc.send might block if opts.Record != nil; see above.
|
||||
uri := fmt.Sprintf("/localapi/v0/bugreport?%s", qparams.Encode())
|
||||
body, err := lc.send(ctx, "POST", uri, 200, nil)
|
||||
body, err := lc.send(ctx, "POST", uri, 200, requestBody)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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+
|
||||
|
|
|
@ -325,11 +325,31 @@ func main() {
|
|||
}
|
||||
}
|
||||
|
||||
const (
|
||||
noContentChallengeHeader = "X-Tailscale-Challenge"
|
||||
noContentResponseHeader = "X-Tailscale-Response"
|
||||
)
|
||||
|
||||
// For captive portal detection
|
||||
func serveNoContent(w http.ResponseWriter, r *http.Request) {
|
||||
if challenge := r.Header.Get(noContentChallengeHeader); challenge != "" {
|
||||
badChar := strings.IndexFunc(challenge, func(r rune) bool {
|
||||
return !isChallengeChar(r)
|
||||
}) != -1
|
||||
if len(challenge) <= 64 && !badChar {
|
||||
w.Header().Set(noContentResponseHeader, "response "+challenge)
|
||||
}
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
||||
func isChallengeChar(c rune) bool {
|
||||
// Semi-randomly chosen as a limited set of valid characters
|
||||
return ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z') ||
|
||||
('0' <= c && c <= '9') ||
|
||||
c == '.' || c == '-' || c == '_'
|
||||
}
|
||||
|
||||
// probeHandler is the endpoint that js/wasm clients hit to measure
|
||||
// DERP latency, since they can't do UDP STUN queries.
|
||||
func probeHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
|
|
@ -7,6 +7,9 @@ package main
|
|||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"tailscale.com/net/stun"
|
||||
|
@ -67,3 +70,57 @@ func BenchmarkServerSTUN(b *testing.B) {
|
|||
}
|
||||
|
||||
}
|
||||
|
||||
func TestNoContent(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
input string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "no challenge",
|
||||
},
|
||||
{
|
||||
name: "valid challenge",
|
||||
input: "input",
|
||||
want: "response input",
|
||||
},
|
||||
{
|
||||
name: "invalid challenge",
|
||||
input: "foo\x00bar",
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "whitespace invalid challenge",
|
||||
input: "foo bar",
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "long challenge",
|
||||
input: strings.Repeat("x", 65),
|
||||
want: "",
|
||||
},
|
||||
}
|
||||
for _, tt := range testCases {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
req, _ := http.NewRequest("GET", "https://localhost/generate_204", nil)
|
||||
if tt.input != "" {
|
||||
req.Header.Set(noContentChallengeHeader, tt.input)
|
||||
}
|
||||
w := httptest.NewRecorder()
|
||||
serveNoContent(w, req)
|
||||
resp := w.Result()
|
||||
|
||||
if tt.want == "" {
|
||||
if h, found := resp.Header[noContentResponseHeader]; found {
|
||||
t.Errorf("got %+v; expected no response header", h)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if got := resp.Header.Get(noContentResponseHeader); got != tt.want {
|
||||
t.Errorf("got %q; want %q", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ import (
|
|||
|
||||
"nhooyr.io/websocket"
|
||||
"tailscale.com/derp"
|
||||
"tailscale.com/net/wsconn"
|
||||
)
|
||||
|
||||
var counterWebSocketAccepts = expvar.NewInt("derp_websocket_accepts")
|
||||
|
@ -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)
|
||||
})
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -501,7 +501,7 @@ func runUp(ctx context.Context, args []string) (retErr error) {
|
|||
fatalf("%s", err)
|
||||
}
|
||||
if justEditMP != nil {
|
||||
justEditMP.EggSet = true
|
||||
justEditMP.EggSet = egg
|
||||
_, err := localClient.EditPrefs(ctx, justEditMP)
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -474,8 +474,8 @@ func tailscaleUp(ctx context.Context, prefs *ipn.Prefs, forceReauth bool) (authU
|
|||
authURL = *url
|
||||
cancel()
|
||||
}
|
||||
if !forceReauth && n.Prefs != nil {
|
||||
p1, p2 := *n.Prefs, *prefs
|
||||
if !forceReauth && n.Prefs != nil && n.Prefs.Valid() {
|
||||
p1, p2 := n.Prefs.AsStruct(), *prefs
|
||||
p1.Persist = nil
|
||||
p2.Persist = nil
|
||||
if p1.Equals(&p2) {
|
||||
|
|
|
@ -7,6 +7,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
|||
W 💣 github.com/alexbrainman/sspi/negotiate from tailscale.com/net/tshttpproxy
|
||||
github.com/fxamacker/cbor/v2 from tailscale.com/tka
|
||||
github.com/golang/groupcache/lru from tailscale.com/net/dnscache
|
||||
D github.com/google/uuid from tailscale.com/util/quarantine
|
||||
github.com/hdevalence/ed25519consensus from tailscale.com/tka
|
||||
L github.com/josharian/native from github.com/mdlayher/netlink+
|
||||
L 💣 github.com/jsimonetti/rtnetlink from tailscale.com/net/interfaces
|
||||
|
@ -70,6 +71,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
|||
tailscale.com/net/tlsdial from tailscale.com/derp/derphttp+
|
||||
tailscale.com/net/tsaddr from tailscale.com/net/interfaces+
|
||||
💣 tailscale.com/net/tshttpproxy from tailscale.com/derp/derphttp+
|
||||
tailscale.com/net/wsconn from tailscale.com/control/controlhttp+
|
||||
tailscale.com/paths from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/safesocket from tailscale.com/cmd/tailscale/cli+
|
||||
tailscale.com/syncs from tailscale.com/net/netcheck+
|
||||
|
@ -101,6 +103,7 @@ tailscale.com/cmd/tailscale dependencies: (generated by github.com/tailscale/dep
|
|||
tailscale.com/util/lineread from tailscale.com/net/interfaces+
|
||||
tailscale.com/util/mak from tailscale.com/net/netcheck
|
||||
tailscale.com/util/multierr from tailscale.com/control/controlhttp
|
||||
tailscale.com/util/quarantine from tailscale.com/cmd/tailscale/cli
|
||||
tailscale.com/util/singleflight from tailscale.com/net/dnscache
|
||||
L tailscale.com/util/strs from tailscale.com/hostinfo
|
||||
W 💣 tailscale.com/util/winutil from tailscale.com/hostinfo+
|
||||
|
@ -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+
|
||||
|
|
|
@ -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+
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -13,6 +13,7 @@ import (
|
|||
|
||||
"nhooyr.io/websocket"
|
||||
"tailscale.com/control/controlbase"
|
||||
"tailscale.com/net/wsconn"
|
||||
)
|
||||
|
||||
// Variant of Dial that tunnels the request over WebSockets, since we cannot do
|
||||
|
@ -51,7 +52,7 @@ func (d *Dialer) Dial(ctx context.Context) (*controlbase.Conn, error) {
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
netConn := websocket.NetConn(context.Background(), wsConn, websocket.MessageBinary)
|
||||
netConn := wsconn.NetConn(context.Background(), wsConn, websocket.MessageBinary)
|
||||
cbConn, err := cont(ctx, netConn)
|
||||
if err != nil {
|
||||
netConn.Close()
|
||||
|
|
|
@ -459,13 +459,26 @@ func TestDialPlan(t *testing.T) {
|
|||
|
||||
const (
|
||||
testProtocolVersion = 1
|
||||
|
||||
// We need consistent ports for each address; these are chosen
|
||||
// randomly and we hope that they won't conflict during this test.
|
||||
httpPort = "40080"
|
||||
httpsPort = "40443"
|
||||
)
|
||||
|
||||
getRandomPort := func() string {
|
||||
ln, err := net.Listen("tcp", ":0")
|
||||
if err != nil {
|
||||
t.Fatalf("net.Listen: %v", err)
|
||||
}
|
||||
defer ln.Close()
|
||||
_, port, err := net.SplitHostPort(ln.Addr().String())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return port
|
||||
}
|
||||
|
||||
// We need consistent ports for each address; these are chosen
|
||||
// randomly and we hope that they won't conflict during this test.
|
||||
httpPort := getRandomPort()
|
||||
httpsPort := getRandomPort()
|
||||
|
||||
makeHandler := func(t *testing.T, name string, host netip.Addr, wrap func(http.Handler) http.Handler) {
|
||||
done := make(chan struct{})
|
||||
t.Cleanup(func() {
|
||||
|
|
|
@ -14,6 +14,7 @@ import (
|
|||
"nhooyr.io/websocket"
|
||||
"tailscale.com/control/controlbase"
|
||||
"tailscale.com/net/netutil"
|
||||
"tailscale.com/net/wsconn"
|
||||
"tailscale.com/types/key"
|
||||
)
|
||||
|
||||
|
@ -111,7 +112,7 @@ func acceptWebsocket(ctx context.Context, w http.ResponseWriter, r *http.Request
|
|||
return nil, fmt.Errorf("decoding base64 handshake parameter: %v", err)
|
||||
}
|
||||
|
||||
conn := websocket.NetConn(ctx, c, websocket.MessageBinary)
|
||||
conn := wsconn.NetConn(ctx, c, websocket.MessageBinary)
|
||||
nc, err := controlbase.Server(ctx, conn, private, init)
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -22,7 +22,8 @@ func (b *FakeBackend) Start(opts Options) error {
|
|||
}
|
||||
nl := NeedsLogin
|
||||
if b.notify != nil {
|
||||
b.notify(Notify{Prefs: opts.Prefs})
|
||||
p := opts.Prefs.View()
|
||||
b.notify(Notify{Prefs: &p})
|
||||
b.notify(Notify{State: &nl})
|
||||
}
|
||||
return nil
|
||||
|
@ -83,7 +84,8 @@ func (b *FakeBackend) SetPrefs(new *Prefs) {
|
|||
}
|
||||
|
||||
if b.notify != nil {
|
||||
b.notify(Notify{Prefs: new.Clone()})
|
||||
p := new.View()
|
||||
b.notify(Notify{Prefs: &p})
|
||||
}
|
||||
if new.WantRunning && !b.live {
|
||||
b.newState(Starting)
|
||||
|
|
176
ipn/handle.go
176
ipn/handle.go
|
@ -1,176 +0,0 @@
|
|||
// Copyright (c) 2020 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package ipn
|
||||
|
||||
import (
|
||||
"net/netip"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/types/logger"
|
||||
"tailscale.com/types/netmap"
|
||||
)
|
||||
|
||||
type Handle struct {
|
||||
b Backend
|
||||
logf logger.Logf
|
||||
|
||||
// Mutex protects everything below
|
||||
mu sync.Mutex
|
||||
xnotify func(Notify)
|
||||
frontendLogID string
|
||||
netmapCache *netmap.NetworkMap
|
||||
engineStatusCache EngineStatus
|
||||
stateCache State
|
||||
prefsCache *Prefs
|
||||
}
|
||||
|
||||
func NewHandle(b Backend, logf logger.Logf, notify func(Notify), opts Options) (*Handle, error) {
|
||||
h := &Handle{
|
||||
b: b,
|
||||
logf: logf,
|
||||
}
|
||||
|
||||
h.SetNotifyCallback(notify)
|
||||
err := h.Start(opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return h, nil
|
||||
}
|
||||
|
||||
func (h *Handle) SetNotifyCallback(notify func(Notify)) {
|
||||
h.mu.Lock()
|
||||
h.xnotify = notify
|
||||
h.mu.Unlock()
|
||||
|
||||
h.b.SetNotifyCallback(h.notify)
|
||||
}
|
||||
|
||||
func (h *Handle) Start(opts Options) error {
|
||||
h.mu.Lock()
|
||||
h.frontendLogID = opts.FrontendLogID
|
||||
h.netmapCache = nil
|
||||
h.engineStatusCache = EngineStatus{}
|
||||
h.stateCache = NoState
|
||||
if opts.Prefs != nil {
|
||||
h.prefsCache = opts.Prefs.Clone()
|
||||
}
|
||||
h.mu.Unlock()
|
||||
return h.b.Start(opts)
|
||||
}
|
||||
|
||||
func (h *Handle) Reset() {
|
||||
st := NoState
|
||||
h.notify(Notify{State: &st})
|
||||
}
|
||||
|
||||
func (h *Handle) notify(n Notify) {
|
||||
h.mu.Lock()
|
||||
if n.BackendLogID != nil {
|
||||
h.logf("Handle: logs: be:%v fe:%v",
|
||||
*n.BackendLogID, h.frontendLogID)
|
||||
}
|
||||
if n.State != nil {
|
||||
h.stateCache = *n.State
|
||||
}
|
||||
if n.Prefs != nil {
|
||||
h.prefsCache = n.Prefs.Clone()
|
||||
}
|
||||
if n.NetMap != nil {
|
||||
h.netmapCache = n.NetMap
|
||||
}
|
||||
if n.Engine != nil {
|
||||
h.engineStatusCache = *n.Engine
|
||||
}
|
||||
h.mu.Unlock()
|
||||
|
||||
if h.xnotify != nil {
|
||||
// Forward onward to our parent's notifier
|
||||
h.xnotify(n)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handle) Prefs() *Prefs {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
|
||||
return h.prefsCache.Clone()
|
||||
}
|
||||
|
||||
func (h *Handle) UpdatePrefs(updateFn func(p *Prefs)) {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
|
||||
new := h.prefsCache.Clone()
|
||||
updateFn(new)
|
||||
h.prefsCache = new
|
||||
h.b.SetPrefs(new)
|
||||
}
|
||||
|
||||
func (h *Handle) State() State {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
|
||||
return h.stateCache
|
||||
}
|
||||
|
||||
func (h *Handle) EngineStatus() EngineStatus {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
|
||||
return h.engineStatusCache
|
||||
}
|
||||
|
||||
func (h *Handle) LocalAddrs() []netip.Prefix {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
|
||||
nm := h.netmapCache
|
||||
if nm != nil {
|
||||
return nm.Addresses
|
||||
}
|
||||
return []netip.Prefix{}
|
||||
}
|
||||
|
||||
func (h *Handle) NetMap() *netmap.NetworkMap {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
|
||||
return h.netmapCache
|
||||
}
|
||||
|
||||
func (h *Handle) Expiry() time.Time {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
|
||||
nm := h.netmapCache
|
||||
if nm != nil {
|
||||
return nm.Expiry
|
||||
}
|
||||
return time.Time{}
|
||||
}
|
||||
|
||||
func (h *Handle) AdminPageURL() string {
|
||||
return h.prefsCache.AdminPageURL()
|
||||
}
|
||||
|
||||
func (h *Handle) StartLoginInteractive() {
|
||||
h.b.StartLoginInteractive()
|
||||
}
|
||||
|
||||
func (h *Handle) Login(token *tailcfg.Oauth2Token) {
|
||||
h.b.Login(token)
|
||||
}
|
||||
|
||||
func (h *Handle) Logout() {
|
||||
h.b.Logout()
|
||||
}
|
||||
|
||||
func (h *Handle) RequestEngineStatus() {
|
||||
h.b.RequestEngineStatus()
|
||||
}
|
|
@ -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
|
||||
}{})
|
|
@ -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")
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -62,7 +62,7 @@ func TestLocalLogLines(t *testing.T) {
|
|||
defer lb.Shutdown()
|
||||
|
||||
// custom adjustments for required non-nil fields
|
||||
lb.prefs = ipn.NewPrefs()
|
||||
lb.prefs = ipn.NewPrefs().View()
|
||||
lb.hostinfo = &tailcfg.Hostinfo{}
|
||||
// hacky manual override of the usual log-on-change behaviour of keylogf
|
||||
lb.keyLogf = logListen.Logf
|
||||
|
|
|
@ -17,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()
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
|
|
@ -46,6 +46,7 @@ func (nt *notifyThrottler) expect(count int) {
|
|||
|
||||
// put adds one notification into the throttler's queue.
|
||||
func (nt *notifyThrottler) put(n ipn.Notify) {
|
||||
nt.t.Helper()
|
||||
nt.mu.Lock()
|
||||
ch := nt.ch
|
||||
nt.mu.Unlock()
|
||||
|
@ -315,7 +316,7 @@ func TestStateMachine(t *testing.T) {
|
|||
return
|
||||
}
|
||||
if n.State != nil ||
|
||||
n.Prefs != nil ||
|
||||
(n.Prefs != nil && n.Prefs.Valid()) ||
|
||||
n.BrowseToURL != nil ||
|
||||
n.LoginFinished != nil {
|
||||
logf("\n%v\n\n", n)
|
||||
|
@ -344,12 +345,12 @@ func TestStateMachine(t *testing.T) {
|
|||
cc.assertCalls()
|
||||
c.Assert(nn[0].Prefs, qt.IsNotNil)
|
||||
c.Assert(nn[1].State, qt.IsNotNil)
|
||||
prefs := *nn[0].Prefs
|
||||
prefs := nn[0].Prefs
|
||||
// Note: a totally fresh system has Prefs.LoggedOut=false by
|
||||
// default. We are logged out, but not because the user asked
|
||||
// for it, so it doesn't count as Prefs.LoggedOut==true.
|
||||
c.Assert(nn[0].Prefs.LoggedOut, qt.IsFalse)
|
||||
c.Assert(prefs.WantRunning, qt.IsFalse)
|
||||
c.Assert(prefs.LoggedOut(), qt.IsFalse)
|
||||
c.Assert(prefs.WantRunning(), qt.IsFalse)
|
||||
c.Assert(ipn.NeedsLogin, qt.Equals, *nn[1].State)
|
||||
c.Assert(ipn.NeedsLogin, qt.Equals, b.State())
|
||||
}
|
||||
|
@ -369,8 +370,8 @@ func TestStateMachine(t *testing.T) {
|
|||
cc.assertCalls()
|
||||
c.Assert(nn[0].Prefs, qt.IsNotNil)
|
||||
c.Assert(nn[1].State, qt.IsNotNil)
|
||||
c.Assert(nn[0].Prefs.LoggedOut, qt.IsFalse)
|
||||
c.Assert(nn[0].Prefs.WantRunning, qt.IsFalse)
|
||||
c.Assert(nn[0].Prefs.LoggedOut(), qt.IsFalse)
|
||||
c.Assert(nn[0].Prefs.WantRunning(), qt.IsFalse)
|
||||
c.Assert(ipn.NeedsLogin, qt.Equals, *nn[1].State)
|
||||
c.Assert(ipn.NeedsLogin, qt.Equals, b.State())
|
||||
}
|
||||
|
@ -395,7 +396,7 @@ func TestStateMachine(t *testing.T) {
|
|||
// the user needs to visit a login URL.
|
||||
t.Logf("\n\nLogin (url response)")
|
||||
notifies.expect(1)
|
||||
url1 := "http://localhost:1/1"
|
||||
url1 := "https://localhost:1/1"
|
||||
cc.send(nil, url1, false, nil)
|
||||
{
|
||||
cc.assertCalls("unpause")
|
||||
|
@ -406,8 +407,8 @@ func TestStateMachine(t *testing.T) {
|
|||
nn := notifies.drain(1)
|
||||
|
||||
c.Assert(nn[0].Prefs, qt.IsNotNil)
|
||||
c.Assert(nn[0].Prefs.LoggedOut, qt.IsFalse)
|
||||
c.Assert(nn[0].Prefs.WantRunning, qt.IsFalse)
|
||||
c.Assert(nn[0].Prefs.LoggedOut(), qt.IsFalse)
|
||||
c.Assert(nn[0].Prefs.WantRunning(), qt.IsFalse)
|
||||
}
|
||||
|
||||
// Now we'll try an interactive login.
|
||||
|
@ -441,7 +442,7 @@ func TestStateMachine(t *testing.T) {
|
|||
// Provide a new interactive login URL.
|
||||
t.Logf("\n\nLogin2 (url response)")
|
||||
notifies.expect(1)
|
||||
url2 := "http://localhost:1/2"
|
||||
url2 := "https://localhost:1/2"
|
||||
cc.send(nil, url2, false, nil)
|
||||
{
|
||||
cc.assertCalls("unpause", "unpause")
|
||||
|
@ -476,7 +477,7 @@ func TestStateMachine(t *testing.T) {
|
|||
c.Assert(nn[0].LoginFinished, qt.IsNotNil)
|
||||
c.Assert(nn[1].Prefs, qt.IsNotNil)
|
||||
c.Assert(nn[2].State, qt.IsNotNil)
|
||||
c.Assert(nn[1].Prefs.Persist.LoginName, qt.Equals, "user1")
|
||||
c.Assert(nn[1].Prefs.Persist().LoginName, qt.Equals, "user1")
|
||||
c.Assert(ipn.NeedsMachineAuth, qt.Equals, *nn[2].State)
|
||||
}
|
||||
|
||||
|
@ -576,8 +577,8 @@ func TestStateMachine(t *testing.T) {
|
|||
c.Assert(nn[0].State, qt.IsNotNil)
|
||||
c.Assert(nn[1].Prefs, qt.IsNotNil)
|
||||
c.Assert(ipn.Stopped, qt.Equals, *nn[0].State)
|
||||
c.Assert(nn[1].Prefs.LoggedOut, qt.IsTrue)
|
||||
c.Assert(nn[1].Prefs.WantRunning, qt.IsFalse)
|
||||
c.Assert(nn[1].Prefs.LoggedOut(), qt.IsTrue)
|
||||
c.Assert(nn[1].Prefs.WantRunning(), qt.IsFalse)
|
||||
c.Assert(ipn.Stopped, qt.Equals, b.State())
|
||||
c.Assert(store.sawWrite(), qt.IsTrue)
|
||||
}
|
||||
|
@ -592,8 +593,8 @@ func TestStateMachine(t *testing.T) {
|
|||
cc.assertCalls("unpause", "unpause")
|
||||
c.Assert(nn[0].State, qt.IsNotNil)
|
||||
c.Assert(ipn.NeedsLogin, qt.Equals, *nn[0].State)
|
||||
c.Assert(b.Prefs().LoggedOut, qt.IsTrue)
|
||||
c.Assert(b.Prefs().WantRunning, qt.IsFalse)
|
||||
c.Assert(b.Prefs().LoggedOut(), qt.IsTrue)
|
||||
c.Assert(b.Prefs().WantRunning(), qt.IsFalse)
|
||||
c.Assert(ipn.NeedsLogin, qt.Equals, b.State())
|
||||
}
|
||||
|
||||
|
@ -607,8 +608,8 @@ func TestStateMachine(t *testing.T) {
|
|||
// still logged out. So it shouldn't call it again.
|
||||
cc.assertCalls("StartLogout", "unpause")
|
||||
cc.assertCalls()
|
||||
c.Assert(b.Prefs().LoggedOut, qt.IsTrue)
|
||||
c.Assert(b.Prefs().WantRunning, qt.IsFalse)
|
||||
c.Assert(b.Prefs().LoggedOut(), qt.IsTrue)
|
||||
c.Assert(b.Prefs().WantRunning(), qt.IsFalse)
|
||||
c.Assert(ipn.NeedsLogin, qt.Equals, b.State())
|
||||
}
|
||||
|
||||
|
@ -620,8 +621,8 @@ func TestStateMachine(t *testing.T) {
|
|||
{
|
||||
notifies.drain(0)
|
||||
cc.assertCalls("unpause", "unpause")
|
||||
c.Assert(b.Prefs().LoggedOut, qt.IsTrue)
|
||||
c.Assert(b.Prefs().WantRunning, qt.IsFalse)
|
||||
c.Assert(b.Prefs().LoggedOut(), qt.IsTrue)
|
||||
c.Assert(b.Prefs().WantRunning(), qt.IsFalse)
|
||||
c.Assert(ipn.NeedsLogin, qt.Equals, b.State())
|
||||
}
|
||||
|
||||
|
@ -634,8 +635,8 @@ func TestStateMachine(t *testing.T) {
|
|||
{
|
||||
notifies.drain(0)
|
||||
cc.assertCalls("Logout", "unpause")
|
||||
c.Assert(b.Prefs().LoggedOut, qt.IsTrue)
|
||||
c.Assert(b.Prefs().WantRunning, qt.IsFalse)
|
||||
c.Assert(b.Prefs().LoggedOut(), qt.IsTrue)
|
||||
c.Assert(b.Prefs().WantRunning(), qt.IsFalse)
|
||||
c.Assert(ipn.NeedsLogin, qt.Equals, b.State())
|
||||
}
|
||||
|
||||
|
@ -647,8 +648,8 @@ func TestStateMachine(t *testing.T) {
|
|||
{
|
||||
notifies.drain(0)
|
||||
cc.assertCalls("unpause", "unpause")
|
||||
c.Assert(b.Prefs().LoggedOut, qt.IsTrue)
|
||||
c.Assert(b.Prefs().WantRunning, qt.IsFalse)
|
||||
c.Assert(b.Prefs().LoggedOut(), qt.IsTrue)
|
||||
c.Assert(b.Prefs().WantRunning(), qt.IsFalse)
|
||||
c.Assert(ipn.NeedsLogin, qt.Equals, b.State())
|
||||
}
|
||||
|
||||
|
@ -672,8 +673,8 @@ func TestStateMachine(t *testing.T) {
|
|||
cc.assertCalls()
|
||||
c.Assert(nn[0].Prefs, qt.IsNotNil)
|
||||
c.Assert(nn[1].State, qt.IsNotNil)
|
||||
c.Assert(nn[0].Prefs.LoggedOut, qt.IsTrue)
|
||||
c.Assert(nn[0].Prefs.WantRunning, qt.IsFalse)
|
||||
c.Assert(nn[0].Prefs.LoggedOut(), qt.IsTrue)
|
||||
c.Assert(nn[0].Prefs.WantRunning(), qt.IsFalse)
|
||||
c.Assert(ipn.NeedsLogin, qt.Equals, *nn[1].State)
|
||||
c.Assert(ipn.NeedsLogin, qt.Equals, b.State())
|
||||
}
|
||||
|
@ -696,9 +697,9 @@ func TestStateMachine(t *testing.T) {
|
|||
c.Assert(nn[1].Prefs, qt.IsNotNil)
|
||||
c.Assert(nn[2].State, qt.IsNotNil)
|
||||
// Prefs after finishing the login, so LoginName updated.
|
||||
c.Assert(nn[1].Prefs.Persist.LoginName, qt.Equals, "user2")
|
||||
c.Assert(nn[1].Prefs.LoggedOut, qt.IsFalse)
|
||||
c.Assert(nn[1].Prefs.WantRunning, qt.IsTrue)
|
||||
c.Assert(nn[1].Prefs.Persist().LoginName, qt.Equals, "user2")
|
||||
c.Assert(nn[1].Prefs.LoggedOut(), qt.IsFalse)
|
||||
c.Assert(nn[1].Prefs.WantRunning(), qt.IsTrue)
|
||||
c.Assert(ipn.Starting, qt.Equals, *nn[2].State)
|
||||
}
|
||||
|
||||
|
@ -716,7 +717,7 @@ func TestStateMachine(t *testing.T) {
|
|||
c.Assert(nn[0].State, qt.IsNotNil)
|
||||
c.Assert(nn[1].Prefs, qt.IsNotNil)
|
||||
c.Assert(ipn.Stopped, qt.Equals, *nn[0].State)
|
||||
c.Assert(nn[1].Prefs.LoggedOut, qt.IsFalse)
|
||||
c.Assert(nn[1].Prefs.LoggedOut(), qt.IsFalse)
|
||||
}
|
||||
|
||||
// One more restart, this time with a valid key, but WantRunning=false.
|
||||
|
@ -734,8 +735,8 @@ func TestStateMachine(t *testing.T) {
|
|||
cc.assertCalls("Shutdown", "unpause", "New", "Login", "unpause")
|
||||
c.Assert(nn[0].Prefs, qt.IsNotNil)
|
||||
c.Assert(nn[1].State, qt.IsNotNil)
|
||||
c.Assert(nn[0].Prefs.WantRunning, qt.IsFalse)
|
||||
c.Assert(nn[0].Prefs.LoggedOut, qt.IsFalse)
|
||||
c.Assert(nn[0].Prefs.WantRunning(), qt.IsFalse)
|
||||
c.Assert(nn[0].Prefs.LoggedOut(), qt.IsFalse)
|
||||
c.Assert(ipn.Stopped, qt.Equals, *nn[1].State)
|
||||
}
|
||||
|
||||
|
@ -796,7 +797,7 @@ func TestStateMachine(t *testing.T) {
|
|||
t.Logf("\n\nLoginDifferent")
|
||||
notifies.expect(1)
|
||||
b.StartLoginInteractive()
|
||||
url3 := "http://localhost:1/3"
|
||||
url3 := "https://localhost:1/3"
|
||||
cc.send(nil, url3, false, nil)
|
||||
{
|
||||
nn := notifies.drain(1)
|
||||
|
@ -834,9 +835,9 @@ func TestStateMachine(t *testing.T) {
|
|||
c.Assert(nn[1].Prefs, qt.IsNotNil)
|
||||
c.Assert(nn[2].State, qt.IsNotNil)
|
||||
// Prefs after finishing the login, so LoginName updated.
|
||||
c.Assert(nn[1].Prefs.Persist.LoginName, qt.Equals, "user3")
|
||||
c.Assert(nn[1].Prefs.LoggedOut, qt.IsFalse)
|
||||
c.Assert(nn[1].Prefs.WantRunning, qt.IsTrue)
|
||||
c.Assert(nn[1].Prefs.Persist().LoginName, qt.Equals, "user3")
|
||||
c.Assert(nn[1].Prefs.LoggedOut(), qt.IsFalse)
|
||||
c.Assert(nn[1].Prefs.WantRunning(), qt.IsTrue)
|
||||
c.Assert(ipn.Starting, qt.Equals, *nn[2].State)
|
||||
}
|
||||
|
||||
|
@ -853,8 +854,8 @@ func TestStateMachine(t *testing.T) {
|
|||
nn := notifies.drain(1)
|
||||
cc.assertCalls()
|
||||
c.Assert(nn[0].Prefs, qt.IsNotNil)
|
||||
c.Assert(nn[0].Prefs.LoggedOut, qt.IsFalse)
|
||||
c.Assert(nn[0].Prefs.WantRunning, qt.IsTrue)
|
||||
c.Assert(nn[0].Prefs.LoggedOut(), qt.IsFalse)
|
||||
c.Assert(nn[0].Prefs.WantRunning(), qt.IsTrue)
|
||||
c.Assert(ipn.NoState, qt.Equals, b.State())
|
||||
}
|
||||
|
||||
|
@ -918,6 +919,60 @@ func TestStateMachine(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestEditPrefsHasNoKeys(t *testing.T) {
|
||||
logf := t.Logf
|
||||
store := new(testStateStorage)
|
||||
e, err := wgengine.NewFakeUserspaceEngine(logf, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("NewFakeUserspaceEngine: %v", err)
|
||||
}
|
||||
t.Cleanup(e.Close)
|
||||
|
||||
b, err := NewLocalBackend(logf, "logid", store, nil, e, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("NewLocalBackend: %v", err)
|
||||
}
|
||||
b.hostinfo = &tailcfg.Hostinfo{OS: "testos"}
|
||||
b.prefs = (&ipn.Prefs{
|
||||
Persist: &persist.Persist{
|
||||
PrivateNodeKey: key.NewNode(),
|
||||
OldPrivateNodeKey: key.NewNode(),
|
||||
|
||||
LegacyFrontendPrivateMachineKey: key.NewMachine(),
|
||||
},
|
||||
}).View()
|
||||
if b.prefs.Persist().PrivateNodeKey.IsZero() {
|
||||
t.Fatalf("PrivateNodeKey not set")
|
||||
}
|
||||
p, err := b.EditPrefs(&ipn.MaskedPrefs{
|
||||
Prefs: ipn.Prefs{
|
||||
Hostname: "foo",
|
||||
},
|
||||
HostnameSet: true,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("EditPrefs: %v", err)
|
||||
}
|
||||
if p.Hostname() != "foo" {
|
||||
t.Errorf("Hostname = %q; want foo", p.Hostname())
|
||||
}
|
||||
|
||||
// Test that we can't see the PrivateNodeKey.
|
||||
if !p.Persist().PrivateNodeKey.IsZero() {
|
||||
t.Errorf("PrivateNodeKey = %v; want zero", p.Persist().PrivateNodeKey)
|
||||
}
|
||||
|
||||
// Test that we can't see the PrivateNodeKey.
|
||||
if !p.Persist().OldPrivateNodeKey.IsZero() {
|
||||
t.Errorf("OldPrivateNodeKey = %v; want zero", p.Persist().OldPrivateNodeKey)
|
||||
}
|
||||
|
||||
// Test that we can't see the PrivateNodeKey.
|
||||
if !p.Persist().LegacyFrontendPrivateMachineKey.IsZero() {
|
||||
t.Errorf("LegacyFrontendPrivateMachineKey = %v; want zero", p.Persist().LegacyFrontendPrivateMachineKey)
|
||||
}
|
||||
}
|
||||
|
||||
type testStateStorage struct {
|
||||
mem mem.Store
|
||||
written atomic.Bool
|
||||
|
|
|
@ -27,6 +27,7 @@ import (
|
|||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
"unicode"
|
||||
|
||||
"go4.org/mem"
|
||||
"inet.af/peercred"
|
||||
|
@ -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?
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -6,12 +6,8 @@ package ipn
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/tstest"
|
||||
)
|
||||
|
||||
|
@ -61,133 +57,6 @@ func TestReadWrite(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestClientServer(t *testing.T) {
|
||||
tstest.PanicOnLog()
|
||||
tstest.ResourceCheck(t)
|
||||
|
||||
b := &FakeBackend{}
|
||||
var bs *BackendServer
|
||||
var bc *BackendClient
|
||||
serverToClientCh := make(chan []byte, 16)
|
||||
defer close(serverToClientCh)
|
||||
go func() {
|
||||
for b := range serverToClientCh {
|
||||
bc.GotNotifyMsg(b)
|
||||
}
|
||||
}()
|
||||
serverToClient := func(n Notify) {
|
||||
b, err := json.Marshal(n)
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
serverToClientCh <- append([]byte{}, b...)
|
||||
}
|
||||
clientToServer := func(b []byte) {
|
||||
bs.GotCommandMsg(context.TODO(), b)
|
||||
}
|
||||
slogf := func(fmt string, args ...any) {
|
||||
t.Logf("s: "+fmt, args...)
|
||||
}
|
||||
clogf := func(fmt string, args ...any) {
|
||||
t.Logf("c: "+fmt, args...)
|
||||
}
|
||||
bs = NewBackendServer(slogf, b, serverToClient)
|
||||
// Verify that this doesn't break bs's callback:
|
||||
NewBackendServer(slogf, b, nil)
|
||||
bc = NewBackendClient(clogf, clientToServer)
|
||||
|
||||
ch := make(chan Notify, 256)
|
||||
notify := func(n Notify) { ch <- n }
|
||||
h, err := NewHandle(bc, clogf, notify, Options{
|
||||
Prefs: &Prefs{
|
||||
ControlURL: "http://example.com/fake",
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("NewHandle error: %v\n", err)
|
||||
}
|
||||
|
||||
notes := Notify{}
|
||||
nn := []Notify{}
|
||||
processNote := func(n Notify) {
|
||||
nn = append(nn, n)
|
||||
if n.State != nil {
|
||||
t.Logf("state change: %v", *n.State)
|
||||
notes.State = n.State
|
||||
}
|
||||
if n.Prefs != nil {
|
||||
notes.Prefs = n.Prefs
|
||||
}
|
||||
if n.NetMap != nil {
|
||||
notes.NetMap = n.NetMap
|
||||
}
|
||||
if n.Engine != nil {
|
||||
notes.Engine = n.Engine
|
||||
}
|
||||
if n.BrowseToURL != nil {
|
||||
notes.BrowseToURL = n.BrowseToURL
|
||||
}
|
||||
}
|
||||
notesState := func() State {
|
||||
if notes.State != nil {
|
||||
return *notes.State
|
||||
}
|
||||
return NoState
|
||||
}
|
||||
|
||||
flushUntil := func(wantFlush State) {
|
||||
t.Helper()
|
||||
timer := time.NewTimer(1 * time.Second)
|
||||
loop:
|
||||
for {
|
||||
select {
|
||||
case n := <-ch:
|
||||
processNote(n)
|
||||
if notesState() == wantFlush {
|
||||
break loop
|
||||
}
|
||||
case <-timer.C:
|
||||
t.Fatalf("timeout waiting for state %v, got %v", wantFlush, notes.State)
|
||||
}
|
||||
}
|
||||
timer.Stop()
|
||||
loop2:
|
||||
for {
|
||||
select {
|
||||
case n := <-ch:
|
||||
processNote(n)
|
||||
default:
|
||||
break loop2
|
||||
}
|
||||
}
|
||||
if got, want := h.State(), notesState(); got != want {
|
||||
t.Errorf("h.State()=%v, notes.State=%v (on flush until %v)\n", got, want, wantFlush)
|
||||
}
|
||||
}
|
||||
|
||||
flushUntil(NeedsLogin)
|
||||
|
||||
h.StartLoginInteractive()
|
||||
flushUntil(Running)
|
||||
if notes.NetMap == nil && h.NetMap() != nil {
|
||||
t.Errorf("notes.NetMap == nil while h.NetMap != nil\nnotes:\n%v", nn)
|
||||
}
|
||||
|
||||
h.UpdatePrefs(func(p *Prefs) {
|
||||
p.WantRunning = false
|
||||
})
|
||||
flushUntil(Stopped)
|
||||
|
||||
h.Logout()
|
||||
flushUntil(NeedsLogin)
|
||||
|
||||
h.Login(&tailcfg.Oauth2Token{
|
||||
AccessToken: "google_id_token",
|
||||
TokenType: GoogleIDTokenType,
|
||||
})
|
||||
flushUntil(Running)
|
||||
}
|
||||
|
||||
func TestNilBackend(t *testing.T) {
|
||||
var called *Notify
|
||||
bs := NewBackendServer(t.Logf, nil, func(n Notify) {
|
||||
|
|
26
ipn/prefs.go
26
ipn/prefs.go
|
@ -27,7 +27,7 @@ import (
|
|||
"tailscale.com/util/dnsname"
|
||||
)
|
||||
|
||||
//go:generate go run tailscale.com/cmd/cloner -type=Prefs
|
||||
//go:generate go run tailscale.com/cmd/viewer -type=Prefs
|
||||
|
||||
// DefaultControlURL is the URL base of the control plane
|
||||
// ("coordination server") for use when no explicit one is configured.
|
||||
|
@ -288,6 +288,8 @@ func (m *MaskedPrefs) Pretty() string {
|
|||
// IsEmpty reports whether p is nil or pointing to a Prefs zero value.
|
||||
func (p *Prefs) IsEmpty() bool { return p == nil || p.Equals(&Prefs{}) }
|
||||
|
||||
func (p PrefsView) Pretty() string { return p.ж.Pretty() }
|
||||
|
||||
func (p *Prefs) Pretty() string { return p.pretty(runtime.GOOS) }
|
||||
func (p *Prefs) pretty(goos string) string {
|
||||
var sb strings.Builder
|
||||
|
@ -347,6 +349,10 @@ func (p *Prefs) pretty(goos string) string {
|
|||
return sb.String()
|
||||
}
|
||||
|
||||
func (p PrefsView) ToBytes() []byte {
|
||||
return p.ж.ToBytes()
|
||||
}
|
||||
|
||||
func (p *Prefs) ToBytes() []byte {
|
||||
data, err := json.MarshalIndent(p, "", "\t")
|
||||
if err != nil {
|
||||
|
@ -355,6 +361,10 @@ func (p *Prefs) ToBytes() []byte {
|
|||
return data
|
||||
}
|
||||
|
||||
func (p PrefsView) Equals(p2 PrefsView) bool {
|
||||
return p.ж.Equals(p2.ж)
|
||||
}
|
||||
|
||||
func (p *Prefs) Equals(p2 *Prefs) bool {
|
||||
if p == nil && p2 == nil {
|
||||
return true
|
||||
|
@ -431,6 +441,14 @@ func NewPrefs() *Prefs {
|
|||
}
|
||||
}
|
||||
|
||||
// ControlURLOrDefault returns the coordination server's URL base.
|
||||
//
|
||||
// If not configured, or if the configured value is a legacy name equivalent to
|
||||
// the default, then DefaultControlURL is returned instead.
|
||||
func (p PrefsView) ControlURLOrDefault() string {
|
||||
return p.ж.ControlURLOrDefault()
|
||||
}
|
||||
|
||||
// ControlURLOrDefault returns the coordination server's URL base.
|
||||
//
|
||||
// If not configured, or if the configured value is a legacy name equivalent to
|
||||
|
@ -588,6 +606,12 @@ func (p *Prefs) SetExitNodeIP(s string, st *ipnstate.Status) error {
|
|||
return err
|
||||
}
|
||||
|
||||
// ShouldSSHBeRunning reports whether the SSH server should be running based on
|
||||
// the prefs.
|
||||
func (p PrefsView) ShouldSSHBeRunning() bool {
|
||||
return p.Valid() && p.ж.ShouldSSHBeRunning()
|
||||
}
|
||||
|
||||
// ShouldSSHBeRunning reports whether the SSH server should be running based on
|
||||
// the prefs.
|
||||
func (p *Prefs) ShouldSSHBeRunning() bool {
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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{}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -2,11 +2,11 @@
|
|||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// This might work on other BSDs, but only tested on FreeBSD.
|
||||
// Originally a fork of interfaces_darwin.go with slightly different flags.
|
||||
// Common code for FreeBSD and Darwin. This might also work on other
|
||||
// BSD systems (e.g. OpenBSD) but has not been tested.
|
||||
|
||||
//go:build freebsd
|
||||
// +build freebsd
|
||||
//go:build darwin || freebsd
|
||||
// +build darwin freebsd
|
||||
|
||||
package interfaces
|
||||
|
||||
|
@ -37,11 +37,6 @@ func defaultRoute() (d DefaultRouteDetails, err error) {
|
|||
return d, nil
|
||||
}
|
||||
|
||||
// fetchRoutingTable calls route.FetchRIB, fetching NET_RT_DUMP.
|
||||
func fetchRoutingTable() (rib []byte, err error) {
|
||||
return route.FetchRIB(syscall.AF_UNSPEC, unix.NET_RT_DUMP, 0)
|
||||
}
|
||||
|
||||
func DefaultRouteInterfaceIndex() (int, error) {
|
||||
// $ netstat -nr
|
||||
// Routing tables
|
||||
|
@ -61,35 +56,20 @@ func DefaultRouteInterfaceIndex() (int, error) {
|
|||
if err != nil {
|
||||
return 0, fmt.Errorf("route.FetchRIB: %w", err)
|
||||
}
|
||||
msgs, err := route.ParseRIB(unix.NET_RT_IFLIST, rib)
|
||||
msgs, err := parseRoutingTable(rib)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("route.ParseRIB: %w", err)
|
||||
}
|
||||
indexSeen := map[int]int{} // index => count
|
||||
for _, m := range msgs {
|
||||
rm, ok := m.(*route.RouteMessage)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
const RTF_GATEWAY = 0x2
|
||||
const RTF_IFSCOPE = 0x1000000
|
||||
if rm.Flags&RTF_GATEWAY == 0 {
|
||||
continue
|
||||
}
|
||||
if rm.Flags&RTF_IFSCOPE != 0 {
|
||||
continue
|
||||
}
|
||||
indexSeen[rm.Index]++
|
||||
}
|
||||
if len(indexSeen) == 0 {
|
||||
return 0, errors.New("no gateway index found")
|
||||
}
|
||||
if len(indexSeen) == 1 {
|
||||
for idx := range indexSeen {
|
||||
return idx, nil
|
||||
if isDefaultGateway(rm) {
|
||||
return rm.Index, nil
|
||||
}
|
||||
}
|
||||
return 0, fmt.Errorf("ambiguous gateway interfaces found: %v", indexSeen)
|
||||
return 0, errors.New("no gateway index found")
|
||||
}
|
||||
|
||||
func init() {
|
||||
|
@ -102,7 +82,7 @@ func likelyHomeRouterIPBSDFetchRIB() (ret netip.Addr, ok bool) {
|
|||
log.Printf("routerIP/FetchRIB: %v", err)
|
||||
return ret, false
|
||||
}
|
||||
msgs, err := route.ParseRIB(unix.NET_RT_IFLIST, rib)
|
||||
msgs, err := parseRoutingTable(rib)
|
||||
if err != nil {
|
||||
log.Printf("routerIP/ParseRIB: %v", err)
|
||||
return ret, false
|
||||
|
@ -112,26 +92,59 @@ func likelyHomeRouterIPBSDFetchRIB() (ret netip.Addr, ok bool) {
|
|||
if !ok {
|
||||
continue
|
||||
}
|
||||
const RTF_IFSCOPE = 0x1000000
|
||||
if rm.Flags&unix.RTF_GATEWAY == 0 {
|
||||
if !isDefaultGateway(rm) {
|
||||
continue
|
||||
}
|
||||
if rm.Flags&RTF_IFSCOPE != 0 {
|
||||
|
||||
gw, ok := rm.Addrs[unix.RTAX_GATEWAY].(*route.Inet4Addr)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if len(rm.Addrs) > unix.RTAX_GATEWAY {
|
||||
dst4, ok := rm.Addrs[unix.RTAX_DST].(*route.Inet4Addr)
|
||||
if !ok || dst4.IP != ([4]byte{0, 0, 0, 0}) {
|
||||
// Expect 0.0.0.0 as DST field.
|
||||
continue
|
||||
}
|
||||
gw, ok := rm.Addrs[unix.RTAX_GATEWAY].(*route.Inet4Addr)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
return netaddr.IPv4(gw.IP[0], gw.IP[1], gw.IP[2], gw.IP[3]), true
|
||||
}
|
||||
return netaddr.IPv4(gw.IP[0], gw.IP[1], gw.IP[2], gw.IP[3]), true
|
||||
}
|
||||
|
||||
return ret, false
|
||||
}
|
||||
|
||||
var v4default = [4]byte{0, 0, 0, 0}
|
||||
var v6default = [16]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
|
||||
|
||||
func isDefaultGateway(rm *route.RouteMessage) bool {
|
||||
if rm.Flags&unix.RTF_GATEWAY == 0 {
|
||||
return false
|
||||
}
|
||||
// Defined locally because FreeBSD does not have unix.RTF_IFSCOPE.
|
||||
const RTF_IFSCOPE = 0x1000000
|
||||
if rm.Flags&RTF_IFSCOPE != 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
// Addrs is [RTAX_DST, RTAX_GATEWAY, RTAX_NETMASK, ...]
|
||||
if len(rm.Addrs) <= unix.RTAX_NETMASK {
|
||||
return false
|
||||
}
|
||||
|
||||
dst := rm.Addrs[unix.RTAX_DST]
|
||||
netmask := rm.Addrs[unix.RTAX_NETMASK]
|
||||
if dst == nil || netmask == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if dst.Family() == syscall.AF_INET && netmask.Family() == syscall.AF_INET {
|
||||
dstAddr, dstOk := dst.(*route.Inet4Addr)
|
||||
nmAddr, nmOk := netmask.(*route.Inet4Addr)
|
||||
if dstOk && nmOk && dstAddr.IP == v4default && nmAddr.IP == v4default {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
if dst.Family() == syscall.AF_INET6 && netmask.Family() == syscall.AF_INET6 {
|
||||
dstAddr, dstOk := dst.(*route.Inet6Addr)
|
||||
nmAddr, nmOk := netmask.(*route.Inet6Addr)
|
||||
if dstOk && nmOk && dstAddr.IP == v6default && nmAddr.IP == v6default {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
|
|
@ -5,128 +5,16 @@
|
|||
package interfaces
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"net/netip"
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/net/route"
|
||||
"golang.org/x/sys/unix"
|
||||
"tailscale.com/net/netaddr"
|
||||
)
|
||||
|
||||
func defaultRoute() (d DefaultRouteDetails, err error) {
|
||||
idx, err := DefaultRouteInterfaceIndex()
|
||||
if err != nil {
|
||||
return d, err
|
||||
}
|
||||
iface, err := net.InterfaceByIndex(idx)
|
||||
if err != nil {
|
||||
return d, err
|
||||
}
|
||||
d.InterfaceName = iface.Name
|
||||
d.InterfaceIndex = idx
|
||||
return d, nil
|
||||
}
|
||||
|
||||
// fetchRoutingTable calls route.FetchRIB, fetching NET_RT_DUMP2.
|
||||
func fetchRoutingTable() (rib []byte, err error) {
|
||||
return route.FetchRIB(syscall.AF_UNSPEC, syscall.NET_RT_DUMP2, 0)
|
||||
}
|
||||
|
||||
func DefaultRouteInterfaceIndex() (int, error) {
|
||||
// $ netstat -nr
|
||||
// Routing tables
|
||||
// Internet:
|
||||
// Destination Gateway Flags Netif Expire
|
||||
// default 10.0.0.1 UGSc en0 <-- want this one
|
||||
// default 10.0.0.1 UGScI en1
|
||||
|
||||
// From man netstat:
|
||||
// U RTF_UP Route usable
|
||||
// G RTF_GATEWAY Destination requires forwarding by intermediary
|
||||
// S RTF_STATIC Manually added
|
||||
// c RTF_PRCLONING Protocol-specified generate new routes on use
|
||||
// I RTF_IFSCOPE Route is associated with an interface scope
|
||||
|
||||
rib, err := fetchRoutingTable()
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("route.FetchRIB: %w", err)
|
||||
}
|
||||
msgs, err := route.ParseRIB(syscall.NET_RT_IFLIST2, rib)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("route.ParseRIB: %w", err)
|
||||
}
|
||||
indexSeen := map[int]int{} // index => count
|
||||
for _, m := range msgs {
|
||||
rm, ok := m.(*route.RouteMessage)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
const RTF_GATEWAY = 0x2
|
||||
const RTF_IFSCOPE = 0x1000000
|
||||
if rm.Flags&RTF_GATEWAY == 0 {
|
||||
continue
|
||||
}
|
||||
if rm.Flags&RTF_IFSCOPE != 0 {
|
||||
continue
|
||||
}
|
||||
indexSeen[rm.Index]++
|
||||
}
|
||||
if len(indexSeen) == 0 {
|
||||
return 0, errors.New("no gateway index found")
|
||||
}
|
||||
if len(indexSeen) == 1 {
|
||||
for idx := range indexSeen {
|
||||
return idx, nil
|
||||
}
|
||||
}
|
||||
return 0, fmt.Errorf("ambiguous gateway interfaces found: %v", indexSeen)
|
||||
}
|
||||
|
||||
func init() {
|
||||
likelyHomeRouterIP = likelyHomeRouterIPDarwinFetchRIB
|
||||
}
|
||||
|
||||
func likelyHomeRouterIPDarwinFetchRIB() (ret netip.Addr, ok bool) {
|
||||
rib, err := fetchRoutingTable()
|
||||
if err != nil {
|
||||
log.Printf("routerIP/FetchRIB: %v", err)
|
||||
return ret, false
|
||||
}
|
||||
msgs, err := route.ParseRIB(syscall.NET_RT_IFLIST2, rib)
|
||||
if err != nil {
|
||||
log.Printf("routerIP/ParseRIB: %v", err)
|
||||
return ret, false
|
||||
}
|
||||
for _, m := range msgs {
|
||||
rm, ok := m.(*route.RouteMessage)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
const RTF_GATEWAY = 0x2
|
||||
const RTF_IFSCOPE = 0x1000000
|
||||
if rm.Flags&RTF_GATEWAY == 0 {
|
||||
continue
|
||||
}
|
||||
if rm.Flags&RTF_IFSCOPE != 0 {
|
||||
continue
|
||||
}
|
||||
if len(rm.Addrs) > unix.RTAX_GATEWAY {
|
||||
dst4, ok := rm.Addrs[unix.RTAX_DST].(*route.Inet4Addr)
|
||||
if !ok || dst4.IP != ([4]byte{0, 0, 0, 0}) {
|
||||
// Expect 0.0.0.0 as DST field.
|
||||
continue
|
||||
}
|
||||
gw, ok := rm.Addrs[unix.RTAX_GATEWAY].(*route.Inet4Addr)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
return netaddr.IPv4(gw.IP[0], gw.IP[1], gw.IP[2], gw.IP[3]), true
|
||||
}
|
||||
}
|
||||
|
||||
return ret, false
|
||||
func parseRoutingTable(rib []byte) ([]route.Message, error) {
|
||||
return route.ParseRIB(syscall.NET_RT_IFLIST2, rib)
|
||||
}
|
||||
|
|
|
@ -16,18 +16,32 @@ import (
|
|||
)
|
||||
|
||||
func TestLikelyHomeRouterIPSyscallExec(t *testing.T) {
|
||||
syscallIP, syscallOK := likelyHomeRouterIPDarwinFetchRIB()
|
||||
netstatIP, netstatOK := likelyHomeRouterIPDarwinExec()
|
||||
syscallIP, syscallOK := likelyHomeRouterIPBSDFetchRIB()
|
||||
netstatIP, netstatIf, netstatOK := likelyHomeRouterIPDarwinExec()
|
||||
|
||||
if syscallOK != netstatOK || syscallIP != netstatIP {
|
||||
t.Errorf("syscall() = %v, %v, netstat = %v, %v",
|
||||
syscallIP, syscallOK,
|
||||
netstatIP, netstatOK,
|
||||
)
|
||||
}
|
||||
|
||||
if !syscallOK {
|
||||
return
|
||||
}
|
||||
|
||||
def, err := defaultRoute()
|
||||
if err != nil {
|
||||
t.Errorf("defaultRoute() error: %v", err)
|
||||
}
|
||||
|
||||
if def.InterfaceName != netstatIf {
|
||||
t.Errorf("syscall default route interface %s differs from netstat %s", def.InterfaceName, netstatIf)
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
Parse out 10.0.0.1 from:
|
||||
Parse out 10.0.0.1 and en0 from:
|
||||
|
||||
$ netstat -r -n -f inet
|
||||
Routing tables
|
||||
|
@ -40,12 +54,12 @@ default link#14 UCSI utun2
|
|||
10.0.0.1/32 link#4 UCS en0 !
|
||||
...
|
||||
*/
|
||||
func likelyHomeRouterIPDarwinExec() (ret netip.Addr, ok bool) {
|
||||
func likelyHomeRouterIPDarwinExec() (ret netip.Addr, netif string, ok bool) {
|
||||
if version.IsMobile() {
|
||||
// Don't try to do subprocesses on iOS. Ends up with log spam like:
|
||||
// kernel: "Sandbox: IPNExtension(86580) deny(1) process-fork"
|
||||
// This is why we have likelyHomeRouterIPDarwinSyscall.
|
||||
return ret, false
|
||||
return ret, "", false
|
||||
}
|
||||
cmd := exec.Command("/usr/sbin/netstat", "-r", "-n", "-f", "inet")
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
|
@ -64,22 +78,26 @@ func likelyHomeRouterIPDarwinExec() (ret netip.Addr, ok bool) {
|
|||
return nil
|
||||
}
|
||||
f = mem.AppendFields(f[:0], line)
|
||||
if len(f) < 3 || !f[0].EqualString("default") {
|
||||
if len(f) < 4 || !f[0].EqualString("default") {
|
||||
return nil
|
||||
}
|
||||
ipm, flagsm := f[1], f[2]
|
||||
ipm, flagsm, netifm := f[1], f[2], f[3]
|
||||
if !mem.Contains(flagsm, mem.S("G")) {
|
||||
return nil
|
||||
}
|
||||
if mem.Contains(flagsm, mem.S("I")) {
|
||||
return nil
|
||||
}
|
||||
ip, err := netip.ParseAddr(string(mem.Append(nil, ipm)))
|
||||
if err == nil && ip.IsPrivate() {
|
||||
ret = ip
|
||||
netif = netifm.StringCopy()
|
||||
// We've found what we're looking for.
|
||||
return errStopReadingNetstatTable
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return ret, ret.IsValid()
|
||||
return ret, netif, ret.IsValid()
|
||||
}
|
||||
|
||||
func TestFetchRoutingTable(t *testing.T) {
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
// Copyright (c) 2022 Tailscale Inc & AUTHORS All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// This might work on other BSDs, but only tested on FreeBSD.
|
||||
|
||||
//go:build freebsd
|
||||
// +build freebsd
|
||||
|
||||
package interfaces
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/net/route"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
// fetchRoutingTable calls route.FetchRIB, fetching NET_RT_DUMP.
|
||||
func fetchRoutingTable() (rib []byte, err error) {
|
||||
return route.FetchRIB(syscall.AF_UNSPEC, unix.NET_RT_DUMP, 0)
|
||||
}
|
||||
|
||||
func parseRoutingTable(rib []byte) ([]route.Message, error) {
|
||||
return route.ParseRIB(syscall.NET_RT_IFLIST, rib)
|
||||
}
|
|
@ -19,6 +19,7 @@ import (
|
|||
"net/netip"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
|
@ -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
|
||||
|
|
|
@ -122,7 +122,7 @@ func DERPMapOf(stun ...string) *tailcfg.DERPMap {
|
|||
node := &tailcfg.DERPNode{
|
||||
Name: fmt.Sprint(regionID) + "a",
|
||||
RegionID: regionID,
|
||||
HostName: fmt.Sprintf("d%d.invalid", regionID),
|
||||
HostName: fmt.Sprintf("d%d%s", regionID, tailcfg.DotInvalid),
|
||||
IPv4: ipv4,
|
||||
IPv6: ipv6,
|
||||
STUNPort: port,
|
||||
|
|
|
@ -56,10 +56,15 @@ func TailscaleServiceIP() netip.Addr {
|
|||
//
|
||||
// For IPv4, use TailscaleServiceIP.
|
||||
func TailscaleServiceIPv6() netip.Addr {
|
||||
serviceIPv6.Do(func() { mustPrefix(&serviceIPv6.v, "fd7a:115c:a1e0::53/128") })
|
||||
serviceIPv6.Do(func() { mustPrefix(&serviceIPv6.v, TailscaleServiceIPv6String+"/128") })
|
||||
return serviceIPv6.v.Addr()
|
||||
}
|
||||
|
||||
const (
|
||||
TailscaleServiceIPString = "100.100.100.100"
|
||||
TailscaleServiceIPv6String = "fd7a:115c:a1e0::53"
|
||||
)
|
||||
|
||||
// IsTailscaleIP reports whether ip is an IP address in a range that
|
||||
// Tailscale assigns from.
|
||||
func IsTailscaleIP(ip netip.Addr) bool {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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"
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}{})
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
Loading…
Reference in New Issue