From 824f8255521e989996f30a504ce523b147ec4acf Mon Sep 17 00:00:00 2001 From: Brad Fitzpatrick Date: Tue, 25 Feb 2020 08:07:41 -0800 Subject: [PATCH] tsweb, cmd/derper: move common web/debug stuff from derper to new tsweb Signed-off-by: Brad Fitzpatrick --- cmd/derper/derper.go | 93 +++------------------------------- tsweb/tsweb.go | 116 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 123 insertions(+), 86 deletions(-) create mode 100644 tsweb/tsweb.go diff --git a/cmd/derper/derper.go b/cmd/derper/derper.go index fd14d079d..62eca60c4 100644 --- a/cmd/derper/derper.go +++ b/cmd/derper/derper.go @@ -8,27 +8,22 @@ package main // import "tailscale.com/cmd/derper" import ( "encoding/json" "expvar" - _ "expvar" "flag" "fmt" "io" "io/ioutil" "log" - "net" "net/http" - _ "net/http/pprof" "os" "path/filepath" - "strings" - "time" "github.com/tailscale/wireguard-go/wgcfg" "golang.org/x/crypto/acme/autocert" "tailscale.com/atomicfile" "tailscale.com/derp" "tailscale.com/derp/derphttp" - "tailscale.com/interfaces" "tailscale.com/logpolicy" + "tailscale.com/tsweb" "tailscale.com/types/key" ) @@ -36,20 +31,12 @@ var ( dev = flag.Bool("dev", false, "run in localhost development mode") addr = flag.String("a", ":443", "server address") configPath = flag.String("c", "", "config file path") - certDir = flag.String("certdir", defaultCertDir(), "directory to store LetsEncrypt certs, if addr's port is :443") + certDir = flag.String("certdir", tsweb.DefaultCertDir("derper-certs"), "directory to store LetsEncrypt certs, if addr's port is :443") hostname = flag.String("hostname", "derp.tailscale.com", "LetsEncrypt host name, if addr's port is :443") mbps = flag.Int("mbps", 5, "Mbps (mebibit/s) per-client rate limit; 0 means unlimited") logCollection = flag.String("logcollection", "", "If non-empty, logtail collection to log to") ) -func defaultCertDir() string { - cacheDir, err := os.UserCacheDir() - if err == nil { - return filepath.Join(cacheDir, "tailscale", "derper-certs") - } - return "" -} - type config struct { PrivateKey wgcfg.PrivateKey } @@ -120,24 +107,17 @@ func main() { cfg := loadConfig() - letsEncrypt := false - if _, port, _ := net.SplitHostPort(*addr); port == "443" { - letsEncrypt = true - } + letsEncrypt := tsweb.IsProd443(*addr) s := derp.NewServer(key.Private(cfg.PrivateKey), log.Printf) if *mbps != 0 { s.BytesPerSecond = (*mbps << 20) / 8 } expvar.Publish("derp", s.ExpVar()) - expvar.Publish("uptime", uptimeVar{}) // Create our own mux so we don't expose /debug/ stuff to the world. - mux := http.NewServeMux() + mux := tsweb.NewMux(debugHandler(s)) mux.Handle("/derp", derphttp.Handler(s)) - mux.Handle("/debug/", protected(debugHandler(s))) - mux.Handle("/debug/pprof/", protected(http.DefaultServeMux)) // to net/http/pprof - mux.Handle("/debug/vars", protected(http.DefaultServeMux)) // to expvar mux.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/html; charset=utf-8") w.WriteHeader(200) @@ -150,7 +130,7 @@ func main() { server.

