Something went wrong. Try again.
forked from tailscale/tailscale
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590// Copyright (c) Tailscale Inc & contributors// SPDX-License-Identifier: BSD-3-Clause
//go:build !ts_omit_debug
package localapi
import ( "cmp" "context" "encoding/json" "errors" "fmt" "io" "net" "net/http" "net/netip" "reflect" "slices" "strconv" "sync" "time"
"tailscale.com/client/tailscale/apitype" "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")
func init() { Register("component-debug-logging", (*Handler).serveComponentDebugLogging) Register("debug", (*Handler).serveDebug) Register("debug-rotate-disco-key", (*Handler).serveDebugRotateDiscoKey) 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) Register("debug-packet-filter-matches", (*Handler).serveDebugPacketFilterMatches) Register("debug-packet-filter-rules", (*Handler).serveDebugPacketFilterRules) Register("debug-peer-endpoint-changes", (*Handler).serveDebugPeerEndpointChanges) Register("debug-optional-features", (*Handler).serveDebugOptionalFeatures)}
func (h *Handler) serveDebugPeerEndpointChanges(w http.ResponseWriter, r *http.Request) { if !h.PermitRead { http.Error(w, "status access denied", http.StatusForbidden) return }
ipStr := r.FormValue("ip") if ipStr == "" { http.Error(w, "missing 'ip' parameter", http.StatusBadRequest) return } ip, err := netip.ParseAddr(ipStr) if err != nil { http.Error(w, "invalid IP", http.StatusBadRequest) return } w.Header().Set("Content-Type", "application/json") chs, err := h.b.GetPeerEndpointChanges(r.Context(), ip) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return }
e := json.NewEncoder(w) e.SetIndent("", "\t") e.Encode(chs)}
func (h *Handler) serveComponentDebugLogging(w http.ResponseWriter, r *http.Request) { if !h.PermitWrite { http.Error(w, "debug access denied", http.StatusForbidden) return } component := r.FormValue("component") secs, _ := strconv.Atoi(r.FormValue("secs")) err := h.b.SetComponentDebugLogging(component, h.clock.Now().Add(time.Duration(secs)*time.Second)) var res struct { Error string } if err != nil { res.Error = err.Error() } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(res)}
func (h *Handler) serveDebugDialTypes(w http.ResponseWriter, r *http.Request) { if !h.PermitWrite { http.Error(w, "debug-dial-types access denied", http.StatusForbidden) return } if r.Method != httpm.POST { http.Error(w, "only POST allowed", http.StatusMethodNotAllowed) return }
ip := r.FormValue("ip") port := r.FormValue("port") network := r.FormValue("network")
addr := ip + ":" + port if _, err := netip.ParseAddrPort(addr); err != nil { w.WriteHeader(http.StatusBadRequest) fmt.Fprintf(w, "invalid address %q: %v", addr, err) return }
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second) defer cancel()
var bareDialer net.Dialer
dialer := h.b.Dialer()
var peerDialer net.Dialer peerDialer.Control = dialer.PeerDialControlFunc()
// Kick off a dial with each available dialer in parallel. dialers := []struct { name string dial func(context.Context, string, string) (net.Conn, error) }{ {"SystemDial", dialer.SystemDial}, {"UserDial", dialer.UserDial}, {"PeerDial", peerDialer.DialContext}, {"BareDial", bareDialer.DialContext}, } type result struct { name string conn net.Conn err error } results := make(chan result, len(dialers))
var wg sync.WaitGroup for _, dialer := range dialers {
wg.Go(func() { conn, err := dialer.dial(ctx, network, addr) results <- result{dialer.name, conn, err} }) }
wg.Wait() for range len(dialers) { res := <-results fmt.Fprintf(w, "[%s] connected=%v err=%v\n", res.name, res.conn != nil, res.err) if res.conn != nil { res.conn.Close() } }}
func (h *Handler) serveDebug(w http.ResponseWriter, r *http.Request) { if !buildfeatures.HasDebug { http.Error(w, "debug not supported in this build", http.StatusNotImplemented) return } if !h.PermitWrite { http.Error(w, "debug access denied", http.StatusForbidden) return } if r.Method != httpm.POST { http.Error(w, "POST required", http.StatusMethodNotAllowed) return } // The action is normally in a POST form parameter, but // some actions (like "notify") want a full JSON body, so // permit some to have their action in a header. var action string switch v := r.Header.Get("Debug-Action"); v { case "notify": action = v default: action = r.FormValue("action") } var err error switch action { case "derp-set-homeless": h.b.MagicConn().SetHomeless(true) case "derp-unset-homeless": h.b.MagicConn().SetHomeless(false) case "rebind": err = h.b.DebugRebind() case "restun": err = h.b.DebugReSTUN() case "notify": var n ipn.Notify err = json.NewDecoder(r.Body).Decode(&n) if err != nil { break } h.b.DebugNotify(n) case "break-tcp-conns": err = h.b.DebugBreakTCPConns() case "break-derp-conns": err = h.b.DebugBreakDERPConns() case "force-netmap-update": h.b.DebugForceNetmapUpdate() case "control-knobs": k := h.b.ControlKnobs() w.Header().Set("Content-Type", "application/json") err = json.NewEncoder(w).Encode(k.AsDebugJSON()) if err == nil { return } case "pick-new-derp": err = h.b.DebugPickNewDERP() case "force-prefer-derp": var n tailcfg.DERPRegionID err = json.NewDecoder(r.Body).Decode(&n) if err != nil { break } h.b.DebugForcePreferDERP(n) case "peer-relay-servers": servers := h.b.DebugPeerRelayServers().Slice() slices.SortFunc(servers, func(a, b netip.Addr) int { return a.Compare(b) }) err = json.NewEncoder(w).Encode(servers) 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: err = fmt.Errorf("unknown action %q", action) } if err != nil { http.Error(w, err.Error(), http.StatusBadRequest) return } w.Header().Set("Content-Type", "text/plain") io.WriteString(w, "done\n")}
func (h *Handler) serveDevSetStateStore(w http.ResponseWriter, r *http.Request) { if !h.PermitWrite { 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 } if err := h.b.SetDevStateStore(r.FormValue("key"), r.FormValue("value")); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } w.Header().Set("Content-Type", "text/plain") io.WriteString(w, "done\n")}
func (h *Handler) serveDebugPacketFilterRules(w http.ResponseWriter, r *http.Request) { if !h.PermitWrite { http.Error(w, "debug access denied", http.StatusForbidden) return } nm := h.b.NetMapNoPeers() if nm == nil { http.Error(w, "no netmap", http.StatusNotFound) return } w.Header().Set("Content-Type", "application/json")
enc := json.NewEncoder(w) enc.SetIndent("", "\t") enc.Encode(nm.PacketFilterRules)}
func (h *Handler) serveDebugPacketFilterMatches(w http.ResponseWriter, r *http.Request) { if !h.PermitWrite { http.Error(w, "debug access denied", http.StatusForbidden) return } nm := h.b.NetMapNoPeers() if nm == nil { http.Error(w, "no netmap", http.StatusNotFound) return } w.Header().Set("Content-Type", "application/json")
enc := json.NewEncoder(w) enc.SetIndent("", "\t") enc.Encode(nm.PacketFilter)}
// debugEventError provides the JSON encoding of internal errors from event processing.type debugEventError struct { Error string}
// serveDebugBusEvents taps into the tailscaled/utils/eventbus and streams// events to the client.func (h *Handler) serveDebugBusEvents(w http.ResponseWriter, r *http.Request) { // Require write access (~root) as the logs could contain something // sensitive. if !h.PermitWrite { http.Error(w, "event bus access denied", http.StatusForbidden) return } 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.StatusNoContent) return }
f, ok := w.(http.Flusher) if !ok { http.Error(w, "streaming unsupported", http.StatusInternalServerError) return }
io.WriteString(w, `{"Event":"[event listener connected]\n"}`+"\n") f.Flush()
mon := bus.Debugger().WatchBus() defer mon.Close()
i := 0 for { select { case <-r.Context().Done(): fmt.Fprintf(w, `{"Event":"[event listener closed]\n"}`) return case <-mon.Done(): return case event := <-mon.Events(): data := eventbus.DebugEvent{ Count: i, Type: reflect.TypeOf(event.Event).String(), Event: event.Event, From: event.From.Name(), } for _, client := range event.To { data.To = append(data.To, client.Name()) }
if msg, err := json.Marshal(data); err != nil { data.Event = debugEventError{Error: fmt.Sprintf( "failed to marshal JSON for %T", event.Event, )} if errMsg, err := json.Marshal(data); err != nil { fmt.Fprintf(w, `{"Count": %d, "Event":"[ERROR] failed to marshal JSON for %T\n"}`, i, event.Event) } else { w.Write(errMsg) } } else { w.Write(msg) } f.Flush() i++ } }}
// serveEventBusGraph taps into the event bus and dumps out the active graph of// publishers and subscribers. It does not represent anything about the messages// exchanged.func (h *Handler) serveEventBusGraph(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() clients := debugger.Clients()
graph := map[string]eventbus.DebugTopic{}
for _, client := range clients { for _, pub := range debugger.PublishTypes(client) { topic, ok := graph[pub.Name()] if !ok { topic = eventbus.DebugTopic{Name: pub.Name()} } topic.Publisher = client.Name() graph[pub.Name()] = topic } for _, sub := range debugger.SubscribeTypes(client) { topic, ok := graph[sub.Name()] if !ok { topic = eventbus.DebugTopic{Name: sub.Name()} } topic.Subscribers = append(topic.Subscribers, client.Name()) graph[sub.Name()] = topic } }
// The top level map is not really needed for the client, convert to a list. topics := eventbus.DebugTopics{} for _, v := range graph { topics.Topics = append(topics.Topics, v) }
w.Header().Set("Content-Type", "application/json") 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.PermitWrite { http.Error(w, "debug-log access denied", http.StatusForbidden) return } if r.Method != httpm.POST { http.Error(w, "only POST allowed", http.StatusMethodNotAllowed) return } defer h.b.TryFlushLogs() // kick off upload after we're done logging
type logRequestJSON struct { Lines []string Prefix string }
var logRequest logRequestJSON if err := json.NewDecoder(r.Body).Decode(&logRequest); err != nil { http.Error(w, "invalid JSON body", http.StatusBadRequest) return }
prefix := logRequest.Prefix if prefix == "" { prefix = "debug-log" } logf := logger.WithPrefix(h.logf, prefix+": ")
// We can write logs too fast for logtail to handle, even when // opting-out of rate limits. Limit ourselves to at most one message // per 20ms and a burst of 60 log lines, which should be fast enough to // not block for too long but slow enough that we can upload all lines. logf = logger.SlowLoggerWithClock(r.Context(), logf, 20*time.Millisecond, 60, h.clock.Now)
for _, line := range logRequest.Lines { logf("%s", line) }
w.WriteHeader(http.StatusNoContent)}
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)}
func (h *Handler) serveDebugRotateDiscoKey(w http.ResponseWriter, r *http.Request) { if !h.PermitWrite { http.Error(w, "debug access denied", http.StatusForbidden) return } if r.Method != httpm.POST { http.Error(w, "POST required", http.StatusMethodNotAllowed) return } if err := h.b.DebugRotateDiscoKey(); err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } w.Header().Set("Content-Type", "text/plain") io.WriteString(w, "done\n")}