tailscaled tailscale combined (linux/amd64)
29853147 17384418 31412596 omitting everything
+ 621570 + 219277 + 554256 .. add serve
Updates #17128
Change-Id: I87c2c6c3d3fc2dc026c3de8ef7000a813b41d31c
Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
main
parent
5b5ae2b2ee
commit
4cca9f7c67
@ -0,0 +1,55 @@ |
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !ts_omit_serve
|
||||
|
||||
package local |
||||
|
||||
import ( |
||||
"context" |
||||
"encoding/json" |
||||
"fmt" |
||||
"net/http" |
||||
|
||||
"tailscale.com/ipn" |
||||
) |
||||
|
||||
// GetServeConfig return the current serve config.
|
||||
//
|
||||
// If the serve config is empty, it returns (nil, nil).
|
||||
func (lc *Client) GetServeConfig(ctx context.Context) (*ipn.ServeConfig, error) { |
||||
body, h, err := lc.sendWithHeaders(ctx, "GET", "/localapi/v0/serve-config", 200, nil, nil) |
||||
if err != nil { |
||||
return nil, fmt.Errorf("getting serve config: %w", err) |
||||
} |
||||
sc, err := getServeConfigFromJSON(body) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
if sc == nil { |
||||
sc = new(ipn.ServeConfig) |
||||
} |
||||
sc.ETag = h.Get("Etag") |
||||
return sc, nil |
||||
} |
||||
|
||||
func getServeConfigFromJSON(body []byte) (sc *ipn.ServeConfig, err error) { |
||||
if err := json.Unmarshal(body, &sc); err != nil { |
||||
return nil, err |
||||
} |
||||
return sc, nil |
||||
} |
||||
|
||||
// SetServeConfig sets or replaces the serving settings.
|
||||
// If config is nil, settings are cleared and serving is disabled.
|
||||
func (lc *Client) SetServeConfig(ctx context.Context, config *ipn.ServeConfig) error { |
||||
h := make(http.Header) |
||||
if config != nil { |
||||
h.Set("If-Match", config.ETag) |
||||
} |
||||
_, _, err := lc.sendWithHeaders(ctx, "POST", "/localapi/v0/serve-config", 200, jsonBody(config), h) |
||||
if err != nil { |
||||
return fmt.Errorf("sending serve config: %w", err) |
||||
} |
||||
return nil |
||||
} |
||||
@ -0,0 +1,34 @@ |
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build ts_omit_serve
|
||||
|
||||
// These are temporary (2025-09-13) stubs for when tailscaled is built with the
|
||||
// ts_omit_serve build tag, disabling serve.
|
||||
//
|
||||
// TODO: move serve to a separate package, out of ipnlocal, and delete this
|
||||
// file. One step at a time.
|
||||
|
||||
package ipnlocal |
||||
|
||||
import ( |
||||
"tailscale.com/ipn" |
||||
"tailscale.com/tailcfg" |
||||
) |
||||
|
||||
const serveEnabled = false |
||||
|
||||
type localListener = struct{} |
||||
|
||||
func (b *LocalBackend) DeleteForegroundSession(sessionID string) error { |
||||
return nil |
||||
} |
||||
|
||||
type funnelFlow = struct{} |
||||
|
||||
func (*LocalBackend) hasIngressEnabledLocked() bool { return false } |
||||
func (*LocalBackend) shouldWireInactiveIngressLocked() bool { return false } |
||||
|
||||
func (b *LocalBackend) vipServicesFromPrefsLocked(prefs ipn.PrefsView) []*tailcfg.VIPService { |
||||
return nil |
||||
} |
||||
@ -0,0 +1,108 @@ |
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
//go:build !ts_omit_serve
|
||||
|
||||
package localapi |
||||
|
||||
import ( |
||||
"crypto/sha256" |
||||
"encoding/hex" |
||||
"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 := h.b.ServeConfig() |
||||
bts, err := json.Marshal(config) |
||||
if err != nil { |
||||
http.Error(w, "error encoding config: "+err.Error(), http.StatusInternalServerError) |
||||
return |
||||
} |
||||
sum := sha256.Sum256(bts) |
||||
etag := hex.EncodeToString(sum[:]) |
||||
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") |
||||
} |
||||
} |
||||
Loading…
Reference in new issue