`) - if allowDebugAccess(r) { + if tsweb.AllowDebugAccess(r) { io.WriteString(w, "

Debug info at /debug/.

\n") } })) @@ -173,7 +153,7 @@ func main() { } httpsrv.TLSConfig = certManager.TLSConfig() go func() { - err := http.ListenAndServe(":80", certManager.HTTPHandler(port80Handler{mux})) + err := http.ListenAndServe(":80", certManager.HTTPHandler(tsweb.Port80Handler{mux})) if err != nil { if err != http.ErrServerClosed { log.Fatal(err) @@ -190,57 +170,6 @@ func main() { } } -func protected(h http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if !allowDebugAccess(r) { - http.Error(w, "debug access denied", http.StatusForbidden) - return - } - h.ServeHTTP(w, r) - }) -} - -func allowDebugAccess(r *http.Request) bool { - if r.Header.Get("X-Forwarded-For") != "" { - // TODO if/when needed. For now, conservative: - return false - } - ipStr, _, err := net.SplitHostPort(r.RemoteAddr) - if err != nil { - return false - } - ip := net.ParseIP(ipStr) - return interfaces.IsTailscaleIP(ip) || ip.IsLoopback() || ipStr == os.Getenv("ALLOW_DEBUG_IP") -} - -type port80Handler struct{ tlsHandler http.Handler } - -func (h port80Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - path := r.RequestURI - if path == "/debug" || strings.HasPrefix(path, "/debug") { - h.tlsHandler.ServeHTTP(w, r) - return - } - if r.Method != "GET" && r.Method != "HEAD" { - http.Error(w, "Use HTTPS", http.StatusBadRequest) - return - } - if path == "/" && allowDebugAccess(r) { - // Redirect authorized user to the debug handler. - path = "/debug/" - } - target := "https://" + stripPort(r.Host) + path - http.Redirect(w, r, target, http.StatusFound) -} - -func stripPort(hostport string) string { - host, _, err := net.SplitHostPort(hostport) - if err != nil { - return hostport - } - return net.JoinHostPort(host, "443") -} - func debugHandler(s *derp.Server) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { f := func(format string, args ...interface{}) { fmt.Fprintf(w, format, args...) } @@ -250,7 +179,7 @@ func debugHandler(s *derp.Server) http.Handler { `) f("
  • Hostname: %v
  • \n", *hostname) f("
  • Rate Limit: %v Mbps
  • \n", *mbps) - f("
  • Uptime: %v
  • \n", uptime().Round(time.Second)) + f("
  • Uptime: %v
  • \n", tsweb.Uptime()) f(`
  • /debug/vars
  • /debug/pprof/
  • @@ -261,11 +190,3 @@ func debugHandler(s *derp.Server) http.Handler { `) }) } - -var timeStart = time.Now() - -func uptime() time.Duration { return time.Since(timeStart) } - -type uptimeVar struct{} - -func (uptimeVar) String() string { return fmt.Sprint(int64(uptime().Seconds())) } diff --git a/tsweb/tsweb.go b/tsweb/tsweb.go new file mode 100644 index 000000000..48461f5ee --- /dev/null +++ b/tsweb/tsweb.go @@ -0,0 +1,116 @@ +// Copyright (c) 2020 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 contains code between various Tailscale webservers. +package tsweb + +import ( + "expvar" + _ "expvar" + "fmt" + "net" + "net/http" + _ "net/http/pprof" + "os" + "path/filepath" + "strings" + "time" + + "tailscale.com/interfaces" +) + +// 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) { + expvar.Publish("uptime", uptimeVar{}) + mux.Handle("/debug/pprof/", Protected(http.DefaultServeMux)) // to net/http/pprof + mux.Handle("/debug/vars", Protected(http.DefaultServeMux)) // to expvar +} + +func DefaultCertDir(leafDir string) string { + cacheDir, err := os.UserCacheDir() + if err == nil { + return filepath.Join(cacheDir, "tailscale", leafDir) + } + return "" +} + +// IsProd443 reports whether addr is a Go listen address for port 443. +func IsProd443(addr string) bool { + _, port, _ := net.SplitHostPort(addr) + return port == "443" || port == "https" +} + +// AllowDebugAccess reports whether r should be permitted to access +// various debug endpoints. +func AllowDebugAccess(r *http.Request) bool { + if r.Header.Get("X-Forwarded-For") != "" { + // TODO if/when needed. For now, conservative: + return false + } + ipStr, _, err := net.SplitHostPort(r.RemoteAddr) + if err != nil { + return false + } + ip := net.ParseIP(ipStr) + return interfaces.IsTailscaleIP(ip) || ip.IsLoopback() || ipStr == os.Getenv("ALLOW_DEBUG_IP") +} + +// Protected wraps a provided debug handler, h, returning a Handler +// that enforces AllowDebugAccess and returns forbiden replies for +// unauthorized requests. +func Protected(h http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if !AllowDebugAccess(r) { + http.Error(w, "debug access denied", http.StatusForbidden) + return + } + h.ServeHTTP(w, r) + }) +} + +var timeStart = time.Now() + +func Uptime() time.Duration { return time.Since(timeStart).Round(time.Second) } + +type uptimeVar struct{} + +func (uptimeVar) String() string { return fmt.Sprint(int64(Uptime().Seconds())) } + +// Port80Handler is the handler to be given to +// autocert.Manager.HTTPHandler. The inner handler is the mux +// returned by NewMux containing registered /debug handlers. +type Port80Handler struct{ Main http.Handler } + +func (h Port80Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + path := r.RequestURI + if path == "/debug" || strings.HasPrefix(path, "/debug") { + h.Main.ServeHTTP(w, r) + return + } + if r.Method != "GET" && r.Method != "HEAD" { + http.Error(w, "Use HTTPS", http.StatusBadRequest) + return + } + if path == "/" && AllowDebugAccess(r) { + // Redirect authorized user to the debug handler. + path = "/debug/" + } + target := "https://" + stripPort(r.Host) + path + http.Redirect(w, r, target, http.StatusFound) +} + +func stripPort(hostport string) string { + host, _, err := net.SplitHostPort(hostport) + if err != nil { + return hostport + } + return net.JoinHostPort(host, "443") +}