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:

committed by
David Crawshaw

parent
e3211ff88b
commit
768df4ff7a
@ -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
|
||||
|
||||
|
Reference in New Issue
Block a user