diff --git a/cmd/derper/derper.go b/cmd/derper/derper.go
index c726389c0..2ea77cbc3 100644
--- a/cmd/derper/derper.go
+++ b/cmd/derper/derper.go
@@ -12,8 +12,6 @@
"errors"
"expvar"
"flag"
- "fmt"
- "html"
"io"
"io/ioutil"
"log"
@@ -35,7 +33,6 @@
"tailscale.com/tsweb"
"tailscale.com/types/key"
"tailscale.com/types/wgkey"
- "tailscale.com/version"
)
var (
@@ -143,8 +140,7 @@ func main() {
}
expvar.Publish("derp", s.ExpVar())
- // Create our own mux so we don't expose /debug/ stuff to the world.
- mux := tsweb.NewMux(debugHandler(s))
+ mux := http.NewServeMux()
mux.Handle("/derp", derphttp.Handler(s))
go refreshBootstrapDNSLoop()
mux.HandleFunc("/bootstrap-dns", handleBootstrapDNS)
@@ -164,6 +160,17 @@ func main() {
io.WriteString(w, "
Debug info at /debug/.
\n")
}
}))
+ debug := tsweb.Debugger(mux)
+ debug.KV("TLS hostname", *hostname)
+ debug.KV("Mesh key", s.HasMeshKey())
+ debug.Handle("check", "Consistency check", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ err := s.ConsistencyCheck()
+ if err != nil {
+ http.Error(w, err.Error(), 500)
+ } else {
+ io.WriteString(w, "derp.Server ConsistencyCheck okay")
+ }
+ }))
if *runSTUN {
go serveSTUN()
@@ -217,39 +224,6 @@ func main() {
}
}
-func debugHandler(s *derp.Server) http.Handler {
- return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
- if r.RequestURI == "/debug/check" {
- err := s.ConsistencyCheck()
- if err != nil {
- http.Error(w, err.Error(), 500)
- } else {
- io.WriteString(w, "derp.Server ConsistencyCheck okay")
- }
- return
- }
- f := func(format string, args ...interface{}) { fmt.Fprintf(w, format, args...) }
- f(`
-DERP debug
-
-`)
- f("- Hostname: %v
\n", html.EscapeString(*hostname))
- f("- Uptime: %v
\n", tsweb.Uptime())
- f("- Mesh Key: %v
\n", s.HasMeshKey())
- f("- Version: %v
\n", html.EscapeString(version.Long))
-
- f(`- /debug/vars (Go)
- - /debug/varz (Prometheus)
- - /debug/pprof/
- - /debug/pprof/goroutine (collapsed)
- - /debug/pprof/goroutine (full)
- - /debug/check internal consistency check
-
-
-`)
- })
-}
-
func serveSTUN() {
pc, err := net.ListenPacket("udp", ":3478")
if err != nil {
diff --git a/cmd/microproxy/microproxy.go b/cmd/microproxy/microproxy.go
index 76ee41b9c..e537ab140 100644
--- a/cmd/microproxy/microproxy.go
+++ b/cmd/microproxy/microproxy.go
@@ -55,7 +55,8 @@ func main() {
log.Fatalf("Couldn't parse URL %q: %v", *goVarsURL, err)
}
- mux := tsweb.NewMux(http.HandlerFunc(debugHandler))
+ mux := http.NewServeMux()
+ tsweb.Debugger(mux) // registers /debug/*
mux.Handle("/metrics", tsweb.Protected(proxy))
mux.Handle("/varz", tsweb.Protected(tsweb.StdHandler(&goVarsHandler{*goVarsURL}, tsweb.HandlerOptions{
Quiet200s: true,
@@ -170,23 +171,3 @@ func (c *certHolder) loadLocked() error {
c.loaded = time.Now()
return nil
}
-
-// debugHandler serves a page with links to tsweb-managed debug URLs
-// at /debug/.
-func debugHandler(w http.ResponseWriter, r *http.Request) {
- f := func(format string, args ...interface{}) { fmt.Fprintf(w, format, args...) }
- f(`
-microproxy debug
-
-`)
- f("- Hostname: %v
\n", *hostname)
- f("- Uptime: %v
\n", tsweb.Uptime())
- f(`- /debug/vars (Go)
- - /debug/varz (Prometheus)
- - /debug/pprof/
- - /debug/pprof/goroutine (collapsed)
- - /debug/pprof/goroutine (full)
-
-
-`)
-}
diff --git a/tsweb/debug.go b/tsweb/debug.go
new file mode 100644
index 000000000..8125c5e8b
--- /dev/null
+++ b/tsweb/debug.go
@@ -0,0 +1,136 @@
+// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package tsweb
+
+import (
+ "expvar"
+ "fmt"
+ "html"
+ "io"
+ "net/http"
+ "net/http/pprof"
+ "net/url"
+ "os"
+ "runtime"
+
+ "tailscale.com/version"
+)
+
+// DebugHandler is an http.Handler that serves a debugging "homepage",
+// and provides helpers to register more debug endpoints and reports.
+//
+// The rendered page consists of three sections: informational
+// key/value pairs, links to other pages, and additional
+// program-specific HTML. Callers can add to these sections using the
+// KV, URL and Section helpers respectively.
+//
+// Additionally, the Handle method offers a shorthand for correctly
+// registering debug handlers and cross-linking them from /debug/.
+type DebugHandler struct {
+ mux *http.ServeMux // where this handler is registered
+ kvs []func(io.Writer) // output one - ...
each, see KV()
+ urls []string // one - ...
block with link each
+ sections []func(io.Writer, *http.Request) // invoked in registration order prior to outputting
+}
+
+// Debugger returns the DebugHandler registered on mux at /debug/,
+// creating it if necessary.
+func Debugger(mux *http.ServeMux) *DebugHandler {
+ h, pat := mux.Handler(&http.Request{URL: &url.URL{Path: "/debug/"}})
+ if d, ok := h.(*DebugHandler); ok && pat == "/debug/" {
+ return d
+ }
+ ret := &DebugHandler{
+ mux: mux,
+ }
+ mux.Handle("/debug/", ret)
+
+ ret.KVFunc("Uptime", func() interface{} { return Uptime() })
+ ret.KV("Version", version.Long)
+ ret.Handle("vars", "Metrics (Go)", expvar.Handler())
+ ret.Handle("varz", "Metrics (Prometheus)", http.HandlerFunc(VarzHandler))
+ ret.Handle("pprof/", "pprof", http.HandlerFunc(pprof.Index))
+ ret.URL("/debug/pprof/goroutine?debug=1", "Goroutines (collapsed)")
+ ret.URL("/debug/pprof/goroutine?debug=2", "Goroutines (full)")
+ ret.Handle("gc", "force GC", http.HandlerFunc(gcHandler))
+ hostname, err := os.Hostname()
+ if err == nil {
+ ret.KV("Machine", hostname)
+ }
+ return ret
+}
+
+// ServeHTTP implements http.Handler.
+func (d *DebugHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+ if !AllowDebugAccess(r) {
+ http.Error(w, "debug access denied", http.StatusForbidden)
+ return
+ }
+ if r.URL.Path != "/debug/" {
+ // Sub-handlers are handled by the parent mux directly.
+ http.NotFound(w, r)
+ return
+ }
+
+ f := func(format string, args ...interface{}) { fmt.Fprintf(w, format, args...) }
+ f("%s debug
", version.CmdName())
+ for _, kv := range d.kvs {
+ kv(w)
+ }
+ for _, url := range d.urls {
+ io.WriteString(w, url)
+ }
+ for _, section := range d.sections {
+ section(w, r)
+ }
+}
+
+// Handle registers handler at /debug/ and creates a descriptive
+// entry in /debug/ for it.
+func (d *DebugHandler) Handle(slug, desc string, handler http.Handler) {
+ href := "/debug/" + slug
+ d.mux.Handle(href, Protected(handler))
+ d.URL(href, desc)
+}
+
+// KV adds a key/value list item to /debug/.
+func (d *DebugHandler) KV(k string, v interface{}) {
+ val := html.EscapeString(fmt.Sprintf("%v", v))
+ d.kvs = append(d.kvs, func(w io.Writer) {
+ fmt.Fprintf(w, "- %s: %s
", k, val)
+ })
+}
+
+// KVFunc adds a key/value list item to /debug/. v is called on every
+// render of /debug/.
+func (d *DebugHandler) KVFunc(k string, v func() interface{}) {
+ d.kvs = append(d.kvs, func(w io.Writer) {
+ val := html.EscapeString(fmt.Sprintf("%v", v()))
+ fmt.Fprintf(w, "- %s: %s
", k, val)
+ })
+}
+
+// URL adds a URL and description list item to /debug/.
+func (d *DebugHandler) URL(url, desc string) {
+ if desc != "" {
+ desc = " (" + desc + ")"
+ }
+ d.urls = append(d.urls, fmt.Sprintf(`- %s%s
`, url, url, html.EscapeString(desc)))
+}
+
+// Section invokes f on every render of /debug/ to add supplemental
+// HTML to the page body.
+func (d *DebugHandler) Section(f func(w io.Writer, r *http.Request)) {
+ d.sections = append(d.sections, f)
+}
+
+func gcHandler(w http.ResponseWriter, r *http.Request) {
+ w.Write([]byte("running GC...\n"))
+ if f, ok := w.(http.Flusher); ok {
+ f.Flush()
+ }
+ runtime.GC()
+ w.Write([]byte("Done.\n"))
+}
diff --git a/tsweb/debug_test.go b/tsweb/debug_test.go
new file mode 100644
index 000000000..9bb391e96
--- /dev/null
+++ b/tsweb/debug_test.go
@@ -0,0 +1,189 @@
+// Copyright (c) 2021 Tailscale Inc & AUTHORS All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package tsweb
+
+import (
+ "fmt"
+ "io"
+ "net/http"
+ "net/http/httptest"
+ "strings"
+ "testing"
+)
+
+func TestDebugger(t *testing.T) {
+ mux := http.NewServeMux()
+
+ dbg1 := Debugger(mux)
+ if dbg1 == nil {
+ t.Fatal("didn't get a debugger from mux")
+ }
+
+ dbg2 := Debugger(mux)
+ if dbg2 != dbg1 {
+ t.Fatal("Debugger returned different debuggers for the same mux")
+ }
+}
+
+func get(m http.Handler, path, srcIP string) (int, string) {
+ req := httptest.NewRequest("GET", path, nil)
+ req.RemoteAddr = srcIP + ":1234"
+ rec := httptest.NewRecorder()
+ m.ServeHTTP(rec, req)
+ return rec.Result().StatusCode, rec.Body.String()
+}
+
+const (
+ tsIP = "100.100.100.100"
+ pubIP = "8.8.8.8"
+)
+
+func TestDebuggerKV(t *testing.T) {
+ mux := http.NewServeMux()
+ dbg := Debugger(mux)
+ dbg.KV("Donuts", 42)
+ dbg.KV("Secret code", "hunter2")
+ val := "red"
+ dbg.KVFunc("Condition", func() interface{} { return val })
+
+ code, _ := get(mux, "/debug/", pubIP)
+ if code != 403 {
+ t.Fatalf("debug access wasn't denied, got %v", code)
+ }
+
+ code, body := get(mux, "/debug/", tsIP)
+ if code != 200 {
+ t.Fatalf("debug access failed, got %v", code)
+ }
+ for _, want := range []string{"Donuts", "42", "Secret code", "hunter2", "Condition", "red"} {
+ if !strings.Contains(body, want) {
+ t.Errorf("want %q in output, not found", want)
+ }
+ }
+
+ val = "green"
+ code, body = get(mux, "/debug/", tsIP)
+ if code != 200 {
+ t.Fatalf("debug access failed, got %v", code)
+ }
+ for _, want := range []string{"Condition", "green"} {
+ if !strings.Contains(body, want) {
+ t.Errorf("want %q in output, not found", want)
+ }
+ }
+}
+
+func TestDebuggerURL(t *testing.T) {
+ mux := http.NewServeMux()
+ dbg := Debugger(mux)
+ dbg.URL("https://www.tailscale.com", "Homepage")
+
+ code, body := get(mux, "/debug/", tsIP)
+ if code != 200 {
+ t.Fatalf("debug access failed, got %v", code)
+ }
+ for _, want := range []string{"https://www.tailscale.com", "Homepage"} {
+ if !strings.Contains(body, want) {
+ t.Errorf("want %q in output, not found", want)
+ }
+ }
+}
+
+func TestDebuggerSection(t *testing.T) {
+ mux := http.NewServeMux()
+ dbg := Debugger(mux)
+ dbg.Section(func(w io.Writer, r *http.Request) {
+ fmt.Fprintf(w, "Test output %v", r.RemoteAddr)
+ })
+
+ code, body := get(mux, "/debug/", tsIP)
+ if code != 200 {
+ t.Fatalf("debug access failed, got %v", code)
+ }
+ want := `Test output 100.100.100.100:1234`
+ if !strings.Contains(body, want) {
+ t.Errorf("want %q in output, not found", want)
+ }
+}
+
+func TestDebuggerHandle(t *testing.T) {
+ mux := http.NewServeMux()
+ dbg := Debugger(mux)
+ dbg.Handle("check", "Consistency check", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ fmt.Fprintf(w, "Test output %v", r.RemoteAddr)
+ }))
+
+ code, body := get(mux, "/debug/", tsIP)
+ if code != 200 {
+ t.Fatalf("debug access failed, got %v", code)
+ }
+ for _, want := range []string{"/debug/check", "Consistency check"} {
+ if !strings.Contains(body, want) {
+ t.Errorf("want %q in output, not found", want)
+ }
+ }
+
+ code, _ = get(mux, "/debug/check", pubIP)
+ if code != 403 {
+ t.Fatal("/debug/check should be protected, but isn't")
+ }
+
+ code, body = get(mux, "/debug/check", tsIP)
+ if code != 200 {
+ t.Fatal("/debug/check denied debug access")
+ }
+ want := "Test output " + tsIP
+ if !strings.Contains(body, want) {
+ t.Errorf("want %q in output, not found", want)
+ }
+}
+
+func ExampleDebugHandler_Handle() {
+ mux := http.NewServeMux()
+ dbg := Debugger(mux)
+ // Registers /debug/flushcache with the given handler, and adds a
+ // link to /debug/ with the description "Flush caches".
+ dbg.Handle("flushcache", "Flush caches", http.HandlerFunc(http.NotFound))
+}
+
+func ExampleDebugHandler_KV() {
+ mux := http.NewServeMux()
+ dbg := Debugger(mux)
+ // Adds two list items to /debug/, showing that the condition is
+ // red and there are 42 donuts.
+ dbg.KV("Conditon", "red")
+ dbg.KV("Donuts", 42)
+}
+
+func ExampleDebugHandler_KVFunc() {
+ mux := http.NewServeMux()
+ dbg := Debugger(mux)
+ // Adds an count of page renders to /debug/. Note this example
+ // isn't concurrency-safe.
+ views := 0
+ dbg.KVFunc("Debug pageviews", func() interface{} {
+ views = views + 1
+ return views
+ })
+ dbg.KV("Donuts", 42)
+}
+
+func ExampleDebugHandler_URL() {
+ mux := http.NewServeMux()
+ dbg := Debugger(mux)
+ // Links to the Tailscale website from /debug/.
+ dbg.URL("https://www.tailscale.com", "Homepage")
+}
+
+func ExampleDebugHandler_Section() {
+ mux := http.NewServeMux()
+ dbg := Debugger(mux)
+ // Adds a section to /debug/ that dumps the HTTP request of the
+ // visitor.
+ dbg.Section(func(w io.Writer, r *http.Request) {
+ io.WriteString(w, "Dump of your HTTP request
")
+ fmt.Fprintf(w, "%#v
", r)
+ })
+}
diff --git a/tsweb/tsweb.go b/tsweb/tsweb.go
index 8c8b18e7f..790d1ce66 100644
--- a/tsweb/tsweb.go
+++ b/tsweb/tsweb.go
@@ -37,30 +37,6 @@ func init() {
// DevMode controls whether extra output in shown, for when the binary is being run in dev mode.
var DevMode bool
-// NewMux returns a new ServeMux with debugHandler registered (and protected) at /debug/.
-func NewMux(debugHandler http.Handler) *http.ServeMux {
- mux := http.NewServeMux()
- registerCommonDebug(mux)
- mux.Handle("/debug/", Protected(debugHandler))
- return mux
-}
-
-func registerCommonDebug(mux *http.ServeMux) {
- mux.Handle("/debug/pprof/", Protected(http.DefaultServeMux)) // to net/http/pprof
- mux.Handle("/debug/vars", Protected(http.DefaultServeMux)) // to expvar
- mux.Handle("/debug/varz", Protected(http.HandlerFunc(VarzHandler)))
- mux.Handle("/debug/gc", Protected(http.HandlerFunc(gcHandler)))
-}
-
-func gcHandler(w http.ResponseWriter, r *http.Request) {
- w.Write([]byte("running GC...\n"))
- if f, ok := w.(http.Flusher); ok {
- f.Flush()
- }
- runtime.GC()
- w.Write([]byte("Done.\n"))
-}
-
func DefaultCertDir(leafDir string) string {
cacheDir, err := os.UserCacheDir()
if err == nil {