mirror of
https://github.com/tailscale/tailscale.git
synced 2026-05-05 20:26:47 +02:00
This change adds API to ipn.LocalBackend to retrieve the ETag when querying for the current serve config. This allows consumers of ipn.LocalBackend.SetServeConfig to utilize the concurrency control offered by ETags. Previous to this change, utilizing serve config ETags required copying the local backend's internal ETag calcuation. The local API server was previously copying the local backend's ETag calculation as described above. With this change, the local API server now uses the new ETag retrieval function instead. Serve config ETags are therefore now opaque to clients, in line with best practices. Fixes tailscale/corp#35857 Signed-off-by: Harry Harpham <harry@tailscale.com>
109 lines
2.9 KiB
Go
109 lines
2.9 KiB
Go
// Copyright (c) Tailscale Inc & AUTHORS
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
//go:build !ts_omit_serve
|
|
|
|
package localapi
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"runtime"
|
|
|
|
"tailscale.com/ipn"
|
|
"tailscale.com/ipn/ipnlocal"
|
|
"tailscale.com/util/httpm"
|
|
"tailscale.com/version"
|
|
)
|
|
|
|
func init() {
|
|
Register("serve-config", (*Handler).serveServeConfig)
|
|
}
|
|
|
|
func (h *Handler) serveServeConfig(w http.ResponseWriter, r *http.Request) {
|
|
switch r.Method {
|
|
case httpm.GET:
|
|
if !h.PermitRead {
|
|
http.Error(w, "serve config denied", http.StatusForbidden)
|
|
return
|
|
}
|
|
config, etag, err := h.b.ServeConfigETag()
|
|
if err != nil {
|
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
bts, err := json.Marshal(config)
|
|
if err != nil {
|
|
http.Error(w, "error encoding config: "+err.Error(), http.StatusInternalServerError)
|
|
return
|
|
}
|
|
w.Header().Set("Etag", etag)
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.Write(bts)
|
|
case httpm.POST:
|
|
if !h.PermitWrite {
|
|
http.Error(w, "serve config denied", http.StatusForbidden)
|
|
return
|
|
}
|
|
configIn := new(ipn.ServeConfig)
|
|
if err := json.NewDecoder(r.Body).Decode(configIn); err != nil {
|
|
WriteErrorJSON(w, fmt.Errorf("decoding config: %w", err))
|
|
return
|
|
}
|
|
|
|
// require a local admin when setting a path handler
|
|
// TODO: roll-up this Windows-specific check into either PermitWrite
|
|
// or a global admin escalation check.
|
|
if err := authorizeServeConfigForGOOSAndUserContext(runtime.GOOS, configIn, h); err != nil {
|
|
http.Error(w, err.Error(), http.StatusUnauthorized)
|
|
return
|
|
}
|
|
|
|
etag := r.Header.Get("If-Match")
|
|
if err := h.b.SetServeConfig(configIn, etag); err != nil {
|
|
if errors.Is(err, ipnlocal.ErrETagMismatch) {
|
|
http.Error(w, err.Error(), http.StatusPreconditionFailed)
|
|
return
|
|
}
|
|
WriteErrorJSON(w, fmt.Errorf("updating config: %w", err))
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusOK)
|
|
default:
|
|
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
|
|
}
|
|
}
|
|
|
|
func authorizeServeConfigForGOOSAndUserContext(goos string, configIn *ipn.ServeConfig, h *Handler) error {
|
|
switch goos {
|
|
case "windows", "linux", "darwin", "illumos", "solaris":
|
|
default:
|
|
return nil
|
|
}
|
|
// Only check for local admin on tailscaled-on-mac (based on "sudo"
|
|
// permissions). On sandboxed variants (MacSys and AppStore), tailscaled
|
|
// cannot serve files outside of the sandbox and this check is not
|
|
// relevant.
|
|
if goos == "darwin" && version.IsSandboxedMacOS() {
|
|
return nil
|
|
}
|
|
if !configIn.HasPathHandler() {
|
|
return nil
|
|
}
|
|
if h.Actor.IsLocalAdmin(h.b.OperatorUserID()) {
|
|
return nil
|
|
}
|
|
switch goos {
|
|
case "windows":
|
|
return errors.New("must be a Windows local admin to serve a path")
|
|
case "linux", "darwin", "illumos", "solaris":
|
|
return errors.New("must be root, or be an operator and able to run 'sudo tailscale' to serve a path")
|
|
default:
|
|
// We filter goos at the start of the func, this default case
|
|
// should never happen.
|
|
panic("unreachable")
|
|
}
|
|
}
|