summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorBrad Fitzpatrick <bradfitz@tailscale.com>2023-11-13 09:53:40 -0800
committerBrad Fitzpatrick <brad@danga.com>2023-11-13 11:57:56 -0800
commit103c00a175ae0632dd3bc3f27d8913b2c5deb511 (patch)
tree2e6b22ea27fba2acda56718b9b126c870e552115
parentce46d92ed2b905a829a6a6281b90a972f1cfdfcc (diff)
downloadtailscale-103c00a175ae0632dd3bc3f27d8913b2c5deb511.tar.xz
tailscale-103c00a175ae0632dd3bc3f27d8913b2c5deb511.zip
ipn/ipnlocal: clean up c2n handling's big switch, add a mux table
Updates #cleanup Change-Id: I29ec03db91e7831a3a66a63dcf6ff8e3f72ab045 Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
-rw-r--r--ipn/ipnlocal/c2n.go269
1 files changed, 156 insertions, 113 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)
+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
- 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)
+ }
+ 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
}
- 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)
}
+ 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"] {