tsnet: add a LocalAPI listener on loopback, with basic auth

This is for use by LocalAPI clients written in other languages that
don't appear to be able to talk HTTP over a socket (e.g.
java.net.http.HttpClient).

Signed-off-by: David Crawshaw <crawshaw@tailscale.com>
This commit is contained in:
David Crawshaw
2023-02-27 08:16:11 -08:00
committed by David Crawshaw
parent e3211ff88b
commit 768df4ff7a
4 changed files with 195 additions and 39 deletions

View File

@ -8,6 +8,8 @@ package tsnet
import (
"context"
crand "crypto/rand"
"encoding/hex"
"errors"
"fmt"
"io"
@ -88,19 +90,22 @@ type Server struct {
// If empty, the Tailscale default is used.
ControlURL string
initOnce sync.Once
initErr error
lb *ipnlocal.LocalBackend
netstack *netstack.Impl
linkMon *monitor.Mon
localAPIListener net.Listener
rootPath string // the state directory
hostname string
shutdownCtx context.Context
shutdownCancel context.CancelFunc
localClient *tailscale.LocalClient
logbuffer *filch.Filch
logtail *logtail.Logger
initOnce sync.Once
initErr error
lb *ipnlocal.LocalBackend
netstack *netstack.Impl
linkMon *monitor.Mon
rootPath string // the state directory
hostname string
shutdownCtx context.Context
shutdownCancel context.CancelFunc
localAPICred string // basic auth password for localAPITCPListener
localAPITCPListener net.Listener // optional loopback, restricted to PID
localAPIListener net.Listener // in-memory, used by localClient
localClient *tailscale.LocalClient // in-memory
logbuffer *filch.Filch
logtail *logtail.Logger
logid string
mu sync.Mutex
listeners map[listenKey]*listener
@ -139,6 +144,64 @@ func (s *Server) LocalClient() (*tailscale.LocalClient, error) {
return s.localClient, nil
}
// LoopbackLocalAPI returns a loopback ip:port listening for the "LocalAPI".
//
// As the LocalAPI is powerful, access to endpoints requires BOTH passing a
// "Sec-Tailscale: localapi" HTTP header and passing cred as a basic auth.
//
// It will start the server and the local client listener if they have not
// been started yet.
//
// If you only need to use the LocalAPI from Go, then prefer LocalClient
// as it does not require communication via TCP.
func (s *Server) LoopbackLocalAPI() (addr string, cred string, err error) {
if err := s.Start(); err != nil {
return "", "", err
}
if s.localAPITCPListener == nil {
var cred [16]byte
if _, err := crand.Read(cred[:]); err != nil {
return "", "", err
}
s.localAPICred = hex.EncodeToString(cred[:])
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
return "", "", err
}
s.localAPITCPListener = ln
go func() {
lah := localapi.NewHandler(s.lb, s.logf, s.logid)
lah.PermitWrite = true
lah.PermitRead = true
lah.RequiredPassword = s.localAPICred
h := &localSecHandler{h: lah, cred: s.localAPICred}
if err := http.Serve(s.localAPITCPListener, h); err != nil {
s.logf("localapi tcp serve error: %v", err)
}
}()
}
return s.localAPITCPListener.Addr().String(), s.localAPICred, nil
}
type localSecHandler struct {
h http.Handler
cred string
}
func (h *localSecHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("Sec-Tailscale") != "localapi" {
w.WriteHeader(403)
io.WriteString(w, "missing 'Sec-Tailscale: localapi' header")
return
}
h.h.ServeHTTP(w, r)
}
// Start connects the server to the tailnet.
// Optional: any calls to Dial/Listen will also call Start.
func (s *Server) Start() error {
@ -240,6 +303,9 @@ func (s *Server) Close() error {
if s.localAPIListener != nil {
s.localAPIListener.Close()
}
if s.localAPITCPListener != nil {
s.localAPITCPListener.Close()
}
s.mu.Lock()
defer s.mu.Unlock()
@ -325,7 +391,7 @@ func (s *Server) start() (reterr error) {
if err := lpc.Validate(logtail.CollectionNode); err != nil {
return fmt.Errorf("logpolicy.Config.Validate for %v: %w", cfgPath, err)
}
logid := lpc.PublicID.String()
s.logid = lpc.PublicID.String()
s.logbuffer, err = filch.New(filepath.Join(s.rootPath, "tailscaled"), filch.Options{ReplaceStderr: false})
if err != nil {
@ -399,7 +465,7 @@ func (s *Server) start() (reterr error) {
if s.Ephemeral {
loginFlags = controlclient.LoginEphemeral
}
lb, err := ipnlocal.NewLocalBackend(logf, logid, s.Store, s.dialer, eng, loginFlags)
lb, err := ipnlocal.NewLocalBackend(logf, s.logid, s.Store, s.dialer, eng, loginFlags)
if err != nil {
return fmt.Errorf("NewLocalBackend: %v", err)
}
@ -435,7 +501,7 @@ func (s *Server) start() (reterr error) {
go s.printAuthURLLoop()
// Run the localapi handler, to allow fetching LetsEncrypt certs.
lah := localapi.NewHandler(lb, logf, logid)
lah := localapi.NewHandler(lb, logf, s.logid)
lah.PermitWrite = true
lah.PermitRead = true