From 7e016c1d90790028289cddf3ec51ebfdeb777a0e Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Fri, 25 Nov 2022 07:54:57 -0800 Subject: [PATCH] ipn/ipnserver: remove IPN protocol server Unused in this repo as of the earlier #6450 (300aba61a6) and unused in the Windows GUI as of tailscale/corp#8065. With this ipn.BackendServer is no longer used and could also be removed from this repo. The macOS and iOS clients still temporarily depend on it, but I can move it to that repo instead while and let its migration proceed on its own schedule while we clean this repo up. Updates #6417 Updates tailscale/corp#8051 Change-Id: Ie13f82af3eb9f96b3a21c56cdda51be31ddebdcf Signed-off-by: Brad Fitzpatrick --- cmd/tailscale/cli/up.go | 8 - cmd/tailscale/cli/web.go | 9 - ipn/ipnserver/server.go | 295 ++++---------------------------- ipn/ipnserver/server_test.go | 86 ---------- ipn/message.go | 319 ----------------------------------- ipn/message_test.go | 72 -------- ipn/prefs.go | 2 + 7 files changed, 32 insertions(+), 759 deletions(-) delete mode 100644 ipn/ipnserver/server_test.go delete mode 100644 ipn/message_test.go diff --git a/cmd/tailscale/cli/up.go b/cmd/tailscale/cli/up.go index ed487c622..50ca6be5e 100644 --- a/cmd/tailscale/cli/up.go +++ b/cmd/tailscale/cli/up.go @@ -571,14 +571,6 @@ func runUp(ctx context.Context, cmd string, args []string, upArgs upArgsT) (retE } if n.ErrMessage != nil { msg := *n.ErrMessage - if msg == ipn.ErrMsgPermissionDenied { - switch effectiveGOOS() { - case "windows": - msg += " (Tailscale service in use by other user?)" - default: - msg += " (try 'sudo tailscale up [...]')" - } - } fatalf("backend error: %v\n", msg) } if s := n.State; s != nil { diff --git a/cmd/tailscale/cli/web.go b/cmd/tailscale/cli/web.go index 784ca0508..1bf319dc7 100644 --- a/cmd/tailscale/cli/web.go +++ b/cmd/tailscale/cli/web.go @@ -23,7 +23,6 @@ import ( "net/url" "os" "os/exec" - "runtime" "strings" "github.com/peterbourgon/ff/v3/ffcli" @@ -470,14 +469,6 @@ func tailscaleUp(ctx context.Context, st *ipnstate.Status, forceReauth bool) (au } if n.ErrMessage != nil { msg := *n.ErrMessage - if msg == ipn.ErrMsgPermissionDenied { - switch runtime.GOOS { - case "windows": - msg += " (Tailscale service in use by other user?)" - default: - msg += " (try 'sudo tailscale up [...]')" - } - } return "", fmt.Errorf("backend error: %v", msg) } if url := n.BrowseToURL; url != nil && printAuthURL(*url) { diff --git a/ipn/ipnserver/server.go b/ipn/ipnserver/server.go index b8473cfce..99cf30b9a 100644 --- a/ipn/ipnserver/server.go +++ b/ipn/ipnserver/server.go @@ -6,10 +6,7 @@ package ipnserver import ( "bufio" - "bytes" "context" - "encoding/json" - "errors" "fmt" "io" "log" @@ -38,11 +35,9 @@ import ( "tailscale.com/net/dnsfallback" "tailscale.com/net/netutil" "tailscale.com/net/tsdial" - "tailscale.com/safesocket" "tailscale.com/smallzstd" "tailscale.com/types/logger" "tailscale.com/util/systemd" - "tailscale.com/version" "tailscale.com/version/distro" "tailscale.com/wgengine" "tailscale.com/wgengine/monitor" @@ -97,66 +92,16 @@ type Server struct { // is true, the ForceDaemon pref can override this. resetOnZero bool - bsMu sync.Mutex // lock order: bsMu, then mu - bs *ipn.BackendServer - // mu guards the fields that follow. // lock order: mu, then LocalBackend.mu - mu sync.Mutex - lastUserID string // tracks last userid; on change, Reset state for paranoia - allClients map[net.Conn]*ipnauth.ConnIdentity // HTTP or IPN - clients map[net.Conn]bool // subset of allClients; only IPN protocol - disconnectSub map[chan<- struct{}]struct{} // keys are subscribers of disconnects + mu sync.Mutex + lastUserID string // tracks last userid; on change, Reset state for paranoia + allClients map[net.Conn]*ipnauth.ConnIdentity } // LocalBackend returns the server's LocalBackend. func (s *Server) LocalBackend() *ipnlocal.LocalBackend { return s.b } -// blockWhileInUse blocks while until either a Read from conn fails -// (i.e. it's closed) or until the server is able to accept ci as a -// user. -func (s *Server) blockWhileInUse(conn io.Reader, ci *ipnauth.ConnIdentity) { - s.logf("blocking client while server in use; connIdentity=%v", ci) - connDone := make(chan struct{}) - go func() { - io.Copy(io.Discard, conn) - close(connDone) - }() - ch := make(chan struct{}, 1) - s.registerDisconnectSub(ch, true) - defer s.registerDisconnectSub(ch, false) - for { - select { - case <-connDone: - s.logf("blocked client Read completed; connIdentity=%v", ci) - return - case <-ch: - s.mu.Lock() - err := s.checkConnIdentityLocked(ci) - s.mu.Unlock() - if err == nil { - s.logf("unblocking client, server is free; connIdentity=%v", ci) - // Server is now available again for a new user. - // TODO(bradfitz): keep this connection alive. But for - // now just return and have our caller close the connection - // (which unblocks the io.Copy goroutine we started above) - // and then the client (e.g. Windows) will reconnect and - // discover that it works. - return - } - } - } -} - -// bufferHasHTTPRequest reports whether br looks like it has an HTTP -// request in it, without reading any bytes from it. -func bufferHasHTTPRequest(br *bufio.Reader) bool { - peek, _ := br.Peek(br.Buffered()) - return mem.HasPrefix(mem.B(peek), mem.S("GET ")) || - mem.HasPrefix(mem.B(peek), mem.S("POST ")) || - mem.Contains(mem.B(peek), mem.S(" HTTP/")) -} - // bufferIsConnect reports whether br looks like it's likely an HTTP // CONNECT request. // @@ -166,38 +111,11 @@ func bufferIsConnect(br *bufio.Reader) bool { return mem.HasPrefix(mem.B(peek), mem.S("CONN")) } -// permitOldProtocol is whether we permit the old pre-HTTP protocol from the -// client (cmd/tailscale or GUI client) to the tailscaled server. -// -// This is currently (2022-11-24) only permitted on Windows. There is an -// outstanding change to the Windows GUI to finish the migration to the -// HTTP-based protocol. Once it's in, this constant will go away and the old -// protocol will not be permitted for any platform. -const permitOldProtocol = runtime.GOOS == "windows" - -// ipnProtoAndMethodSniffTimeout returns the read timeout to try to read a few -// bytes from incoming IPN connection to determine whether it's an old-style -// IPN bus connection or a new-style HTTP connection. And if an HTTP connection, -// what its HTTP method is. -func ipnProtoAndMethodSniffTimeout() time.Duration { - if permitOldProtocol { - // In the old protocol, the client might not be sending anything at all - // and only receiving, so keep a short timeout as to not delay - // connecting to the IPN bus and getting ipn.Notify messages. - return 1 * time.Second - } - // But in the new protocol, there will always be an HTTP request to start, - // so we can take a long time to receive the first few bytes. 30s is - // overkill. - return 30 * time.Second -} - func (s *Server) serveConn(ctx context.Context, c net.Conn, logf logger.Logf) { - // First sniff a few bytes to see if it's an HTTP request. And if so, which - // HTTP method. + // First sniff a few bytes to check its HTTP method. br := bufio.NewReader(c) - c.SetReadDeadline(time.Now().Add(ipnProtoAndMethodSniffTimeout())) - br.Peek(4) // either 4 bytes old protocol length header, or HTTP "GET " etc. + c.SetReadDeadline(time.Now().Add(30 * time.Second)) + br.Peek(len("GET / HTTP/1.1\r\n")) // reasonable sniff size to get HTTP method c.SetReadDeadline(time.Time{}) // Handle logtail CONNECT requests early. (See docs on handleProxyConnectConn) @@ -206,78 +124,28 @@ func (s *Server) serveConn(ctx context.Context, c net.Conn, logf logger.Logf) { return } - // If we don't permit the old "IPN bus" JSON bidi stream protocol, then - // assume it's HTTP. Otherwise sniff the first few bytes to see if it looks - // like HTTP. - isHTTPReq := !permitOldProtocol || bufferHasHTTPRequest(br) - - ci, err := s.addConn(c, isHTTPReq) + ci, err := s.addConn(c) if err != nil { - if isHTTPReq { - fmt.Fprintf(c, "HTTP/1.0 500 Nope\r\nContent-Type: text/plain\r\nX-Content-Type-Options: nosniff\r\n\r\n%s\n", err.Error()) - c.Close() - return - } - defer c.Close() - bs := ipn.NewBackendServer(logf, nil, jsonNotifier(c, s.logf)) - _, occupied := err.(inUseOtherUserError) - if occupied { - bs.SendInUseOtherUserErrorMessage(err.Error()) - s.blockWhileInUse(c, ci) - } else { - bs.SendErrorMessage(err.Error()) - time.Sleep(time.Second) - } - return + fmt.Fprintf(c, "HTTP/1.0 500 Nope\r\nContent-Type: text/plain\r\nX-Content-Type-Options: nosniff\r\n\r\n%s\n", err.Error()) + c.Close() } // Tell the LocalBackend about the identity we're now running as. s.b.SetCurrentUserID(ci.UserID()) - if isHTTPReq { - httpServer := &http.Server{ - // Localhost connections are cheap; so only do - // keep-alives for a short period of time, as these - // active connections lock the server into only serving - // that user. If the user has this page open, we don't - // want another switching user to be locked out for - // minutes. 5 seconds is enough to let browser hit - // favicon.ico and such. - IdleTimeout: 5 * time.Second, - ErrorLog: logger.StdLogger(logf), - Handler: s.localhostHandler(ci), - } - httpServer.Serve(netutil.NewOneConnListener(&protoSwitchConn{s: s, br: br, Conn: c}, nil)) - return - } - - defer s.removeAndCloseConn(c) - logf("[v1] incoming control connection") - - if ci.IsReadonlyConn(s.b.OperatorUserID(), logf) { - ctx = ipn.ReadonlyContextOf(ctx) - } - - for ctx.Err() == nil { - msg, err := ipn.ReadMsg(br) - if err != nil { - if errors.Is(err, io.EOF) { - logf("[v1] ReadMsg: %v", err) - } else if ctx.Err() == nil { - logf("ReadMsg: %v", err) - } - return - } - s.bsMu.Lock() - if err := s.bs.GotCommandMsg(ctx, msg); err != nil { - logf("GotCommandMsg: %v", err) - } - gotQuit := s.bs.GotQuit - s.bsMu.Unlock() - if gotQuit { - return - } + httpServer := &http.Server{ + // Localhost connections are cheap; so only do + // keep-alives for a short period of time, as these + // active connections lock the server into only serving + // that user. If the user has this page open, we don't + // want another switching user to be locked out for + // minutes. 5 seconds is enough to let browser hit + // favicon.ico and such. + IdleTimeout: 5 * time.Second, + ErrorLog: logger.StdLogger(logf), + Handler: s.localhostHandler(ci), } + httpServer.Serve(netutil.NewOneConnListener(&protoSwitchConn{s: s, br: br, Conn: c}, nil)) } // inUseOtherUserError is the error type for when the server is in use @@ -375,27 +243,11 @@ func (s *Server) connCanFetchCerts(ci *ipnauth.ConnIdentity) bool { return false } -// registerDisconnectSub adds ch as a subscribe to connection disconnect -// events. If add is false, the subscriber is removed. -func (s *Server) registerDisconnectSub(ch chan<- struct{}, add bool) { - s.mu.Lock() - defer s.mu.Unlock() - if add { - if s.disconnectSub == nil { - s.disconnectSub = make(map[chan<- struct{}]struct{}) - } - s.disconnectSub[ch] = struct{}{} - } else { - delete(s.disconnectSub, ch) - } - -} - // addConn adds c to the server's list of clients. // // If the returned error is of type inUseOtherUserError then the // returned connIdentity is also valid. -func (s *Server) addConn(c net.Conn, isHTTP bool) (ci *ipnauth.ConnIdentity, err error) { +func (s *Server) addConn(c net.Conn) (ci *ipnauth.ConnIdentity, err error) { ci, err = ipnauth.GetConnIdentity(s.logf, c) if err != nil { return @@ -414,9 +266,6 @@ func (s *Server) addConn(c net.Conn, isHTTP bool) (ci *ipnauth.ConnIdentity, err s.mu.Lock() defer s.mu.Unlock() - if s.clients == nil { - s.clients = map[net.Conn]bool{} - } if s.allClients == nil { s.allClients = map[net.Conn]*ipnauth.ConnIdentity{} } @@ -425,9 +274,6 @@ func (s *Server) addConn(c net.Conn, isHTTP bool) (ci *ipnauth.ConnIdentity, err return ci, err } - if !isHTTP { - s.clients[c] = true - } s.allClients[c] = ci if s.lastUserID != ci.UserID() { @@ -441,15 +287,8 @@ func (s *Server) addConn(c net.Conn, isHTTP bool) (ci *ipnauth.ConnIdentity, err func (s *Server) removeAndCloseConn(c net.Conn) { s.mu.Lock() - delete(s.clients, c) delete(s.allClients, c) remain := len(s.allClients) - for sub := range s.disconnectSub { - select { - case sub <- struct{}{}: - default: - } - } s.mu.Unlock() if remain == 0 && s.resetOnZero { @@ -463,36 +302,6 @@ func (s *Server) removeAndCloseConn(c net.Conn) { c.Close() } -func (s *Server) stopAll() { - s.mu.Lock() - defer s.mu.Unlock() - for c := range s.clients { - safesocket.ConnCloseRead(c) - safesocket.ConnCloseWrite(c) - } - s.clients = nil -} - -var jsonEscapedZero = []byte(`\u0000`) - -func (s *Server) writeToClients(n ipn.Notify) { - s.mu.Lock() - defer s.mu.Unlock() - - if len(s.clients) == 0 { - // Common case (at least on busy servers): nobody - // connected (no GUI, etc), so return before - // serializing JSON. - return - } - - if b, ok := marshalNotify(n, s.logf); ok { - for c := range s.clients { - ipn.WriteMsg(c, b) - } - } -} - // Run runs a Tailscale backend service. // The getEngine func is called repeatedly, once per connection, until it returns an engine successfully. // @@ -502,9 +311,6 @@ func Run(ctx context.Context, logf logger.Logf, ln net.Listener, store ipn.State runDone := make(chan struct{}) defer close(runDone) - var serverMu sync.Mutex - var serverOrNil *Server - // When the context is closed or when we return, whichever is first, close our listener // and all open connections. go func() { @@ -512,11 +318,6 @@ func Run(ctx context.Context, logf logger.Logf, ln net.Listener, store ipn.State case <-ctx.Done(): case <-runDone: } - serverMu.Lock() - if s := serverOrNil; s != nil { - s.stopAll() - } - serverMu.Unlock() ln.Close() }() logf("Listening on %v", ln.Addr()) @@ -542,13 +343,10 @@ func Run(ctx context.Context, logf logger.Logf, ln net.Listener, store ipn.State break } logf("ipnserver%d: getEngine failed again: %v", i, err) - errMsg := err.Error() - go func() { - defer c.Close() - bs := ipn.NewBackendServer(logf, nil, jsonNotifier(c, logf)) - bs.SendErrorMessage(errMsg) - time.Sleep(time.Second) - }() + // TODO(bradfitz): queue this error up for the next IPN bus watcher call + // to get for the Windows GUI? We used to send it over the pre-HTTP + // protocol to the Windows GUI. Just close it. + c.Close() } if err := ctx.Err(); err != nil { return err @@ -568,9 +366,6 @@ func Run(ctx context.Context, logf logger.Logf, ln net.Listener, store ipn.State if ns != nil { ns.SetLocalBackend(server.LocalBackend()) } - serverMu.Lock() - serverOrNil = server - serverMu.Unlock() return server.Run(ctx, ln) } @@ -613,7 +408,6 @@ func New(logf logger.Logf, logid string, store ipn.StateStore, eng wgengine.Engi logf: logf, resetOnZero: !opts.SurviveDisconnects, } - server.bs = ipn.NewBackendServer(logf, b, server.writeToClients) return server, nil } @@ -633,17 +427,11 @@ func (s *Server) Run(ctx context.Context, ln net.Listener) error { case <-ctx.Done(): case <-runDone: } - s.stopAll() ln.Close() }() if s.b.Prefs().Valid() { - s.bs.GotCommand(ctx, &ipn.Command{ - Version: version.Long, - Start: &ipn.StartArgs{ - Opts: ipn.Options{}, - }, - }) + s.b.Start(ipn.Options{}) } systemd.Ready() @@ -815,10 +603,9 @@ func getEngineUntilItWorksWrapper(getEngine func() (wgengine.Engine, *netstack.I } } -// protoSwitchConn is a net.Conn that's we want to speak HTTP to but -// it's already had a few bytes read from it to determine that it's -// HTTP. So we Read from its bufio.Reader. On Close, we we tell the -// server it's closed, so the server can account the who's connected. +// protoSwitchConn is a net.Conn with which we want to speak HTTP to but +// it's already had a few bytes read from it to determine its HTTP method. +// So we Read from its bufio.Reader. On Close, we we tell the type protoSwitchConn struct { s *Server net.Conn @@ -870,28 +657,6 @@ func (s *Server) ServeHTMLStatus(w http.ResponseWriter, r *http.Request) { st.WriteHTML(w) } -// jsonNotifier returns a notify-writer func that writes ipn.Notify -// messages to w. -func jsonNotifier(w io.Writer, logf logger.Logf) func(ipn.Notify) { - return func(n ipn.Notify) { - if b, ok := marshalNotify(n, logf); ok { - ipn.WriteMsg(w, b) - } - } -} - -func marshalNotify(n ipn.Notify, logf logger.Logf) (b []byte, ok bool) { - b, err := json.Marshal(n) - if err != nil { - logf("ipnserver: [unexpected] error serializing JSON: %v", err) - return nil, false - } - if bytes.Contains(b, jsonEscapedZero) { - logf("[unexpected] zero byte in BackendServer.send notify message: %q", b) - } - return b, true -} - // listenerWithReadyConn is a net.Listener wrapper that has // one net.Conn ready to be accepted already. type listenerWithReadyConn struct { diff --git a/ipn/ipnserver/server_test.go b/ipn/ipnserver/server_test.go deleted file mode 100644 index a7fb9cd82..000000000 --- a/ipn/ipnserver/server_test.go +++ /dev/null @@ -1,86 +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 ipnserver_test - -import ( - "context" - "fmt" - "path/filepath" - "strings" - "testing" - - "tailscale.com/ipn" - "tailscale.com/ipn/ipnserver" - "tailscale.com/ipn/store/mem" - "tailscale.com/net/tsdial" - "tailscale.com/safesocket" - "tailscale.com/wgengine" - "tailscale.com/wgengine/netstack" -) - -func TestRunMultipleAccepts(t *testing.T) { - t.Skipf("TODO(bradfitz): finish this test, once other fires are out") - - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - - td := t.TempDir() - socketPath := filepath.Join(td, "tailscale.sock") - - logf := func(format string, args ...any) { - format = strings.TrimRight(format, "\n") - println(fmt.Sprintf(format, args...)) - t.Logf(format, args...) - } - - s := safesocket.DefaultConnectionStrategy(socketPath) - connect := func() { - for i := 1; i <= 2; i++ { - logf("connect %d ...", i) - c, err := safesocket.Connect(s) - if err != nil { - t.Fatalf("safesocket.Connect: %v\n", err) - } - clientToServer := func(b []byte) { - ipn.WriteMsg(c, b) - } - bc := ipn.NewBackendClient(logf, clientToServer) - prefs := ipn.NewPrefs() - bc.SetPrefs(prefs) - c.Close() - } - } - - logTriggerTestf := func(format string, args ...any) { - logf(format, args...) - if strings.HasPrefix(format, "Listening on ") { - connect() - } - } - - eng, err := wgengine.NewFakeUserspaceEngine(logf, 0) - if err != nil { - t.Fatal(err) - } - t.Cleanup(eng.Close) - - opts := ipnserver.Options{} - t.Logf("pre-Run") - store := new(mem.Store) - - ln, _, err := safesocket.Listen(socketPath, 0) - if err != nil { - t.Fatal(err) - } - defer ln.Close() - - err = ipnserver.Run(ctx, logTriggerTestf, ln, store, nil /* mon */, new(tsdial.Dialer), "dummy_logid", FixedEngine(eng), opts) - t.Logf("ipnserver.Run = %v", err) -} - -// FixedEngine returns a func that returns eng and a nil error. -func FixedEngine(eng wgengine.Engine) func() (wgengine.Engine, *netstack.Impl, error) { - return func() (wgengine.Engine, *netstack.Impl, error) { return eng, nil, nil } -} diff --git a/ipn/message.go b/ipn/message.go index 8404c2c08..4ae624e89 100644 --- a/ipn/message.go +++ b/ipn/message.go @@ -5,19 +5,9 @@ package ipn import ( - "bytes" "context" - "encoding/binary" - "encoding/json" - "errors" - "fmt" - "io" - "log" "tailscale.com/envknob" - "tailscale.com/tailcfg" - "tailscale.com/types/logger" - "tailscale.com/types/structs" "tailscale.com/version" ) @@ -39,178 +29,6 @@ func ReadonlyContextOf(ctx context.Context) context.Context { return context.WithValue(ctx, readOnlyContextKey{}, readOnlyContextKey{}) } -var jsonEscapedZero = []byte(`\u0000`) - -type NoArgs struct{} - -type StartArgs struct { - Opts Options -} - -type SetPrefsArgs struct { - New *Prefs -} - -// Command is a command message that is JSON encoded and sent by a -// frontend to a backend. -type Command struct { - _ structs.Incomparable - - // Version is the binary version of the frontend (the client). - Version string - - // AllowVersionSkew controls whether it's permitted for the - // client and server to have a different version. The default - // (false) means to be strict. - AllowVersionSkew bool - - // Exactly one of the following must be non-nil. - Quit *NoArgs - Start *StartArgs - StartLoginInteractive *NoArgs - Login *tailcfg.Oauth2Token - Logout *NoArgs - SetPrefs *SetPrefsArgs - RequestEngineStatus *NoArgs - RequestStatus *NoArgs -} - -type BackendServer struct { - logf logger.Logf - b Backend // the Backend we are serving up - sendNotifyMsg func(Notify) // send a notification message - GotQuit bool // a Quit command was received -} - -// NewBackendServer creates a new BackendServer using b. -// -// If sendNotifyMsg is non-nil, it additionally sets the Backend's -// notification callback to call the func with ipn.Notify messages in -// JSON form. If nil, it does not change the notification callback. -func NewBackendServer(logf logger.Logf, b Backend, sendNotifyMsg func(Notify)) *BackendServer { - bs := &BackendServer{ - logf: logf, - b: b, - sendNotifyMsg: sendNotifyMsg, - } - // b may be nil if the BackendServer is being created just to - // encapsulate and send an error message. - if sendNotifyMsg != nil && b != nil { - b.SetNotifyCallback(bs.send) - } - return bs -} - -func (bs *BackendServer) send(n Notify) { - if bs.sendNotifyMsg == nil { - return - } - n.Version = ipcVersion - bs.sendNotifyMsg(n) -} - -func (bs *BackendServer) SendErrorMessage(msg string) { - bs.send(Notify{ErrMessage: &msg}) -} - -// SendInUseOtherUserErrorMessage sends a Notify message to the client that -// both sets the state to 'InUseOtherUser' and sets the associated reason -// to msg. -func (bs *BackendServer) SendInUseOtherUserErrorMessage(msg string) { - inUse := InUseOtherUser - bs.send(Notify{ - State: &inUse, - ErrMessage: &msg, - }) -} - -// GotCommandMsg parses the incoming message b as a JSON Command and -// calls GotCommand with it. -func (bs *BackendServer) GotCommandMsg(ctx context.Context, b []byte) error { - cmd := &Command{} - if len(b) == 0 { - return nil - } - if err := json.Unmarshal(b, cmd); err != nil { - return err - } - return bs.GotCommand(ctx, cmd) -} - -// ErrMsgPermissionDenied is the Notify.ErrMessage value used an -// operation was done from a user/context that didn't have permission. -const ErrMsgPermissionDenied = "permission denied" - -func (bs *BackendServer) GotCommand(ctx context.Context, cmd *Command) error { - if cmd.Version != ipcVersion && !cmd.AllowVersionSkew { - vs := fmt.Sprintf("GotCommand: Version mismatch! frontend=%#v backend=%#v", - cmd.Version, ipcVersion) - bs.logf("%s", vs) - // ignore the command, but send a message back to the - // caller so it can realize the version mismatch too. - // We don't want to exit because it might cause a crash - // loop, and restarting won't fix the problem. - bs.send(Notify{ - ErrMessage: &vs, - }) - return nil - } - - // TODO(bradfitz): finish plumbing context down to all the methods below; - // currently we just check for read-only contexts in this method and - // then never use contexts again. - - // Actions permitted with a read-only context: - if c := cmd.RequestEngineStatus; c != nil { - bs.b.RequestEngineStatus() - return nil - } - - if IsReadonlyContext(ctx) { - msg := ErrMsgPermissionDenied - bs.send(Notify{ErrMessage: &msg}) - return nil - } - - if cmd.Quit != nil { - bs.GotQuit = true - return errors.New("Quit command received") - } else if c := cmd.Start; c != nil { - opts := c.Opts - return bs.b.Start(opts) - } else if c := cmd.StartLoginInteractive; c != nil { - bs.b.StartLoginInteractive() - return nil - } else if c := cmd.Login; c != nil { - bs.b.Login(c) - return nil - } else if c := cmd.Logout; c != nil { - bs.b.Logout() - return nil - } else if c := cmd.SetPrefs; c != nil { - bs.b.SetPrefs(c.New) - return nil - } - return fmt.Errorf("BackendServer.Do: no command specified") -} - -type BackendClient struct { - logf logger.Logf - sendCommandMsg func(jsonb []byte) - notify func(Notify) - - // AllowVersionSkew controls whether to allow mismatched - // frontend & backend versions. - AllowVersionSkew bool -} - -func NewBackendClient(logf logger.Logf, sendCommandMsg func(jsonb []byte)) *BackendClient { - return &BackendClient{ - logf: logf, - sendCommandMsg: sendCommandMsg, - } -} - // IPCVersion returns version.Long usually, unless TS_DEBUG_FAKE_IPC_VERSION is // set, in which it contains that value. This is only used for weird development // cases when testing mismatched versions and you want the client to act like it's @@ -221,140 +39,3 @@ func IPCVersion() string { } return version.Long } - -var ipcVersion = IPCVersion() - -func (bc *BackendClient) GotNotifyMsg(b []byte) { - if len(b) == 0 { - // not interesting - return - } - if bytes.Contains(b, jsonEscapedZero) { - log.Printf("[unexpected] zero byte in BackendClient.GotNotifyMsg message: %q", b) - } - n := Notify{} - if err := json.Unmarshal(b, &n); err != nil { - log.Fatalf("BackendClient.Notify: cannot decode message (length=%d, %#q): %v", len(b), b, err) - } - if n.Version != ipcVersion && !bc.AllowVersionSkew { - vs := fmt.Sprintf("GotNotify: Version mismatch! frontend=%#v backend=%#v", - ipcVersion, n.Version) - bc.logf("%s", vs) - // delete anything in the notification except the version, - // to prevent incorrect operation. - n = Notify{ - Version: n.Version, - ErrMessage: &vs, - } - } - if bc.notify != nil { - bc.notify(n) - } -} - -func (bc *BackendClient) send(cmd Command) { - cmd.Version = ipcVersion - b, err := json.Marshal(cmd) - if err != nil { - log.Fatalf("Failed json.Marshal(cmd): %v\n", err) - } - if bytes.Contains(b, jsonEscapedZero) { - log.Printf("[unexpected] zero byte in BackendClient.send command") - } - bc.sendCommandMsg(b) -} - -func (bc *BackendClient) SetNotifyCallback(fn func(Notify)) { - bc.notify = fn -} - -func (bc *BackendClient) Quit() error { - bc.send(Command{Quit: &NoArgs{}}) - return nil -} - -func (bc *BackendClient) Start(opts Options) error { - bc.send(Command{Start: &StartArgs{Opts: opts}}) - return nil // remote Start() errors must be handled remotely -} - -func (bc *BackendClient) StartLoginInteractive() { - bc.send(Command{StartLoginInteractive: &NoArgs{}}) -} - -func (bc *BackendClient) Login(token *tailcfg.Oauth2Token) { - bc.send(Command{Login: token}) -} - -func (bc *BackendClient) Logout() { - bc.send(Command{Logout: &NoArgs{}}) -} - -func (bc *BackendClient) SetPrefs(new *Prefs) { - bc.send(Command{SetPrefs: &SetPrefsArgs{New: new}}) -} - -func (bc *BackendClient) RequestEngineStatus() { - bc.send(Command{RequestEngineStatus: &NoArgs{}}) -} - -func (bc *BackendClient) RequestStatus() { - bc.send(Command{AllowVersionSkew: true, RequestStatus: &NoArgs{}}) -} - -// MaxMessageSize is the maximum message size, in bytes. -const MaxMessageSize = 10 << 20 - -// TODO(apenwarr): incremental json decode? That would let us avoid -// storing the whole byte array uselessly in RAM. -func ReadMsg(r io.Reader) ([]byte, error) { - cb := make([]byte, 4) - _, err := io.ReadFull(r, cb) - if err != nil { - return nil, err - } - n := binary.LittleEndian.Uint32(cb) - if n > MaxMessageSize { - return nil, fmt.Errorf("ipn.Read: message too large: %v bytes", n) - } - b := make([]byte, n) - nn, err := io.ReadFull(r, b) - if err != nil { - return nil, err - } - if nn != int(n) { - return nil, fmt.Errorf("ipn.Read: expected %v bytes, got %v", n, nn) - } - return b, nil -} - -func WriteMsg(w io.Writer, b []byte) error { - // TODO(apenwarr): incremental json encode? That would save RAM, at the - // expense of having to encode once so that we can produce the initial byte - // count. - - // TODO(bradfitz): this does two writes to w, which likely - // does two writes on the wire, two frame generations, etc. We - // should take a concrete buffered type, or use a sync.Pool to - // allocate a buf and do one write. - cb := make([]byte, 4) - if len(b) > MaxMessageSize { - return fmt.Errorf("ipn.Write: message too large: %v bytes", len(b)) - } - binary.LittleEndian.PutUint32(cb, uint32(len(b))) - n, err := w.Write(cb) - if err != nil { - return err - } - if n != 4 { - return fmt.Errorf("ipn.Write: short write: %v bytes (wanted 4)", n) - } - n, err = w.Write(b) - if err != nil { - return err - } - if n != len(b) { - return fmt.Errorf("ipn.Write: short write: %v bytes (wanted %v)", n, len(b)) - } - return nil -} diff --git a/ipn/message_test.go b/ipn/message_test.go deleted file mode 100644 index cc95e938a..000000000 --- a/ipn/message_test.go +++ /dev/null @@ -1,72 +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 ( - "bytes" - "testing" - - "tailscale.com/tstest" -) - -func TestReadWrite(t *testing.T) { - tstest.PanicOnLog() - tstest.ResourceCheck(t) - - buf := bytes.Buffer{} - err := WriteMsg(&buf, []byte("Test string1")) - if err != nil { - t.Fatalf("write1: %v\n", err) - } - err = WriteMsg(&buf, []byte("")) - if err != nil { - t.Fatalf("write2: %v\n", err) - } - err = WriteMsg(&buf, []byte("Test3")) - if err != nil { - t.Fatalf("write3: %v\n", err) - } - - b, err := ReadMsg(&buf) - if err != nil { - t.Fatalf("read1 error: %v", err) - } - if want, got := "Test string1", string(b); want != got { - t.Fatalf("read1: %#v != %#v\n", want, got) - } - b, err = ReadMsg(&buf) - if err != nil { - t.Fatalf("read2 error: %v", err) - } - if want, got := "", string(b); want != got { - t.Fatalf("read2: %#v != %#v\n", want, got) - } - b, err = ReadMsg(&buf) - if err != nil { - t.Fatalf("read3 error: %v", err) - } - if want, got := "Test3", string(b); want != got { - t.Fatalf("read3: %#v != %#v\n", want, got) - } - - b, err = ReadMsg(&buf) - if err == nil { - t.Fatalf("read4: expected error, got %#v\n", b) - } -} - -func TestNilBackend(t *testing.T) { - var called *Notify - bs := NewBackendServer(t.Logf, nil, func(n Notify) { - called = &n - }) - bs.SendErrorMessage("Danger, Will Robinson!") - if called == nil { - t.Errorf("expect callback to be called, wasn't") - } - if called.ErrMessage == nil || *called.ErrMessage != "Danger, Will Robinson!" { - t.Errorf("callback got wrong error: %v", called.ErrMessage) - } -} diff --git a/ipn/prefs.go b/ipn/prefs.go index 988c72152..eb1d48497 100644 --- a/ipn/prefs.go +++ b/ipn/prefs.go @@ -665,6 +665,8 @@ func PrefsFromBytes(b []byte) (*Prefs, error) { return p, err } +var jsonEscapedZero = []byte(`\u0000`) + // LoadPrefs loads a legacy relaynode config file into Prefs // with sensible migration defaults set. func LoadPrefs(filename string) (*Prefs, error) {