This commit is contained in:
bluepython508 committed 2026-10-22 12:27:19 +01:00
1 parent 3530afb854
commit 6903fcff27
2146 files changed
+112256 -190703

No files matched your search

+13 -6
View File
@@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
//go:build !ios && !android && !js && !ts_omit_acme
@@ -6,12 +6,15 @@
package localapi
import (
"errors"
"fmt"
"maps"
"net/http"
"strings"
"time"
"tailscale.com/ipn/ipnlocal"
"tailscale.com/tsweb"
)
func init() {
@@ -39,11 +42,15 @@ func (h *Handler) serveCert(w http.ResponseWriter, r *http.Request) {
}
pair, err := h.b.GetCertPEMWithValidity(r.Context(), domain, minValidity)
if err != nil {
// TODO(bradfitz): 500 is a little lazy here. The errors returned from
// GetCertPEM (and everywhere) should carry info info to get whether
// they're 400 vs 403 vs 500 at minimum. And then we should have helpers
// (in tsweb probably) to return an error that looks at the error value
// to determine the HTTP status code.
if hs, ok := errors.AsType[tsweb.HTTPStatuser](err); ok {
resp := hs.HTTPStatus()
maps.Copy(w.Header(), resp.Header)
http.Error(w, resp.Msg, resp.Code)
return
}
// TODO(bradfitz): 500 is a little lazy. Other errors from GetCertPEM
// should also implement tsweb.HTTPStatuser (400 vs 403 vs 500 vs …)
// rather than falling through here.
http.Error(w, fmt.Sprint(err), 500)
return
}
+106 -12
View File
@@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
//go:build !ts_omit_debug
@@ -6,8 +6,10 @@
package localapi
import (
"cmp"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net"
@@ -23,6 +25,7 @@ import (
"tailscale.com/feature"
"tailscale.com/feature/buildfeatures"
"tailscale.com/ipn"
"tailscale.com/tailcfg"
"tailscale.com/types/logger"
"tailscale.com/util/eventbus"
"tailscale.com/util/httpm"
@@ -35,6 +38,7 @@ func init() {
Register("dev-set-state-store", (*Handler).serveDevSetStateStore)
Register("debug-bus-events", (*Handler).serveDebugBusEvents)
Register("debug-bus-graph", (*Handler).serveEventBusGraph)
Register("debug-bus-queues", (*Handler).serveDebugBusQueues)
Register("debug-derp-region", (*Handler).serveDebugDERPRegion)
Register("debug-dial-types", (*Handler).serveDebugDialTypes)
Register("debug-log", (*Handler).serveDebugLog)
@@ -140,14 +144,11 @@ func (h *Handler) serveDebugDialTypes(w http.ResponseWriter, r *http.Request) {
var wg sync.WaitGroup
for _, dialer := range dialers {
dialer := dialer // loop capture
wg.Add(1)
go func() {
defer wg.Done()
wg.Go(func() {
conn, err := dialer.dial(ctx, network, addr)
results <- result{dialer.name, conn, err}
}()
})
}
wg.Wait()
@@ -200,8 +201,6 @@ func (h *Handler) serveDebug(w http.ResponseWriter, r *http.Request) {
break
}
h.b.DebugNotify(n)
case "notify-last-netmap":
h.b.DebugNotifyLastNetMap()
case "break-tcp-conns":
err = h.b.DebugBreakTCPConns()
case "break-derp-conns":
@@ -218,7 +217,7 @@ func (h *Handler) serveDebug(w http.ResponseWriter, r *http.Request) {
case "pick-new-derp":
err = h.b.DebugPickNewDERP()
case "force-prefer-derp":
var n int
var n tailcfg.DERPRegionID
err = json.NewDecoder(r.Body).Decode(&n)
if err != nil {
break
@@ -233,8 +232,39 @@ func (h *Handler) serveDebug(w http.ResponseWriter, r *http.Request) {
if err == nil {
return
}
case "peer-disco-keys":
w.Header().Set("Content-Type", "application/json")
err = json.NewEncoder(w).Encode(h.b.DebugPeerDiscoKeys())
if err == nil {
return
}
case "rotate-disco-key":
err = h.b.DebugRotateDiscoKey()
case "statedir":
root := h.b.TailscaleVarRoot()
w.Header().Set("Content-Type", "application/json")
err = json.NewEncoder(w).Encode(root)
if err == nil {
return
}
case "clear-netmap-cache":
h.b.ClearNetmapCache(r.Context())
case "current-netmap":
// Return the current netmap (with peers populated) as JSON. This
// is a debug-only path: the netmap.NetworkMap shape is an
// internal type and may change without notice. Production
// callers should fetch the narrower bits they need via their
// own LocalAPI methods instead.
nm := h.b.NetMapWithPeers()
if nm == nil {
err = errors.New("no netmap")
break
}
w.Header().Set("Content-Type", "application/json")
err = json.NewEncoder(w).Encode(nm)
if err == nil {
return
}
case "":
err = fmt.Errorf("missing parameter 'action'")
default:
@@ -253,6 +283,13 @@ func (h *Handler) serveDevSetStateStore(w http.ResponseWriter, r *http.Request)
http.Error(w, "debug access denied", http.StatusForbidden)
return
}
// state keys are otherwise gated by their own handlers, e.g. serve-config
// requires a local admin for Unix-socket targets; writing a _serve/<profile>
// key here would bypass that, so require a local admin too
if !h.Actor.IsLocalAdmin(h.b.OperatorUserID()) {
http.Error(w, "dev-set-state-store access denied; must be a local admin", http.StatusUnauthorized)
return
}
if r.Method != httpm.POST {
http.Error(w, "POST required", http.StatusMethodNotAllowed)
return
@@ -270,7 +307,7 @@ func (h *Handler) serveDebugPacketFilterRules(w http.ResponseWriter, r *http.Req
http.Error(w, "debug access denied", http.StatusForbidden)
return
}
nm := h.b.NetMap()
nm := h.b.NetMapNoPeers()
if nm == nil {
http.Error(w, "no netmap", http.StatusNotFound)
return
@@ -287,7 +324,7 @@ func (h *Handler) serveDebugPacketFilterMatches(w http.ResponseWriter, r *http.R
http.Error(w, "debug access denied", http.StatusForbidden)
return
}
nm := h.b.NetMap()
nm := h.b.NetMapNoPeers()
if nm == nil {
http.Error(w, "no netmap", http.StatusNotFound)
return
@@ -424,12 +461,68 @@ func (h *Handler) serveEventBusGraph(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(topics)
}
func (h *Handler) serveDebugBusQueues(w http.ResponseWriter, r *http.Request) {
if r.Method != httpm.GET {
http.Error(w, "GET required", http.StatusMethodNotAllowed)
return
}
bus, ok := h.LocalBackend().Sys().Bus.GetOK()
if !ok {
http.Error(w, "event bus not running", http.StatusPreconditionFailed)
return
}
debugger := bus.Debugger()
type clientQueue struct {
Name string `json:"name"`
SubscribeDepth int `json:"subscribeDepth"`
SubscribeTypes []string `json:"subscribeTypes,omitempty"`
PublishTypes []string `json:"publishTypes,omitempty"`
}
publishQueue := debugger.PublishQueue()
clients := debugger.Clients()
result := struct {
PublishQueueDepth int `json:"publishQueueDepth"`
Clients []clientQueue `json:"clients"`
}{
PublishQueueDepth: len(publishQueue),
}
for _, c := range clients {
sq := debugger.SubscribeQueue(c)
cq := clientQueue{
Name: c.Name(),
SubscribeDepth: len(sq),
}
for _, t := range debugger.SubscribeTypes(c) {
cq.SubscribeTypes = append(cq.SubscribeTypes, t.String())
}
for _, t := range debugger.PublishTypes(c) {
cq.PublishTypes = append(cq.PublishTypes, t.String())
}
result.Clients = append(result.Clients, cq)
}
slices.SortFunc(result.Clients, func(a, b clientQueue) int {
if a.SubscribeDepth != b.SubscribeDepth {
return b.SubscribeDepth - a.SubscribeDepth
}
return cmp.Compare(a.Name, b.Name)
})
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(result)
}
func (h *Handler) serveDebugLog(w http.ResponseWriter, r *http.Request) {
if !buildfeatures.HasLogTail {
http.Error(w, feature.ErrUnavailable.Error(), http.StatusNotImplemented)
return
}
if !h.PermitRead {
if !h.PermitWrite {
http.Error(w, "debug-log access denied", http.StatusForbidden)
return
}
@@ -472,6 +565,7 @@ func (h *Handler) serveDebugLog(w http.ResponseWriter, r *http.Request) {
func (h *Handler) serveDebugOptionalFeatures(w http.ResponseWriter, r *http.Request) {
of := &apitype.OptionalFeatures{
Features: feature.Registered(),
Disabled: feature.EnvDisabled(),
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(of)
+22 -11
View File
@@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
//go:build !ts_omit_debug
@@ -15,6 +15,7 @@ import (
"net/http"
"net/netip"
"strconv"
"strings"
"time"
"tailscale.com/derp/derphttp"
@@ -22,11 +23,28 @@ import (
"tailscale.com/net/netaddr"
"tailscale.com/net/netns"
"tailscale.com/net/stun"
"tailscale.com/net/tlsdial"
"tailscale.com/tailcfg"
"tailscale.com/types/key"
"tailscale.com/types/nettype"
)
// tlsConfigForNode builds a *tls.Config for connecting to a DERP node,
// mirroring the logic in derphttp.Client.tlsClient so that sha256-raw cert
// pinning and domain-fronting CertName values are handled correctly.
func tlsConfigForNode(node *tailcfg.DERPNode) *tls.Config {
conf := tlsdial.Config(nil, nil)
conf.ServerName = node.HostName
if node.CertName != "" {
if suf, ok := strings.CutPrefix(node.CertName, "sha256-raw:"); ok {
tlsdial.SetConfigExpectedCertHash(conf, suf)
} else {
tlsdial.SetConfigExpectedCert(conf, node.CertName)
}
}
return conf
}
func (h *Handler) serveDebugDERPRegion(w http.ResponseWriter, r *http.Request) {
if !h.PermitWrite {
http.Error(w, "debug access denied", http.StatusForbidden)
@@ -50,7 +68,7 @@ func (h *Handler) serveDebugDERPRegion(w http.ResponseWriter, r *http.Request) {
}
regStr := r.FormValue("region")
var reg *tailcfg.DERPRegion
if id, err := strconv.Atoi(regStr); err == nil {
if id, err := tailcfg.ParseDERPRegionID(regStr); err == nil {
reg = dm.Regions[id]
} else {
for _, r := range dm.Regions {
@@ -100,9 +118,7 @@ func (h *Handler) serveDebugDERPRegion(w http.ResponseWriter, r *http.Request) {
defer conn.Close()
// Upgrade to TLS and verify that works properly.
tlsConn := tls.Client(conn, &tls.Config{
ServerName: cmp.Or(derpNode.CertName, derpNode.HostName),
})
tlsConn := tls.Client(conn, tlsConfigForNode(derpNode))
if err := tlsConn.HandshakeContext(ctx); err != nil {
st.Errors = append(st.Errors, fmt.Sprintf("Error upgrading connection to node %q @ %q to TLS over IPv4: %v", derpNode.HostName, addr, err))
} else {
@@ -119,12 +135,7 @@ func (h *Handler) serveDebugDERPRegion(w http.ResponseWriter, r *http.Request) {
defer conn.Close()
// Upgrade to TLS and verify that works properly.
tlsConn := tls.Client(conn, &tls.Config{
ServerName: cmp.Or(derpNode.CertName, derpNode.HostName),
// TODO(andrew-d): we should print more
// detailed failure information on if/why TLS
// verification fails
})
tlsConn := tls.Client(conn, tlsConfigForNode(derpNode))
if err := tlsConn.HandshakeContext(ctx); err != nil {
st.Errors = append(st.Errors, fmt.Sprintf("Error upgrading connection to node %q @ %q to TLS over IPv6: %v", derpNode.HostName, addr, err))
} else {
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
//go:build ios || android || js
+258 -64
View File
@@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
// Package localapi contains the HTTP server handlers for tailscaled's API server.
@@ -7,6 +7,7 @@ package localapi
import (
"bytes"
"cmp"
"context"
"crypto/subtle"
"encoding/json"
"errors"
@@ -28,13 +29,13 @@ import (
"tailscale.com/envknob"
"tailscale.com/feature"
"tailscale.com/feature/buildfeatures"
"tailscale.com/health/healthmsg"
"tailscale.com/hostinfo"
"tailscale.com/ipn"
"tailscale.com/ipn/ipnauth"
"tailscale.com/ipn/ipnlocal"
"tailscale.com/ipn/ipnstate"
"tailscale.com/logtail"
"tailscale.com/net/neterror"
"tailscale.com/net/netns"
"tailscale.com/net/netutil"
"tailscale.com/tailcfg"
@@ -43,8 +44,8 @@ import (
"tailscale.com/types/key"
"tailscale.com/types/logger"
"tailscale.com/types/logid"
"tailscale.com/types/ptr"
"tailscale.com/util/clientmetric"
"tailscale.com/util/def"
"tailscale.com/util/eventbus"
"tailscale.com/util/httpm"
"tailscale.com/util/mak"
@@ -73,20 +74,25 @@ var handler = map[string]LocalAPIHandler{
// The other /localapi/v0/NAME handlers are exact matches and contain only NAME
// without a trailing slash:
"cert-domains": (*Handler).serveCertDomains,
"check-prefs": (*Handler).serveCheckPrefs,
"check-so-mark-in-use": (*Handler).serveCheckSOMarkInUse,
"derpmap": (*Handler).serveDERPMap,
"dns-config": (*Handler).serveDNSConfig,
"goroutines": (*Handler).serveGoroutines,
"login-interactive": (*Handler).serveLoginInteractive,
"logout": (*Handler).serveLogout,
"peer-by-id": (*Handler).servePeerByID,
"ping": (*Handler).servePing,
"prefs": (*Handler).servePrefs,
"reload-config": (*Handler).reloadConfig,
"reset-auth": (*Handler).serveResetAuth,
"services": (*Handler).serveServices,
"set-expiry-sooner": (*Handler).serveSetExpirySooner,
"shutdown": (*Handler).serveShutdown,
"start": (*Handler).serveStart,
"status": (*Handler).serveStatus,
"user-profile": (*Handler).serveUserProfile,
"whois": (*Handler).serveWhoIs,
}
@@ -99,9 +105,6 @@ func init() {
Register("check-udp-gro-forwarding", (*Handler).serveCheckUDPGROForwarding)
Register("set-udp-gro-forwarding", (*Handler).serveSetUDPGROForwarding)
}
if buildfeatures.HasUseExitNode && runtime.GOOS == "linux" {
Register("check-reverse-path-filtering", (*Handler).serveCheckReversePathFiltering)
}
if buildfeatures.HasClientMetrics {
Register("upload-client-metrics", (*Handler).serveUploadClientMetrics)
}
@@ -119,7 +122,7 @@ func init() {
Register("bugreport", (*Handler).serveBugReport)
Register("pprof", (*Handler).servePprof)
}
if buildfeatures.HasDebug || buildfeatures.HasServe {
if buildfeatures.HasIPNBus {
Register("watch-ipn-bus", (*Handler).serveWatchIPNBus)
}
if buildfeatures.HasDNS {
@@ -266,6 +269,7 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return
}
}
defer h.b.CheckDeadlocks()()
if fn, route, ok := handlerForPath(r.URL.Path); ok {
h.logRequest(r.Method, route)
fn(h, w, r)
@@ -350,7 +354,7 @@ func (h *Handler) serveIDToken(w http.ResponseWriter, r *http.Request) {
http.Error(w, "id-token access denied", http.StatusForbidden)
return
}
nm := h.b.NetMap()
nm := h.b.NetMapNoPeers()
if nm == nil {
http.Error(w, "no netmap", http.StatusServiceUnavailable)
return
@@ -400,7 +404,7 @@ func (h *Handler) serveBugReport(w http.ResponseWriter, r *http.Request) {
defer h.b.TryFlushLogs() // kick off upload after bugreport's done logging
logMarker := func() string {
return fmt.Sprintf("BUG-%v-%v-%v", h.backendLogID, h.clock.Now().UTC().Format("20060102150405Z"), rands.HexString(16))
return fmt.Sprintf("BUG-%v-%v-%v", h.backendLogID, h.clock.Now().UTC().Format(tstime.NumericDateTimeZ), rands.HexString(16))
}
if envknob.NoLogsNoSupport() {
logMarker = func() string { return "BUG-NO-LOGS-NO-SUPPORT-this-node-has-had-its-logging-disabled" }
@@ -420,7 +424,7 @@ func (h *Handler) serveBugReport(w http.ResponseWriter, r *http.Request) {
}
// Information about the current node from the netmap
if nm := h.b.NetMap(); nm != nil {
if nm := h.b.NetMapNoPeers(); nm != nil {
if self := nm.SelfNode; self.Valid() {
h.logf("user bugreport node info: nodeid=%q stableid=%q expiry=%q", self.ID(), self.StableID(), self.KeyExpiry().Format(time.RFC3339))
}
@@ -438,7 +442,7 @@ func (h *Handler) serveBugReport(w http.ResponseWriter, r *http.Request) {
h.logf.JSON(1, "UserBugReportOS", osdiag.SupportInfo(osdiag.LogSupportInfoReasonBugReport))
// Tailnet Lock details
st := h.b.NetworkLockStatus()
st := h.b.TailnetLockStatus()
if st.Enabled {
h.logf.JSON(1, "UserBugReportTailnetLockStatus", st)
if st.NodeKeySignature != nil {
@@ -446,7 +450,7 @@ func (h *Handler) serveBugReport(w http.ResponseWriter, r *http.Request) {
}
}
if defBool(r.URL.Query().Get("diagnose"), false) {
if def.Bool(r.URL.Query().Get("diagnose"), false) {
if f, ok := ipnlocal.HookDoctor.GetOk(); ok {
f(r.Context(), h.b, logger.WithPrefix(h.logf, "diag: "))
}
@@ -456,7 +460,7 @@ func (h *Handler) serveBugReport(w http.ResponseWriter, r *http.Request) {
// Nothing else to do if we're not in record mode; we wrote the marker
// above, so we can just finish our response now.
if !defBool(r.URL.Query().Get("record"), false) {
if !def.Bool(r.URL.Query().Get("record"), false) {
return
}
@@ -545,6 +549,8 @@ type localBackendWhoIsMethods interface {
WhoIs(string, netip.AddrPort) (n tailcfg.NodeView, u tailcfg.UserProfile, ok bool)
WhoIsNodeKey(key.NodePublic) (n tailcfg.NodeView, u tailcfg.UserProfile, ok bool)
PeerCaps(netip.Addr) tailcfg.PeerCapMap
PeerCapsForIP(src, dst netip.Addr) tailcfg.PeerCapMap
PeerCapsForService(src netip.Addr, svcName tailcfg.ServiceName) tailcfg.PeerCapMap
}
func (h *Handler) serveWhoIsWithBackend(w http.ResponseWriter, r *http.Request, b localBackendWhoIsMethods) {
@@ -592,7 +598,25 @@ func (h *Handler) serveWhoIsWithBackend(w http.ResponseWriter, r *http.Request,
UserProfile: &u, // always non-nil per WhoIsResponse contract
}
if n.Addresses().Len() > 0 {
res.CapMap = b.PeerCaps(n.Addresses().At(0).Addr())
src := n.Addresses().At(0).Addr()
switch {
case r.FormValue("svc_name") != "":
svcName := tailcfg.AsServiceName(r.FormValue("svc_name"))
if svcName == "" {
http.Error(w, "invalid svc_name", http.StatusBadRequest)
return
}
res.CapMap = b.PeerCapsForService(src, svcName)
case r.FormValue("dst_ip") != "":
svcAddr, err := netip.ParseAddr(r.FormValue("dst_ip"))
if err != nil {
http.Error(w, "invalid dst_ip", http.StatusBadRequest)
return
}
res.CapMap = b.PeerCapsForIP(src, svcAddr)
default:
res.CapMap = b.PeerCaps(src)
}
}
j, err := json.MarshalIndent(res, "", "\t")
if err != nil {
@@ -779,32 +803,6 @@ func (h *Handler) serveCheckSOMarkInUse(w http.ResponseWriter, r *http.Request)
})
}
func (h *Handler) serveCheckReversePathFiltering(w http.ResponseWriter, r *http.Request) {
if !h.PermitRead {
http.Error(w, "reverse path filtering check access denied", http.StatusForbidden)
return
}
var warning string
state := h.b.Sys().NetMon.Get().InterfaceState()
warn, err := netutil.CheckReversePathFiltering(state)
if err == nil && len(warn) > 0 {
var msg strings.Builder
msg.WriteString(healthmsg.WarnExitNodeUsage + ":\n")
for _, w := range warn {
msg.WriteString("- " + w + "\n")
}
msg.WriteString(healthmsg.DisableRPFilter)
warning = msg.String()
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(struct {
Warning string
}{
Warning: warning,
})
}
func (h *Handler) serveCheckUDPGROForwarding(w http.ResponseWriter, r *http.Request) {
if !h.PermitRead {
http.Error(w, "UDP GRO forwarding check access denied", http.StatusForbidden)
@@ -850,7 +848,7 @@ func (h *Handler) serveStatus(w http.ResponseWriter, r *http.Request) {
}
w.Header().Set("Content-Type", "application/json")
var st *ipnstate.Status
if defBool(r.FormValue("peers"), true) {
if def.Bool(r.FormValue("peers"), true) {
st = h.b.Status()
} else {
st = h.b.StatusWithoutPeers()
@@ -874,8 +872,8 @@ func InUseOtherUserIPNStream(w http.ResponseWriter, r *http.Request, err error)
}
js, err := json.Marshal(&ipn.Notify{
Version: version.Long(),
State: ptr.To(ipn.InUseOtherUser),
ErrMessage: ptr.To(err.Error()),
State: new(ipn.InUseOtherUser),
ErrMessage: new(err.Error()),
})
if err != nil {
return false
@@ -899,12 +897,18 @@ func (h *Handler) serveWatchIPNBus(w http.ResponseWriter, r *http.Request) {
var mask ipn.NotifyWatchOpt
if s := r.FormValue("mask"); s != "" {
v, err := strconv.ParseUint(s, 10, 64)
if err != nil {
if err := mask.UnmarshalText([]byte(s)); err != nil {
http.Error(w, "bad mask", http.StatusBadRequest)
return
}
mask = ipn.NotifyWatchOpt(v)
}
if mask&ipn.NotifyInProcessNoDisconnect != 0 {
http.Error(w, "NotifyInProcessNoDisconnect is only valid for in-process IPN bus subscribers", http.StatusBadRequest)
return
}
if err := ipn.ValidateNotifyWatchOpt(mask); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", "application/json")
@@ -913,7 +917,9 @@ func (h *Handler) serveWatchIPNBus(w http.ResponseWriter, r *http.Request) {
h.b.WatchNotificationsAs(ctx, h.Actor, mask, f.Flush, func(roNotify *ipn.Notify) (keepGoing bool) {
err := enc.Encode(roNotify)
if err != nil {
h.logf("json.Encode: %v", err)
if !neterror.IsClosedPipeError(err) {
h.logf("json.Encode: %v", err)
}
return false
}
f.Flush()
@@ -1100,6 +1106,121 @@ func (h *Handler) serveDERPMap(w http.ResponseWriter, r *http.Request) {
e.Encode(h.b.DERPMap())
}
// serveCertDomains returns the list of DNS.CertDomains from the current
// netmap, or an empty list if no netmap has been received yet.
// The returned list is sorted in ascending order.
func (h *Handler) serveCertDomains(w http.ResponseWriter, r *http.Request) {
if !h.PermitRead {
http.Error(w, "cert-domains access denied", http.StatusForbidden)
return
}
var domains []string
if nm := h.b.NetMapNoPeers(); nm != nil {
domains = slices.Clone(nm.DNS.CertDomains)
slices.Sort(domains)
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(domains)
}
// serveDNSConfig returns the [tailcfg.DNSConfig] from the current netmap.
// It returns 503 if no netmap has been received yet.
func (h *Handler) serveDNSConfig(w http.ResponseWriter, r *http.Request) {
if !h.PermitRead {
http.Error(w, "dns-config access denied", http.StatusForbidden)
return
}
nm := h.b.NetMapNoPeers()
if nm == nil {
http.Error(w, "no netmap", http.StatusServiceUnavailable)
return
}
w.Header().Set("Content-Type", "application/json")
e := json.NewEncoder(w)
e.SetIndent("", "\t")
e.Encode(nm.DNS)
}
// peerByIDBackend is the subset of [ipnlocal.LocalBackend] used by
// [Handler.servePeerByID]. It exists so the handler can be tested with a
// trivial mock without spinning up a full LocalBackend.
type peerByIDBackend interface {
PeerByID(tailcfg.NodeID) (tailcfg.NodeView, bool)
}
// servePeerByID returns the current full [tailcfg.Node] for the peer with the
// NodeID given in the "id" query parameter. It returns 404 if no such peer is
// in the current netmap.
//
// It is intended for clients that observed a peer-mutation signal (e.g.
// [ipn.Notify.PeerChangedPatch] or [ipn.Notify.PeersChanged]) and want the
// latest state of the affected node without having to apply the patch
// themselves.
func (h *Handler) servePeerByID(w http.ResponseWriter, r *http.Request) {
h.servePeerByIDWithBackend(w, r, h.b)
}
func (h *Handler) servePeerByIDWithBackend(w http.ResponseWriter, r *http.Request, b peerByIDBackend) {
if !h.PermitRead {
http.Error(w, "peer-by-id access denied", http.StatusForbidden)
return
}
idStr := r.FormValue("id")
id, err := strconv.ParseInt(idStr, 10, 64)
if err != nil || id <= 0 {
http.Error(w, "invalid 'id' parameter", http.StatusBadRequest)
return
}
nv, ok := b.PeerByID(tailcfg.NodeID(id))
if !ok {
http.Error(w, "no peer with that NodeID", http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "application/json")
e := json.NewEncoder(w)
e.SetIndent("", "\t")
e.Encode(nv.AsStruct())
}
// userProfileBackend is the subset of [ipnlocal.LocalBackend] used by
// [Handler.serveUserProfile]. It exists so the handler can be tested
// with a trivial mock without spinning up a full LocalBackend.
type userProfileBackend interface {
UserProfile(tailcfg.UserID) (tailcfg.UserProfileView, bool)
}
// serveUserProfile returns the current [tailcfg.UserProfile] for the User
// with the UserID given in the "id" query parameter, in O(1) time. It
// returns 404 if no such user is in the current netmap.
//
// It is the LocalAPI fallback for IPN-bus consumers that see a UserID
// referenced by a peer Node and want to resolve it to a UserProfile.
func (h *Handler) serveUserProfile(w http.ResponseWriter, r *http.Request) {
h.serveUserProfileWithBackend(w, r, h.b)
}
func (h *Handler) serveUserProfileWithBackend(w http.ResponseWriter, r *http.Request, b userProfileBackend) {
if !h.PermitRead {
http.Error(w, "user-profile access denied", http.StatusForbidden)
return
}
idStr := r.FormValue("id")
id, err := strconv.ParseInt(idStr, 10, 64)
if err != nil || id <= 0 {
http.Error(w, "invalid 'id' parameter", http.StatusBadRequest)
return
}
uv, ok := b.UserProfile(tailcfg.UserID(id))
if !ok {
http.Error(w, "no user with that UserID", http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "application/json")
e := json.NewEncoder(w)
e.SetIndent("", "\t")
e.Encode(uv.AsStruct())
}
// serveSetExpirySooner sets the expiry date on the current machine, specified
// by an `expiry` unix timestamp as POST or query param.
func (h *Handler) serveSetExpirySooner(w http.ResponseWriter, r *http.Request) {
@@ -1196,15 +1317,33 @@ func (h *Handler) serveDial(w http.ResponseWriter, r *http.Request) {
http.Error(w, "missing Dial-Host or Dial-Port header", http.StatusBadRequest)
return
}
network := cmp.Or(r.Header.Get("Dial-Network"), "tcp")
addr := net.JoinHostPort(hostStr, portStr)
// Check whether the resolved address is a Tailscale route.
// If not, tell the client to dial it directly so the connection
// comes from the calling user's UID rather than our root-owned daemon.
ipp, viaTailscale, err := h.b.Dialer().UserDialPlan(r.Context(), network, addr)
if err != nil {
http.Error(w, "resolve failure: "+err.Error(), http.StatusBadGateway)
return
}
if !viaTailscale {
w.Header().Set("Dial-Self", "true")
w.Header().Set("Dial-Addr", ipp.String())
w.WriteHeader(http.StatusOK)
return
}
hijacker, ok := w.(http.Hijacker)
if !ok {
http.Error(w, "make request over HTTP/1", http.StatusBadRequest)
return
}
network := cmp.Or(r.Header.Get("Dial-Network"), "tcp")
addr := net.JoinHostPort(hostStr, portStr)
// Dial via Tailscale with the original hostname so UserDial can
// resolve all addresses and race across families (happy eyeballs).
outConn, err := h.b.Dialer().UserDial(r.Context(), network, addr)
if err != nil {
http.Error(w, "dial failure: "+err.Error(), http.StatusBadGateway)
@@ -1485,7 +1624,7 @@ func (h *Handler) serveQueryFeature(w http.ResponseWriter, r *http.Request) {
http.Error(w, "missing feature", http.StatusInternalServerError)
return
}
nm := h.b.NetMap()
nm := h.b.NetMapNoPeers()
if nm == nil {
http.Error(w, "no netmap", http.StatusServiceUnavailable)
return
@@ -1521,17 +1660,6 @@ func (h *Handler) serveQueryFeature(w http.ResponseWriter, r *http.Request) {
}
}
func defBool(a string, def bool) bool {
if a == "" {
return def
}
v, err := strconv.ParseBool(a)
if err != nil {
return def
}
return v
}
// serveUpdateCheck returns the ClientVersion from Status, which contains
// information on whether an update is available, and if so, what version,
// *if* we support auto-updates on this platform. If we don't, this endpoint
@@ -1681,16 +1809,68 @@ func dnsMessageTypeForString(s string) (t dnsmessage.Type, err error) {
return 0, errors.New("unknown DNS message type: " + s)
}
// HookRouteCheckRefresh is the hook to request a refresh of the routecheck Report
// for this LocalBackend.
// It is used by serveSuggestExitNode to probe for reachable exit nodes
// before it makes a suggestion.
var HookRouteCheckRefresh feature.Hook[func(*ipnlocal.LocalBackend, context.Context, time.Duration) error]
// serveSuggestExitNode serves a POST endpoint for returning a suggested exit node.
// If the probe query parameter is true,
// then a new routecheck report will be probed
// so that the suggested exit node isn’t based on stale reachability data.
// Combined with probe=true:
// if the timeout query parameter is 0, any probes will immediately timeout;
// if the timeout is positive, probes will take that duration before timing out;
// if the timeout is negative, probes will use the default routecheck timeout.
func (h *Handler) serveSuggestExitNode(w http.ResponseWriter, r *http.Request) {
if !buildfeatures.HasUseExitNode {
http.Error(w, feature.ErrUnavailable.Error(), http.StatusNotImplemented)
return
}
if r.Method != httpm.GET {
http.Error(w, "only GET allowed", http.StatusMethodNotAllowed)
switch r.Method {
case httpm.GET:
// The GET method is inappropriate because it isn’t cacheable.
// However, we retain it for backwards compatibility.
//
// The original implementation used to only allow GET requests,
// but since this endpoint powers the `tailscale exit-node suggest` command
// whose results can change based on the shape of the network,
// it was never a proper GET method in the first place.
//
// Now that the suggestions are backed by routecheck
// and we provide the user the ability to trigger a probe,
// this is obviously a POST method because it has side-effects.
// However, we don’t want to break existing clients,
// so we silently support the old GET method
// without the extra query parameters.
//
// This is also why the default case returns a "want POST" error
// and not an "only POST allowed" like the other endpoints.
// We still accept GET requests but we don’t want them.
case httpm.POST:
if !def.Bool(r.FormValue("probe"), false) {
break
}
timeout := def.Duration(r.FormValue("timeout"), -1)
// Force routecheck to probe for a new report
// and use it to suggest an exit node.
routecheckRefresh := HookRouteCheckRefresh.GetOrNil()
if routecheckRefresh == nil {
break
}
if err := routecheckRefresh(h.b, r.Context(), timeout); err != nil {
WriteErrorJSON(w, err)
return
}
default:
// Discourage the GET method:
http.Error(w, "want POST", http.StatusMethodNotAllowed)
return
}
res, err := h.b.SuggestExitNode()
if err != nil {
WriteErrorJSON(w, err)
@@ -1735,6 +1915,20 @@ func (h *Handler) serveShutdown(w http.ResponseWriter, r *http.Request) {
eventbus.Publish[Shutdown](ec).Publish(Shutdown{})
}
func (h *Handler) serveServices(w http.ResponseWriter, r *http.Request) {
if r.Method != httpm.GET {
http.Error(w, "only GET allowed", http.StatusMethodNotAllowed)
return
}
nm := h.b.NetMapNoPeers()
if nm == nil {
http.Error(w, "no netmap", http.StatusServiceUnavailable)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(nm.Services())
}
func (h *Handler) serveGetAppcRouteInfo(w http.ResponseWriter, r *http.Request) {
if !buildfeatures.HasAppConnectors {
http.Error(w, feature.ErrUnavailable.Error(), http.StatusNotImplemented)
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
//go:build !ts_omit_drive
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
//go:build !ios && !android && !js && !ts_omit_debug
+6 -5
View File
@@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
//go:build !ts_omit_serve
@@ -53,7 +53,8 @@ func (h *Handler) serveServeConfig(w http.ResponseWriter, r *http.Request) {
return
}
// require a local admin when setting a path handler
// require a local admin when setting a path handler or serving a Unix
// domain socket
// 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 {
@@ -89,7 +90,7 @@ func authorizeServeConfigForGOOSAndUserContext(goos string, configIn *ipn.ServeC
if goos == "darwin" && version.IsSandboxedMacOS() {
return nil
}
if !configIn.HasPathHandler() {
if !configIn.HasPathHandler() && !configIn.IsServingUnixAny() {
return nil
}
if h.Actor.IsLocalAdmin(h.b.OperatorUserID()) {
@@ -97,9 +98,9 @@ func authorizeServeConfigForGOOSAndUserContext(goos string, configIn *ipn.ServeC
}
switch goos {
case "windows":
return errors.New("must be a Windows local admin to serve a path")
return errors.New("must be a Windows local admin to serve a path or Unix socket")
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")
return errors.New("must be root, or be an operator and able to run 'sudo tailscale' to serve a path or Unix socket")
default:
// We filter goos at the start of the func, this default case
// should never happen.
+1 -1
View File
@@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
//go:build !ts_omit_syspolicy
+23 -23
View File
@@ -1,4 +1,4 @@
// Copyright (c) Tailscale Inc & AUTHORS
// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
//go:build !ts_omit_tailnetlock
@@ -43,7 +43,7 @@ func (h *Handler) serveTKAStatus(w http.ResponseWriter, r *http.Request) {
return
}
j, err := json.MarshalIndent(h.b.NetworkLockStatus(), "", "\t")
j, err := json.MarshalIndent(h.b.TailnetLockStatus(), "", "\t")
if err != nil {
http.Error(w, "JSON encoding error", http.StatusInternalServerError)
return
@@ -72,7 +72,7 @@ func (h *Handler) serveTKASign(w http.ResponseWriter, r *http.Request) {
return
}
if err := h.b.NetworkLockSign(req.NodeKey, req.RotationPublic); err != nil {
if err := h.b.TailnetLockSign(req.NodeKey, req.RotationPublic); err != nil {
http.Error(w, "signing failed: "+err.Error(), http.StatusInternalServerError)
return
}
@@ -101,17 +101,17 @@ func (h *Handler) serveTKAInit(w http.ResponseWriter, r *http.Request) {
return
}
if !h.b.NetworkLockAllowed() {
if !h.b.TailnetLockAllowed() {
http.Error(w, "Tailnet Lock is not supported on your pricing plan", http.StatusForbidden)
return
}
if err := h.b.NetworkLockInit(req.Keys, req.DisablementValues, req.SupportDisablement); err != nil {
if err := h.b.TailnetLockInit(req.Keys, req.DisablementValues, req.SupportDisablement); err != nil {
http.Error(w, "initialization failed: "+err.Error(), http.StatusInternalServerError)
return
}
j, err := json.MarshalIndent(h.b.NetworkLockStatus(), "", "\t")
j, err := json.MarshalIndent(h.b.TailnetLockStatus(), "", "\t")
if err != nil {
http.Error(w, "JSON encoding error", http.StatusInternalServerError)
return
@@ -122,7 +122,7 @@ func (h *Handler) serveTKAInit(w http.ResponseWriter, r *http.Request) {
func (h *Handler) serveTKAModify(w http.ResponseWriter, r *http.Request) {
if !h.PermitWrite {
http.Error(w, "network-lock modify access denied", http.StatusForbidden)
http.Error(w, "tailnet-lock modify access denied", http.StatusForbidden)
return
}
if r.Method != httpm.POST {
@@ -140,8 +140,8 @@ func (h *Handler) serveTKAModify(w http.ResponseWriter, r *http.Request) {
return
}
if err := h.b.NetworkLockModify(req.AddKeys, req.RemoveKeys); err != nil {
http.Error(w, "network-lock modify failed: "+err.Error(), http.StatusInternalServerError)
if err := h.b.TailnetLockModify(req.AddKeys, req.RemoveKeys); err != nil {
http.Error(w, "tailnet-lock modify failed: "+err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(204)
@@ -149,7 +149,7 @@ func (h *Handler) serveTKAModify(w http.ResponseWriter, r *http.Request) {
func (h *Handler) serveTKAWrapPreauthKey(w http.ResponseWriter, r *http.Request) {
if !h.PermitWrite {
http.Error(w, "network-lock modify access denied", http.StatusForbidden)
http.Error(w, "tailnet-lock modify access denied", http.StatusForbidden)
return
}
if r.Method != httpm.POST {
@@ -172,7 +172,7 @@ func (h *Handler) serveTKAWrapPreauthKey(w http.ResponseWriter, r *http.Request)
return
}
wrappedKey, err := h.b.NetworkLockWrapPreauthKey(req.TSKey, priv)
wrappedKey, err := h.b.TailnetLockWrapPreauthKey(req.TSKey, priv)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
@@ -200,7 +200,7 @@ func (h *Handler) serveTKAVerifySigningDeeplink(w http.ResponseWriter, r *http.R
return
}
res := h.b.NetworkLockVerifySigningDeeplink(req.URL)
res := h.b.TailnetLockVerifySigningDeeplink(req.URL)
j, err := json.MarshalIndent(res, "", "\t")
if err != nil {
http.Error(w, "JSON encoding error", http.StatusInternalServerError)
@@ -212,7 +212,7 @@ func (h *Handler) serveTKAVerifySigningDeeplink(w http.ResponseWriter, r *http.R
func (h *Handler) serveTKADisable(w http.ResponseWriter, r *http.Request) {
if !h.PermitWrite {
http.Error(w, "network-lock modify access denied", http.StatusForbidden)
http.Error(w, "tailnet-lock modify access denied", http.StatusForbidden)
return
}
if r.Method != httpm.POST {
@@ -227,8 +227,8 @@ func (h *Handler) serveTKADisable(w http.ResponseWriter, r *http.Request) {
return
}
if err := h.b.NetworkLockDisable(secret); err != nil {
http.Error(w, "network-lock disable failed: "+err.Error(), http.StatusBadRequest)
if err := h.b.TailnetLockDisable(secret); err != nil {
http.Error(w, "tailnet-lock disable failed: "+err.Error(), http.StatusBadRequest)
return
}
w.WriteHeader(http.StatusOK)
@@ -236,7 +236,7 @@ func (h *Handler) serveTKADisable(w http.ResponseWriter, r *http.Request) {
func (h *Handler) serveTKALocalDisable(w http.ResponseWriter, r *http.Request) {
if !h.PermitWrite {
http.Error(w, "network-lock modify access denied", http.StatusForbidden)
http.Error(w, "tailnet-lock modify access denied", http.StatusForbidden)
return
}
if r.Method != httpm.POST {
@@ -251,8 +251,8 @@ func (h *Handler) serveTKALocalDisable(w http.ResponseWriter, r *http.Request) {
return
}
if err := h.b.NetworkLockForceLocalDisable(); err != nil {
http.Error(w, "network-lock local disable failed: "+err.Error(), http.StatusBadRequest)
if err := h.b.TailnetLockForceLocalDisable(); err != nil {
http.Error(w, "tailnet-lock local disable failed: "+err.Error(), http.StatusBadRequest)
return
}
w.WriteHeader(http.StatusOK)
@@ -274,7 +274,7 @@ func (h *Handler) serveTKALog(w http.ResponseWriter, r *http.Request) {
limit = int(lm)
}
updates, err := h.b.NetworkLockLog(limit)
updates, err := h.b.TailnetLockLog(limit)
if err != nil {
http.Error(w, "reading log failed: "+err.Error(), http.StatusInternalServerError)
return
@@ -300,7 +300,7 @@ func (h *Handler) serveTKAAffectedSigs(w http.ResponseWriter, r *http.Request) {
return
}
sigs, err := h.b.NetworkLockAffectedSigs(keyID)
sigs, err := h.b.TailnetLockAffectedSigs(keyID)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
@@ -343,7 +343,7 @@ func (h *Handler) serveTKAGenerateRecoveryAUM(w http.ResponseWriter, r *http.Req
}
}
res, err := h.b.NetworkLockGenerateRecoveryAUM(req.Keys, forkFrom)
res, err := h.b.TailnetLockGenerateRecoveryAUM(req.Keys, forkFrom)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
@@ -374,7 +374,7 @@ func (h *Handler) serveTKACosignRecoveryAUM(w http.ResponseWriter, r *http.Reque
return
}
res, err := h.b.NetworkLockCosignRecoveryAUM(&aum)
res, err := h.b.TailnetLockCosignRecoveryAUM(&aum)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
@@ -405,7 +405,7 @@ func (h *Handler) serveTKASubmitRecoveryAUM(w http.ResponseWriter, r *http.Reque
return
}
if err := h.b.NetworkLockSubmitRecoveryAUM(&aum); err != nil {
if err := h.b.TailnetLockSubmitRecoveryAUM(&aum); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}