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>
pull/10004/merge
Brad Fitzpatrick 1 year ago committed by Brad Fitzpatrick
parent ce46d92ed2
commit 103c00a175

@ -28,65 +28,119 @@ import (
"tailscale.com/tailcfg" "tailscale.com/tailcfg"
"tailscale.com/util/clientmetric" "tailscale.com/util/clientmetric"
"tailscale.com/util/goroutines" "tailscale.com/util/goroutines"
"tailscale.com/util/httpm" "tailscale.com/util/set"
"tailscale.com/util/syspolicy" "tailscale.com/util/syspolicy"
"tailscale.com/version" "tailscale.com/version"
"tailscale.com/version/distro" "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,
}
func writeJSON(w http.ResponseWriter, v any) { type c2nHandler func(*LocalBackend, http.ResponseWriter, *http.Request)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(v) 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) { func (b *LocalBackend) handleC2N(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path { // First try to match by both method and path,
case "/echo": if h, ok := c2nHandlers[methodAndPath{r.Method, r.URL.Path}]; ok {
// Test handler. h(b, w, r)
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 return
} }
case "/wol": // Then try to match by just path.
b.handleC2NWoL(w, r) if h, ok := c2nHandlers[methodAndPath{path: r.URL.Path}]; ok {
h(b, w, r)
return return
case "/logtail/flush": }
if r.Method != "POST" { if c2nHandlerPaths.Contains(r.URL.Path) {
http.Error(w, "bad method", http.StatusMethodNotAllowed) http.Error(w, "bad method", http.StatusMethodNotAllowed)
return } 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 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() { if b.TryFlushLogs() {
w.WriteHeader(http.StatusNoContent) w.WriteHeader(http.StatusNoContent)
} else { } else {
http.Error(w, "no log flusher wired up", http.StatusInternalServerError) http.Error(w, "no log flusher wired up", http.StatusInternalServerError)
} }
case "/posture/identity": }
switch r.Method {
case httpm.GET: func handleC2NDebugGoroutines(_ *LocalBackend, w http.ResponseWriter, r *http.Request) {
b.handleC2NPostureIdentityGet(w, r)
default:
http.Error(w, "bad method", http.StatusMethodNotAllowed)
return
}
case "/debug/goroutines":
w.Header().Set("Content-Type", "text/plain") w.Header().Set("Content-Type", "text/plain")
w.Write(goroutines.ScrubbedGoroutineDump(true)) w.Write(goroutines.ScrubbedGoroutineDump(true))
case "/debug/prefs": }
func handleC2NDebugPrefs(b *LocalBackend, w http.ResponseWriter, r *http.Request) {
writeJSON(w, b.Prefs()) writeJSON(w, b.Prefs())
case "/debug/metrics": }
func handleC2NDebugMetrics(_ *LocalBackend, w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain") w.Header().Set("Content-Type", "text/plain")
clientmetric.WritePrometheusExpositionFormat(w) clientmetric.WritePrometheusExpositionFormat(w)
case "/debug/component-logging": }
func handleC2NDebugComponentLogging(b *LocalBackend, w http.ResponseWriter, r *http.Request) {
component := r.FormValue("component") component := r.FormValue("component")
secs, _ := strconv.Atoi(r.FormValue("secs")) secs, _ := strconv.Atoi(r.FormValue("secs"))
if secs == 0 { if secs == 0 {
@ -101,14 +155,21 @@ func (b *LocalBackend) handleC2N(w http.ResponseWriter, r *http.Request) {
res.Error = err.Error() res.Error = err.Error()
} }
writeJSON(w, res) writeJSON(w, res)
case "/debug/logheap": }
if c2nLogHeap != nil {
c2nLogHeap(w, r) var c2nLogHeap func(http.ResponseWriter, *http.Request) // non-nil on most platforms (c2n_pprof.go)
} else {
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) http.Error(w, "not implemented", http.StatusNotImplemented)
return return
} }
case "/ssh/usernames": c2nLogHeap(w, r)
}
func handleC2NSSHUsernames(b *LocalBackend, w http.ResponseWriter, r *http.Request) {
var req tailcfg.C2NSSHUsernamesRequest var req tailcfg.C2NSSHUsernamesRequest
if r.Method == "POST" { if r.Method == "POST" {
if err := json.NewDecoder(r.Body).Decode(&req); err != nil { if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
@ -122,20 +183,9 @@ func (b *LocalBackend) handleC2N(w http.ResponseWriter, r *http.Request) {
return return
} }
writeJSON(w, res) writeJSON(w, res)
case "/appconnector/routes": }
switch r.Method {
case httpm.GET: func handleC2NSockStats(b *LocalBackend, w http.ResponseWriter, r *http.Request) {
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") w.Header().Set("Content-Type", "text/plain")
if b.sockstatLogger == nil { if b.sockstatLogger == nil {
http.Error(w, "no sockstatLogger", http.StatusInternalServerError) http.Error(w, "no sockstatLogger", http.StatusInternalServerError)
@ -144,16 +194,13 @@ func (b *LocalBackend) handleC2N(w http.ResponseWriter, r *http.Request) {
b.sockstatLogger.Flush() b.sockstatLogger.Flush()
fmt.Fprintf(w, "logid: %s\n", b.sockstatLogger.LogID()) fmt.Fprintf(w, "logid: %s\n", b.sockstatLogger.LogID())
fmt.Fprintf(w, "debug info: %v\n", sockstats.DebugInfo()) fmt.Fprintf(w, "debug info: %v\n", sockstats.DebugInfo())
default:
http.Error(w, "unknown c2n path", http.StatusBadRequest)
}
} }
// handleC2NAppConnectorDomainRoutesGet handles returning the domains // handleC2NAppConnectorDomainRoutesGet handles returning the domains
// that the app connector is responsible for, as well as the resolved // that the app connector is responsible for, as well as the resolved
// IP addresses for each domain. If the node is not configured as // IP addresses for each domain. If the node is not configured as
// an app connector, an empty map is returned. // 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") b.logf("c2n: GET /appconnector/routes received")
var res tailcfg.C2NAppConnectorDomainRoutesResponse var res tailcfg.C2NAppConnectorDomainRoutesResponse
@ -169,7 +216,7 @@ func (b *LocalBackend) handleC2NAppConnectorDomainRoutesGet(w http.ResponseWrite
json.NewEncoder(w).Encode(res) 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") b.logf("c2n: GET /update received")
res := b.newC2NUpdateResponse() res := b.newC2NUpdateResponse()
@ -179,7 +226,7 @@ func (b *LocalBackend) handleC2NUpdateGet(w http.ResponseWriter, r *http.Request
json.NewEncoder(w).Encode(res) 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") b.logf("c2n: POST /update received")
res := b.newC2NUpdateResponse() res := b.newC2NUpdateResponse()
defer func() { 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") b.logf("c2n: GET /posture/identity received")
res := tailcfg.C2NPostureIdentityResponse{} res := tailcfg.C2NPostureIdentityResponse{}
@ -370,11 +417,7 @@ func regularFileExists(path string) bool {
return err == nil && fi.Mode().IsRegular() return err == nil && fi.Mode().IsRegular()
} }
func (b *LocalBackend) handleC2NWoL(w http.ResponseWriter, r *http.Request) { func handleC2NWoL(b *LocalBackend, w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
http.Error(w, "bad method", http.StatusMethodNotAllowed)
return
}
r.ParseForm() r.ParseForm()
var macs []net.HardwareAddr var macs []net.HardwareAddr
for _, macStr := range r.Form["mac"] { for _, macStr := range r.Form["mac"] {

Loading…
Cancel
Save