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:
Brad Fitzpatrick
2022-11-25 07:54:57 -08:00
committed by Brad Fitzpatrick
parent 624d9c759b
commit 7e016c1d90
7 changed files with 32 additions and 759 deletions

View File

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