diff --git a/cmd/tailscaled/depaware.txt b/cmd/tailscaled/depaware.txt index c19b50860..f87d3cc91 100644 --- a/cmd/tailscaled/depaware.txt +++ b/cmd/tailscaled/depaware.txt @@ -72,9 +72,10 @@ tailscale.com/cmd/tailscaled dependencies: (generated by github.com/tailscale/de tailscale.com/disco from tailscale.com/derp+ tailscale.com/internal/deepprint from tailscale.com/ipn/ipnlocal+ tailscale.com/ipn from tailscale.com/ipn/ipnserver+ - tailscale.com/ipn/ipnlocal from tailscale.com/ipn/ipnserver + tailscale.com/ipn/ipnlocal from tailscale.com/ipn/ipnserver+ tailscale.com/ipn/ipnserver from tailscale.com/cmd/tailscaled tailscale.com/ipn/ipnstate from tailscale.com/ipn+ + tailscale.com/ipn/localapi from tailscale.com/ipn/ipnserver tailscale.com/ipn/policy from tailscale.com/ipn/ipnlocal tailscale.com/log/filelogger from tailscale.com/ipn/ipnserver tailscale.com/log/logheap from tailscale.com/control/controlclient diff --git a/ipn/ipnserver/server.go b/ipn/ipnserver/server.go index bc120ca60..681e5263c 100644 --- a/ipn/ipnserver/server.go +++ b/ipn/ipnserver/server.go @@ -7,7 +7,6 @@ import ( "bufio" "context" - "encoding/json" "errors" "fmt" "io" @@ -25,16 +24,17 @@ "syscall" "time" + "go4.org/mem" "inet.af/netaddr" "tailscale.com/control/controlclient" "tailscale.com/ipn" "tailscale.com/ipn/ipnlocal" + "tailscale.com/ipn/localapi" "tailscale.com/log/filelogger" "tailscale.com/logtail/backoff" "tailscale.com/net/netstat" "tailscale.com/safesocket" "tailscale.com/smallzstd" - "tailscale.com/tailcfg" "tailscale.com/types/logger" "tailscale.com/util/pidowner" "tailscale.com/util/systemd" @@ -222,13 +222,22 @@ func (s *server) blockWhileInUse(conn io.Reader, ci connIdentity) { } } +// bufferHasHTTPRequest reports whether br looks like it has an HTTP +// request in it, without reading any bytes from it. +func bufferHasHTTPRequest(br *bufio.Reader) bool { + peek, _ := br.Peek(br.Buffered()) + return mem.HasPrefix(mem.B(peek), mem.S("GET ")) || + mem.HasPrefix(mem.B(peek), mem.S("POST ")) || + mem.Contains(mem.B(peek), mem.S(" HTTP/")) +} + func (s *server) serveConn(ctx context.Context, c net.Conn, logf logger.Logf) { // First see if it's an HTTP request. br := bufio.NewReader(c) c.SetReadDeadline(time.Now().Add(time.Second)) - peek, _ := br.Peek(4) + br.Peek(4) c.SetReadDeadline(time.Time{}) - isHTTPReq := string(peek) == "GET " + isHTTPReq := bufferHasHTTPRequest(br) ci, err := s.addConn(c, isHTTPReq) if err != nil { @@ -255,7 +264,7 @@ func (s *server) serveConn(ctx context.Context, c net.Conn, logf logger.Logf) { s.b.SetCurrentUserID(ci.UserID) if isHTTPReq { - httpServer := http.Server{ + httpServer := &http.Server{ // Localhost connections are cheap; so only do // keep-alives for a short period of time, as these // active connections lock the server into only serving @@ -626,7 +635,9 @@ func Run(ctx context.Context, logf logger.Logf, logid string, getEngine func() ( opts.DebugMux.HandleFunc("/debug/ipn", func(w http.ResponseWriter, r *http.Request) { serveHTMLStatus(w, b) }) - opts.DebugMux.Handle("/localapi/v0/whois", whoIsHandler{b}) + h := localapi.NewHandler(b) + h.PermitRead = true + opts.DebugMux.Handle("/localapi/", h) } server.b = b @@ -867,8 +878,11 @@ func (psc *protoSwitchConn) Close() error { func (s *server) localhostHandler(ci connIdentity) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if ci.IsUnixSock && r.URL.Path == "/localapi/v0/whois" { - whoIsHandler{s.b}.ServeHTTP(w, r) + if ci.IsUnixSock && strings.HasPrefix(r.URL.Path, "/localapi/") { + h := localapi.NewHandler(s.b) + h.PermitRead = true + h.PermitWrite = false // TODO: flesh out connIdentity on more platforms then set this + h.ServeHTTP(w, r) return } if ci.Unknown { @@ -894,40 +908,3 @@ func peerPid(entries []netstat.Entry, la, ra netaddr.IPPort) int { } return 0 } - -// whoIsHandler is the debug server's /debug?ip=$IP HTTP handler. -type whoIsHandler struct { - b *ipnlocal.LocalBackend -} - -func (h whoIsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - b := h.b - var ip netaddr.IP - if v := r.FormValue("ip"); v != "" { - var err error - ip, err = netaddr.ParseIP(r.FormValue("ip")) - if err != nil { - http.Error(w, "invalid 'ip' parameter", 400) - return - } - } else { - http.Error(w, "missing 'ip' parameter", 400) - return - } - n, u, ok := b.WhoIs(ip) - if !ok { - http.Error(w, "no match for IP", 404) - return - } - res := &tailcfg.WhoIsResponse{ - Node: n, - UserProfile: &u, - } - j, err := json.MarshalIndent(res, "", "\t") - if err != nil { - http.Error(w, "JSON encoding error", 500) - return - } - w.Header().Set("Content-Type", "application/json") - w.Write(j) -} diff --git a/ipn/localapi/localapi.go b/ipn/localapi/localapi.go new file mode 100644 index 000000000..7b537d404 --- /dev/null +++ b/ipn/localapi/localapi.go @@ -0,0 +1,95 @@ +// 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 localapi contains the HTTP server handlers for tailscaled's API server. +package localapi + +import ( + "encoding/json" + "io" + "net/http" + + "inet.af/netaddr" + "tailscale.com/ipn/ipnlocal" + "tailscale.com/tailcfg" +) + +func NewHandler(b *ipnlocal.LocalBackend) *Handler { + return &Handler{b: b} +} + +type Handler struct { + // RequiredPassword, if non-empty, forces all HTTP + // requests to have HTTP basic auth with this password. + // It's used by the sandboxed macOS sameuserproof GUI auth mechanism. + RequiredPassword string + + // PermitRead is whether read-only HTTP handlers are allowed. + PermitRead bool + + // PermitWrite is whether mutating HTTP handlers are allowed. + PermitWrite bool + + b *ipnlocal.LocalBackend +} + +func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + if h.b == nil { + http.Error(w, "server has no local backend", http.StatusInternalServerError) + return + } + if h.RequiredPassword != "" { + _, pass, ok := r.BasicAuth() + if !ok { + http.Error(w, "auth required", http.StatusUnauthorized) + return + } + if pass != h.RequiredPassword { + http.Error(w, "bad password", http.StatusForbidden) + return + } + } + switch r.URL.Path { + case "/localapi/v0/whois": + h.serveWhoIs(w, r) + default: + io.WriteString(w, "tailscaled\n") + } +} + +func (h *Handler) serveWhoIs(w http.ResponseWriter, r *http.Request) { + if !h.PermitRead { + http.Error(w, "whois access denied", http.StatusForbidden) + return + } + b := h.b + var ip netaddr.IP + if v := r.FormValue("ip"); v != "" { + var err error + ip, err = netaddr.ParseIP(r.FormValue("ip")) + if err != nil { + http.Error(w, "invalid 'ip' parameter", 400) + return + } + } else { + http.Error(w, "missing 'ip' parameter", 400) + return + } + n, u, ok := b.WhoIs(ip) + if !ok { + http.Error(w, "no match for IP", 404) + return + } + res := &tailcfg.WhoIsResponse{ + Node: n, + UserProfile: &u, + } + j, err := json.MarshalIndent(res, "", "\t") + if err != nil { + http.Error(w, "JSON encoding error", 500) + return + } + w.Header().Set("Content-Type", "application/json") + w.Write(j) +}