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 @@
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 @@
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")
+}