Update
This commit is contained in:
1 parent
3530afb854
commit
6903fcff27
2146 files changed
+112256
-190703
No files matched your search
+258
-64
@@ -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)
|
||||
|
||||
Reference in new issue
Block a user