Something went wrong. Try again.
forked from tailscale/tailscale
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579// Copyright (c) Tailscale Inc & contributors// SPDX-License-Identifier: BSD-3-Clause
// Package ipnserver runs the LocalAPI HTTP server that communicates// with the LocalBackend.package ipnserver
import ( "context" "encoding/base64" "encoding/json" "errors" "fmt" "io" "net" "net/http" "os/user" "runtime" "strconv" "strings" "sync" "sync/atomic" "unicode"
"tailscale.com/client/tailscale/apitype" "tailscale.com/envknob" "tailscale.com/feature" "tailscale.com/feature/buildfeatures" "tailscale.com/ipn/ipnauth" "tailscale.com/ipn/ipnlocal" "tailscale.com/ipn/localapi" "tailscale.com/net/netmon" "tailscale.com/types/logger" "tailscale.com/types/logid" "tailscale.com/util/eventbus" "tailscale.com/util/mak" "tailscale.com/util/set" "tailscale.com/util/testenv")
// Server is an IPN backend and its set of 0 or more active localhost// TCP or unix socket connections talking to that backend.type Server struct { lb atomic.Pointer[ipnlocal.LocalBackend] logf logger.Logf bus *eventbus.Bus netMon *netmon.Monitor // must be non-nil backendLogID logid.PublicID
// mu guards the fields that follow. // lock order: mu, then LocalBackend.mu mu sync.Mutex activeReqs map[*http.Request]ipnauth.Actor backendWaiter waiterSet // of LocalBackend waiters zeroReqWaiter waiterSet // of blockUntilZeroConnections waiters}
func (s *Server) mustBackend() *ipnlocal.LocalBackend { lb := s.lb.Load() if lb == nil { panic("unexpected: call to mustBackend in path where SetLocalBackend should've been called") } return lb}
// waiterSet is a set of callers waiting on something. Each item (map value) in// the set is a func that wakes up that waiter's context. The waiter is responsible// for removing itself from the set when woken up. The (*waiterSet).add method// returns a cleanup method which does that removal. The caller than defers that// cleanup.//// TODO(bradfitz): this is a generally useful pattern. Move elsewhere?type waiterSet set.HandleSet[context.CancelFunc]
// add registers a new waiter in the set.// It acquires mu to add the waiter, and does so again when cleanup is called to remove it.// ready is closed when the waiter is ready (or ctx is done).func (s *waiterSet) add(mu *sync.Mutex, ctx context.Context) (ready <-chan struct{}, cleanup func()) { ctx, cancel := context.WithCancel(ctx) hs := (*set.HandleSet[context.CancelFunc])(s) // change method set mu.Lock() h := hs.Add(cancel) mu.Unlock() return ctx.Done(), func() { mu.Lock() delete(*hs, h) mu.Unlock() cancel() }}
// wakeAll wakes up all waiters in the set.func (w waiterSet) wakeAll() { for _, cancel := range w { cancel() // they'll remove themselves }}
func (s *Server) awaitBackend(ctx context.Context) (_ *ipnlocal.LocalBackend, ok bool) { lb := s.lb.Load() if lb != nil { return lb, true }
ready, cleanup := s.backendWaiter.add(&s.mu, ctx) defer cleanup()
// Try again, now that we've registered, in case there was a // race. lb = s.lb.Load() if lb != nil { return lb, true }
<-ready lb = s.lb.Load() return lb, lb != nil}
// serveServerStatus serves the /server-status endpoint which reports whether// the LocalBackend is up yet.// This is primarily for the Windows GUI, because wintun can take awhile to// come up. See https://github.com/tailscale/tailscale/issues/6522.func (s *Server) serveServerStatus(w http.ResponseWriter, r *http.Request) { if !buildfeatures.HasDebug && runtime.GOOS != "windows" { http.Error(w, feature.ErrUnavailable.Error(), http.StatusNotFound) return } ctx := r.Context()
w.Header().Set("Content-Type", "application/json") var res struct { Error string `json:"error,omitempty"` }
lb := s.lb.Load() if lb == nil { w.WriteHeader(http.StatusServiceUnavailable) if wait, _ := strconv.ParseBool(r.FormValue("wait")); wait { w.(http.Flusher).Flush() lb, _ = s.awaitBackend(ctx) } }
if lb == nil { res.Error = "backend not ready" } json.NewEncoder(w).Encode(res)}
func (s *Server) serveHTTP(w http.ResponseWriter, r *http.Request) { ctx := r.Context() if r.Method == "CONNECT" { if envknob.GOOS() == "windows" { // For the GUI client when using an exit node. See docs on handleProxyConnectConn. s.handleProxyConnectConn(w, r) } else { http.Error(w, "bad method for platform", http.StatusMethodNotAllowed) } return }
// Check for this method before the awaitBackend call, as it reports whether // the backend is available. if r.Method == "GET" && r.URL.Path == "/server-status" { s.serveServerStatus(w, r) return }
lb, ok := s.awaitBackend(ctx) if !ok { // Almost certainly because the context was canceled so the response // here doesn't really matter. The client is gone. http.Error(w, "no backend", http.StatusServiceUnavailable) return }
ci, err := actorFromContext(r.Context()) if err != nil { if errors.Is(err, errNoActor) { http.Error(w, "internal error: "+err.Error(), http.StatusInternalServerError) } else { http.Error(w, err.Error(), http.StatusUnauthorized) } return }
onDone, err := s.addActiveHTTPRequest(r, ci) if err != nil { if ou, ok := err.(inUseOtherUserError); ok && localapi.InUseOtherUserIPNStream(w, r, ou.Unwrap()) { w.(http.Flusher).Flush() s.blockWhileIdentityInUse(ctx, ci) return } http.Error(w, err.Error(), http.StatusUnauthorized) return } defer onDone()
if strings.HasPrefix(r.URL.Path, "/localapi/") { if actor, ok := ci.(*actor); ok { reason, err := base64.StdEncoding.DecodeString(r.Header.Get(apitype.RequestReasonHeader)) if err != nil { http.Error(w, "invalid reason header", http.StatusBadRequest) return } ci = actorWithAccessOverride(actor, string(reason)) }
lah := localapi.NewHandler(localapi.HandlerConfig{ Actor: ci, Backend: lb, Logf: s.logf, LogID: s.backendLogID, EventBus: lb.Sys().Bus.Get(), }) if actor, ok := ci.(*actor); ok { lah.PermitRead, lah.PermitWrite = actor.Permissions(lb.OperatorUserID()) lah.PermitCert = actor.CanFetchCerts() } else if testenv.InTest() { lah.PermitRead, lah.PermitWrite = true, true } lah.ServeHTTP(w, r) return }
if r.URL.Path != "/" { http.NotFound(w, r) return }
if envknob.GOOS() == "windows" { // TODO(bradfitz): remove this once we moved to named pipes for LocalAPI // on Windows. This could then move to all platforms instead at // 100.100.100.100 or something (quad100 handler in LocalAPI) s.ServeHTMLStatus(w, r) return }
io.WriteString(w, "<html><title>Tailscale</title><body><h1>Tailscale</h1>This is the local Tailscale daemon.\n")}
// inUseOtherUserError is the error type for when the server is in use// by a different local user.type inUseOtherUserError struct{ error }
func (e inUseOtherUserError) Unwrap() error { return e.error }
// checkConnIdentityLocked checks whether the provided identity is// allowed to connect to the server.//// The returned error, when non-nil, will be of type inUseOtherUserError.//// s.mu must be held.func (s *Server) checkConnIdentityLocked(ci ipnauth.Actor) error { // If clients are already connected, verify they're the same user. // This mostly matters on Windows at the moment. if len(s.activeReqs) > 0 { var active ipnauth.Actor for _, active = range s.activeReqs { break } if active != nil { // Always allow Windows SYSTEM user to connect, // even if Tailscale is currently being used by another user. if ci.IsLocalSystem() { return nil }
if ci.UserID() != active.UserID() { var b strings.Builder b.WriteString("Tailscale already in use") if username, err := active.Username(); err == nil { fmt.Fprintf(&b, " by %s", username) } if active, ok := active.(*actor); ok { fmt.Fprintf(&b, ", pid %d", active.pid()) } return inUseOtherUserError{errors.New(b.String())} } } } if err := s.mustBackend().CheckIPNConnectionAllowed(ci); err != nil { return inUseOtherUserError{err} } return nil}
// blockWhileIdentityInUse blocks while ci can't connect to the server because// the server is in use by a different user.//// This is primarily used for the Windows GUI, to block until one user's done// controlling the tailscaled process.func (s *Server) blockWhileIdentityInUse(ctx context.Context, actor ipnauth.Actor) error { inUse := func() bool { s.mu.Lock() defer s.mu.Unlock() _, ok := s.checkConnIdentityLocked(actor).(inUseOtherUserError) return ok } for inUse() { // Check whenever the connection count drops down to zero. ready, cleanup := s.zeroReqWaiter.add(&s.mu, ctx) if inUse() { // If the server was in use at the time of the initial check, // but disconnected and was removed from the activeReqs map // by the time we registered a waiter, the ready channel // will never be closed, resulting in a deadlock. To avoid // this, we can check again after registering the waiter. // // This method is planned for complete removal as part of the // multi-user improvements in tailscale/corp#18342, // and this approach should be fine as a temporary solution. <-ready } cleanup() if err := ctx.Err(); err != nil { return err } } return nil}
// Permissions returns the actor's permissions for accessing// the Tailscale local daemon API. The operatorUID is only used on// Unix-like platforms and specifies the ID of a local user// (in the os/user.User.Uid string form) who is allowed// to operate tailscaled without being root or using sudo.//// Sandboxed macos clients must directly supply, or be able to read,// an explicit token. Permission is inferred by validating that// token. Sandboxed macos clients also don't use ipnserver.actor at all// (and prior to that, they didn't use ipnauth.ConnIdentity)//// See safesocket and safesocket_darwin.func (a *actor) Permissions(operatorUID string) (read, write bool) { switch envknob.GOOS() { case "windows": // As of 2024-08-27, according to the current permission model, // Windows users always have read/write access to the local API if // they're allowed to connect. Whether a user is allowed to connect // is determined by [Server.checkConnIdentityLocked] when adding a // new connection in [Server.addActiveHTTPRequest]. Therefore, it's // acceptable to permit read and write access without any additional // checks here. Note that this permission model is being changed in // tailscale/corp#18342. return true, true case "js", "plan9": return true, true } if a.ci.IsUnixSock() { return true, !a.ci.IsReadonlyConn(operatorUID, logger.Discard) } return false, false}
// userIDFromString maps from either a numeric user id in string form// ("998") or username ("caddy") to its string userid ("998").// It returns the empty string on error.func userIDFromString(v string) string { if v == "" || isAllDigit(v) { return v } u, err := user.Lookup(v) if err != nil { return "" } return u.Uid}
func isAllDigit(s string) bool { for i := range len(s) { if b := s[i]; b < '0' || b > '9' { return false } } return true}
// CanFetchCerts reports whether the actor is allowed to fetch HTTPS// certs from this server when it wouldn't otherwise be able to.//// That is, this reports whether the actor should grant additional// capabilities over what the actor would otherwise be able to do.//// For now this only returns true on Unix machines when// TS_PERMIT_CERT_UID is set the to the userid of the peer// connection. It's intended to give your non-root webserver access// (www-data, caddy, nginx, etc) to certs.func (a *actor) CanFetchCerts() bool { if !buildfeatures.HasACME { return false } if a.ci.IsUnixSock() && a.ci.Creds() != nil { connUID, ok := a.ci.Creds().UserID() if ok && connUID == userIDFromString(envknob.String("TS_PERMIT_CERT_UID")) { return true } } return false}
// addActiveHTTPRequest adds c to the server's list of active HTTP requests.//// It returns an error if the specified actor is not allowed to connect.// The returned error may be of type [inUseOtherUserError].//// onDone must be called when the HTTP request is done.func (s *Server) addActiveHTTPRequest(req *http.Request, actor ipnauth.Actor) (onDone func(), err error) { if runtime.GOOS != "windows" && !buildfeatures.HasUnixSocketIdentity { return func() {}, nil }
if actor == nil { return nil, errors.New("internal error: nil actor") }
lb := s.mustBackend()
s.mu.Lock() defer s.mu.Unlock()
if err := s.checkConnIdentityLocked(actor); err != nil { return nil, err }
mak.Set(&s.activeReqs, req, actor)
if len(s.activeReqs) == 1 { if envknob.GOOS() == "windows" && !actor.IsLocalSystem() { // Tell the LocalBackend about the identity we're now running as, // unless it's the SYSTEM user. That user is not a real account and // doesn't have a home directory. lb.SetCurrentUser(actor) } }
onDone = func() { s.mu.Lock() defer s.mu.Unlock() delete(s.activeReqs, req) if len(s.activeReqs) != 0 { // The server is not idle yet. return }
if envknob.GOOS() == "windows" && !actor.IsLocalSystem() { lb.SetCurrentUser(nil) }
// Wake up callers waiting for the server to be idle: s.zeroReqWaiter.wakeAll() }
return onDone, nil}
// New returns a new Server.//// To start it, use the Server.Run method.//// At some point, either before or after Run, the Server's SetLocalBackend// method must also be called before Server can do anything useful.func New(logf logger.Logf, logID logid.PublicID, bus *eventbus.Bus, netMon *netmon.Monitor) *Server { if netMon == nil { panic("nil netMon") } return &Server{ backendLogID: logID, logf: logf, bus: bus, netMon: netMon, }}
// SetLocalBackend sets the server's LocalBackend.//// It should only call be called after calling lb.Start.func (s *Server) SetLocalBackend(lb *ipnlocal.LocalBackend) { if lb == nil { panic("nil LocalBackend") }
if !s.lb.CompareAndSwap(nil, lb) { panic("already set") }
s.mu.Lock() s.backendWaiter.wakeAll() s.mu.Unlock()
// TODO(bradfitz): send status update to GUI long poller waiter. See // https://github.com/tailscale/tailscale/issues/6522}
// Run runs the server, accepting connections from ln forever.//// If the context is done, the listener is closed. It is also the base context// of all HTTP requests.//// If the Server's LocalBackend has already been set, Run starts it.// Otherwise, the next call to SetLocalBackend will start it.func (s *Server) Run(ctx context.Context, ln net.Listener) error { defer func() { if lb := s.lb.Load(); lb != nil { lb.Shutdown() } }()
runDone := make(chan struct{}) defer close(runDone)
ec := s.bus.Client("ipnserver.Server") defer ec.Close() shutdownSub := eventbus.Subscribe[localapi.Shutdown](ec)
// When the context is closed, a [localapi.Shutdown] event is received, // or when we return, whichever is first, close our listener // and all open connections. go func() { select { case <-shutdownSub.Events(): case <-ctx.Done(): case <-runDone: } ln.Close() }()
if ready, ok := feature.HookSystemdReady.GetOk(); ok { ready() }
hs := &http.Server{ Handler: http.HandlerFunc(s.serveHTTP), BaseContext: func(_ net.Listener) context.Context { return ctx }, ConnContext: func(ctx context.Context, c net.Conn) context.Context { return contextWithActor(ctx, s.logf, c) }, ErrorLog: logger.StdLogger(logger.WithPrefix(s.logf, "ipnserver: ")), } if err := hs.Serve(ln); err != nil { if err := ctx.Err(); err != nil { return err } return err } return nil}
// ServeHTMLStatus serves an HTML status page at http://localhost:41112/ for// Windows and via $DEBUG_LISTENER/debug/ipn when tailscaled's --debug flag// is used to run a debug server.func (s *Server) ServeHTMLStatus(w http.ResponseWriter, r *http.Request) { if !buildfeatures.HasDebug { http.Error(w, feature.ErrUnavailable.Error(), http.StatusNotFound) return } lb := s.lb.Load() if lb == nil { http.Error(w, "no LocalBackend", http.StatusServiceUnavailable) return }
// As this is only meant for debug, verify there's no DNS name being used to // access this. if !strings.HasPrefix(r.Host, "localhost:") && strings.IndexFunc(r.Host, unicode.IsLetter) != -1 { http.Error(w, "invalid host", http.StatusForbidden) return }
w.Header().Set("Content-Security-Policy", `default-src 'none'; frame-ancestors 'none'; script-src 'none'; script-src-elem 'none'; script-src-attr 'none'`) w.Header().Set("X-Frame-Options", "DENY") w.Header().Set("X-Content-Type-Options", "nosniff") w.Header().Set("Content-Type", "text/html; charset=utf-8") st := lb.Status() // TODO(bradfitz): add LogID and opts to st? st.WriteHTML(w)}