From 103c00a175ae0632dd3bc3f27d8913b2c5deb511 Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Mon, 13 Nov 2023 09:53:40 -0800 Subject: [PATCH] ipn/ipnlocal: clean up c2n handling's big switch, add a mux table Updates #cleanup Change-Id: I29ec03db91e7831a3a66a63dcf6ff8e3f72ab045 Signed-off-by: Brad Fitzpatrick --- ipn/ipnlocal/c2n.go | 275 +++++++++++++++++++++++++------------------- 1 file changed, 159 insertions(+), 116 deletions(-) diff --git a/ipn/ipnlocal/c2n.go b/ipn/ipnlocal/c2n.go index 04c33c705..c48c1edce 100644 --- a/ipn/ipnlocal/c2n.go +++ b/ipn/ipnlocal/c2n.go @@ -28,132 +28,179 @@ import ( "tailscale.com/tailcfg" "tailscale.com/util/clientmetric" "tailscale.com/util/goroutines" - "tailscale.com/util/httpm" + "tailscale.com/util/set" "tailscale.com/util/syspolicy" "tailscale.com/version" "tailscale.com/version/distro" ) -var c2nLogHeap func(http.ResponseWriter, *http.Request) // non-nil on most platforms (c2n_pprof.go) +// c2nHandlers maps an HTTP method and URI path (without query parameters) to +// its handler. The exact method+path match is preferred, but if no entry +// exists for that, a map entry with an empty method is used as a fallback. +var c2nHandlers = map[methodAndPath]c2nHandler{ + // Debug. + req("/echo"): handleC2NEcho, + req("/debug/goroutines"): handleC2NDebugGoroutines, + req("/debug/prefs"): handleC2NDebugPrefs, + req("/debug/metrics"): handleC2NDebugMetrics, + req("/debug/component-logging"): handleC2NDebugComponentLogging, + req("/debug/logheap"): handleC2NDebugLogHeap, + req("POST /logtail/flush"): handleC2NLogtailFlush, + req("POST /sockstats"): handleC2NSockStats, + + // SSH + req("/ssh/usernames"): handleC2NSSHUsernames, + + // Auto-updates. + req("GET /update"): handleC2NUpdateGet, + req("POST /update"): handleC2NUpdatePost, + + // Wake-on-LAN. + req("POST /wol"): handleC2NWoL, + + // Device posture. + req("GET /posture/identity"): handleC2NPostureIdentityGet, + + // App Connectors. + req("GET /appconnector/routes"): handleC2NAppConnectorDomainRoutesGet, +} + +type c2nHandler func(*LocalBackend, http.ResponseWriter, *http.Request) + +type methodAndPath struct { + method string // empty string means fallback + path string // Request.URL.Path (without query string) +} + +func req(s string) methodAndPath { + if m, p, ok := strings.Cut(s, " "); ok { + return methodAndPath{m, p} + } + return methodAndPath{"", s} +} + +// c2nHandlerPaths is all the set of paths from c2nHandlers, without their HTTP methods. +// It's used to detect requests with a non-matching method. +var c2nHandlerPaths = set.Set[string]{} + +func init() { + for k := range c2nHandlers { + c2nHandlerPaths.Add(k.path) + } +} + +func (b *LocalBackend) handleC2N(w http.ResponseWriter, r *http.Request) { + // First try to match by both method and path, + if h, ok := c2nHandlers[methodAndPath{r.Method, r.URL.Path}]; ok { + h(b, w, r) + return + } + // Then try to match by just path. + if h, ok := c2nHandlers[methodAndPath{path: r.URL.Path}]; ok { + h(b, w, r) + return + } + if c2nHandlerPaths.Contains(r.URL.Path) { + http.Error(w, "bad method", http.StatusMethodNotAllowed) + } else { + http.Error(w, "unknown c2n path", http.StatusBadRequest) + } +} func writeJSON(w http.ResponseWriter, v any) { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(v) } -func (b *LocalBackend) handleC2N(w http.ResponseWriter, r *http.Request) { - switch r.URL.Path { - case "/echo": - // Test handler. - body, _ := io.ReadAll(r.Body) - w.Write(body) - case "/update": - switch r.Method { - case httpm.GET: - b.handleC2NUpdateGet(w, r) - case httpm.POST: - b.handleC2NUpdatePost(w, r) - default: - http.Error(w, "bad method", http.StatusMethodNotAllowed) - return - } - case "/wol": - b.handleC2NWoL(w, r) - return - case "/logtail/flush": - if r.Method != "POST" { - http.Error(w, "bad method", http.StatusMethodNotAllowed) - return - } - if b.TryFlushLogs() { - w.WriteHeader(http.StatusNoContent) - } else { - http.Error(w, "no log flusher wired up", http.StatusInternalServerError) - } - case "/posture/identity": - switch r.Method { - case httpm.GET: - b.handleC2NPostureIdentityGet(w, r) - default: - http.Error(w, "bad method", http.StatusMethodNotAllowed) - return - } - case "/debug/goroutines": - w.Header().Set("Content-Type", "text/plain") - w.Write(goroutines.ScrubbedGoroutineDump(true)) - case "/debug/prefs": - writeJSON(w, b.Prefs()) - case "/debug/metrics": - w.Header().Set("Content-Type", "text/plain") - clientmetric.WritePrometheusExpositionFormat(w) - case "/debug/component-logging": - component := r.FormValue("component") - secs, _ := strconv.Atoi(r.FormValue("secs")) - if secs == 0 { - secs -= 1 - } - until := b.clock.Now().Add(time.Duration(secs) * time.Second) - err := b.SetComponentDebugLogging(component, until) - var res struct { - Error string `json:",omitempty"` - } - if err != nil { - res.Error = err.Error() - } - writeJSON(w, res) - case "/debug/logheap": - if c2nLogHeap != nil { - c2nLogHeap(w, r) - } else { - http.Error(w, "not implemented", http.StatusNotImplemented) - return - } - case "/ssh/usernames": - var req tailcfg.C2NSSHUsernamesRequest - if r.Method == "POST" { - if err := json.NewDecoder(r.Body).Decode(&req); err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - return - } - } - res, err := b.getSSHUsernames(&req) - if err != nil { - http.Error(w, err.Error(), 500) - return - } - writeJSON(w, res) - case "/appconnector/routes": - switch r.Method { - case httpm.GET: - b.handleC2NAppConnectorDomainRoutesGet(w, r) - return - default: - http.Error(w, "bad method", http.StatusMethodNotAllowed) - return - } - case "/sockstats": - if r.Method != "POST" { - http.Error(w, "bad method", http.StatusMethodNotAllowed) - return - } - w.Header().Set("Content-Type", "text/plain") - if b.sockstatLogger == nil { - http.Error(w, "no sockstatLogger", http.StatusInternalServerError) - return - } - b.sockstatLogger.Flush() - fmt.Fprintf(w, "logid: %s\n", b.sockstatLogger.LogID()) - fmt.Fprintf(w, "debug info: %v\n", sockstats.DebugInfo()) - default: - http.Error(w, "unknown c2n path", http.StatusBadRequest) +func handleC2NEcho(b *LocalBackend, w http.ResponseWriter, r *http.Request) { + // Test handler. + body, _ := io.ReadAll(r.Body) + w.Write(body) +} + +func handleC2NLogtailFlush(b *LocalBackend, w http.ResponseWriter, r *http.Request) { + if b.TryFlushLogs() { + w.WriteHeader(http.StatusNoContent) + } else { + http.Error(w, "no log flusher wired up", http.StatusInternalServerError) } } +func handleC2NDebugGoroutines(_ *LocalBackend, w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/plain") + w.Write(goroutines.ScrubbedGoroutineDump(true)) +} + +func handleC2NDebugPrefs(b *LocalBackend, w http.ResponseWriter, r *http.Request) { + writeJSON(w, b.Prefs()) +} + +func handleC2NDebugMetrics(_ *LocalBackend, w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/plain") + clientmetric.WritePrometheusExpositionFormat(w) +} + +func handleC2NDebugComponentLogging(b *LocalBackend, w http.ResponseWriter, r *http.Request) { + component := r.FormValue("component") + secs, _ := strconv.Atoi(r.FormValue("secs")) + if secs == 0 { + secs -= 1 + } + until := b.clock.Now().Add(time.Duration(secs) * time.Second) + err := b.SetComponentDebugLogging(component, until) + var res struct { + Error string `json:",omitempty"` + } + if err != nil { + res.Error = err.Error() + } + writeJSON(w, res) +} + +var c2nLogHeap func(http.ResponseWriter, *http.Request) // non-nil on most platforms (c2n_pprof.go) + +func handleC2NDebugLogHeap(b *LocalBackend, w http.ResponseWriter, r *http.Request) { + if c2nLogHeap == nil { + // Not implemented on platforms trying to optimize for binary size or + // reduced memory usage. + http.Error(w, "not implemented", http.StatusNotImplemented) + return + } + c2nLogHeap(w, r) +} + +func handleC2NSSHUsernames(b *LocalBackend, w http.ResponseWriter, r *http.Request) { + var req tailcfg.C2NSSHUsernamesRequest + if r.Method == "POST" { + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + } + res, err := b.getSSHUsernames(&req) + if err != nil { + http.Error(w, err.Error(), 500) + return + } + writeJSON(w, res) +} + +func handleC2NSockStats(b *LocalBackend, w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/plain") + if b.sockstatLogger == nil { + http.Error(w, "no sockstatLogger", http.StatusInternalServerError) + return + } + b.sockstatLogger.Flush() + fmt.Fprintf(w, "logid: %s\n", b.sockstatLogger.LogID()) + fmt.Fprintf(w, "debug info: %v\n", sockstats.DebugInfo()) +} + // handleC2NAppConnectorDomainRoutesGet handles returning the domains // that the app connector is responsible for, as well as the resolved // IP addresses for each domain. If the node is not configured as // an app connector, an empty map is returned. -func (b *LocalBackend) handleC2NAppConnectorDomainRoutesGet(w http.ResponseWriter, r *http.Request) { +func handleC2NAppConnectorDomainRoutesGet(b *LocalBackend, w http.ResponseWriter, r *http.Request) { b.logf("c2n: GET /appconnector/routes received") var res tailcfg.C2NAppConnectorDomainRoutesResponse @@ -169,7 +216,7 @@ func (b *LocalBackend) handleC2NAppConnectorDomainRoutesGet(w http.ResponseWrite json.NewEncoder(w).Encode(res) } -func (b *LocalBackend) handleC2NUpdateGet(w http.ResponseWriter, r *http.Request) { +func handleC2NUpdateGet(b *LocalBackend, w http.ResponseWriter, r *http.Request) { b.logf("c2n: GET /update received") res := b.newC2NUpdateResponse() @@ -179,7 +226,7 @@ func (b *LocalBackend) handleC2NUpdateGet(w http.ResponseWriter, r *http.Request json.NewEncoder(w).Encode(res) } -func (b *LocalBackend) handleC2NUpdatePost(w http.ResponseWriter, r *http.Request) { +func handleC2NUpdatePost(b *LocalBackend, w http.ResponseWriter, r *http.Request) { b.logf("c2n: POST /update received") res := b.newC2NUpdateResponse() defer func() { @@ -255,7 +302,7 @@ func (b *LocalBackend) handleC2NUpdatePost(w http.ResponseWriter, r *http.Reques }() } -func (b *LocalBackend) handleC2NPostureIdentityGet(w http.ResponseWriter, r *http.Request) { +func handleC2NPostureIdentityGet(b *LocalBackend, w http.ResponseWriter, r *http.Request) { b.logf("c2n: GET /posture/identity received") res := tailcfg.C2NPostureIdentityResponse{} @@ -370,11 +417,7 @@ func regularFileExists(path string) bool { return err == nil && fi.Mode().IsRegular() } -func (b *LocalBackend) handleC2NWoL(w http.ResponseWriter, r *http.Request) { - if r.Method != "POST" { - http.Error(w, "bad method", http.StatusMethodNotAllowed) - return - } +func handleC2NWoL(b *LocalBackend, w http.ResponseWriter, r *http.Request) { r.ParseForm() var macs []net.HardwareAddr for _, macStr := range r.Form["mac"] {