mirror of
https://github.com/tailscale/tailscale.git
synced 2025-01-05 14:57:49 +00:00
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 <bradfitz@tailscale.com>
This commit is contained in:
parent
624d9c759b
commit
7e016c1d90
@ -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 {
|
||||
|
@ -23,7 +23,6 @@
|
||||
"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) {
|
||||
|
@ -6,10 +6,7 @@
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
@ -38,11 +35,9 @@
|
||||
"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 {
|
||||
|
@ -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 }
|
||||
}
|
319
ipn/message.go
319
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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
@ -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) {
|
||||
|
Loading…
x
Reference in New Issue
Block a user