diff --git a/ipn/ipnauth/ipnauth.go b/ipn/ipnauth/ipnauth.go index 2ca6caaa3..fb82a08aa 100644 --- a/ipn/ipnauth/ipnauth.go +++ b/ipn/ipnauth/ipnauth.go @@ -57,7 +57,6 @@ func (ci *ConnIdentity) UserID() string { return ci.userID } func (ci *ConnIdentity) User() *user.User { return ci.user } func (ci *ConnIdentity) Pid() int { return ci.pid } func (ci *ConnIdentity) IsUnixSock() bool { return ci.isUnixSock } -func (ci *ConnIdentity) NotWindows() bool { return ci.notWindows } func (ci *ConnIdentity) Creds() *peercred.Creds { return ci.creds } // GetConnIdentity returns the localhost TCP connection's identity information diff --git a/ipn/ipnserver/proxyconnect.go b/ipn/ipnserver/proxyconnect.go index b3eb0d458..b1c8c76f4 100644 --- a/ipn/ipnserver/proxyconnect.go +++ b/ipn/ipnserver/proxyconnect.go @@ -7,15 +7,11 @@ package ipnserver import ( - "bufio" - "context" "io" "net" "net/http" - "time" "tailscale.com/logpolicy" - "tailscale.com/types/logger" ) // handleProxyConnectConn handles a CONNECT request to @@ -27,40 +23,42 @@ import ( // "Internet Kill Switch" installed by tailscaled for exit nodes // precludes that from working and instead the GUI fails to dial out. // So, go through tailscaled (with a CONNECT request) instead. -func (s *Server) handleProxyConnectConn(ctx context.Context, br *bufio.Reader, c net.Conn, logf logger.Logf) { - defer c.Close() - - c.SetReadDeadline(time.Now().Add(5 * time.Second)) // should be long enough to send the HTTP headers - req, err := http.ReadRequest(br) - if err != nil { - logf("ReadRequest: %v", err) - return - } - c.SetReadDeadline(time.Time{}) - - if req.Method != "CONNECT" { - logf("ReadRequest: unexpected method %q, not CONNECT", req.Method) - return +func (s *Server) handleProxyConnectConn(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + if r.Method != "CONNECT" { + panic("[unexpected] miswired") } - hostPort := req.RequestURI + hostPort := r.RequestURI logHost := logpolicy.LogHost() allowed := net.JoinHostPort(logHost, "443") if hostPort != allowed { - logf("invalid CONNECT target %q; want %q", hostPort, allowed) - io.WriteString(c, "HTTP/1.1 403 Forbidden\r\n\r\nBad CONNECT target.\n") + s.logf("invalid CONNECT target %q; want %q", hostPort, allowed) + http.Error(w, "Bad CONNECT target.", http.StatusForbidden) return } tr := logpolicy.NewLogtailTransport(logHost) back, err := tr.DialContext(ctx, "tcp", hostPort) if err != nil { - logf("error CONNECT dialing %v: %v", hostPort, err) - io.WriteString(c, "HTTP/1.1 502 Fail\r\n\r\nConnect failure.\n") + s.logf("error CONNECT dialing %v: %v", hostPort, err) + http.Error(w, "Connect failure", http.StatusBadGateway) return } defer back.Close() + hj, ok := w.(http.Hijacker) + if !ok { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + c, br, err := hj.Hijack() + if err != nil { + s.logf("CONNECT hijack: %v", err) + return + } + defer c.Close() + io.WriteString(c, "HTTP/1.1 200 OK\r\n\r\n") errc := make(chan error, 2) diff --git a/ipn/ipnserver/proxyconnect_js.go b/ipn/ipnserver/proxyconnect_js.go index 42cfe44f5..843f50180 100644 --- a/ipn/ipnserver/proxyconnect_js.go +++ b/ipn/ipnserver/proxyconnect_js.go @@ -4,14 +4,8 @@ package ipnserver -import ( - "bufio" - "context" - "net" +import "net/http" - "tailscale.com/types/logger" -) - -func (s *Server) handleProxyConnectConn(ctx context.Context, br *bufio.Reader, c net.Conn, logf logger.Logf) { +func (s *Server) handleProxyConnectConn(w http.ResponseWriter, r *http.Request) { panic("unreachable") } diff --git a/ipn/ipnserver/server.go b/ipn/ipnserver/server.go index 00c89eb7f..b9a705bd7 100644 --- a/ipn/ipnserver/server.go +++ b/ipn/ipnserver/server.go @@ -5,8 +5,8 @@ package ipnserver import ( - "bufio" "context" + "errors" "fmt" "io" "net" @@ -20,7 +20,6 @@ import ( "time" "unicode" - "go4.org/mem" "tailscale.com/control/controlclient" "tailscale.com/envknob" "tailscale.com/ipn" @@ -29,10 +28,10 @@ import ( "tailscale.com/ipn/localapi" "tailscale.com/logtail/backoff" "tailscale.com/net/dnsfallback" - "tailscale.com/net/netutil" "tailscale.com/net/tsdial" "tailscale.com/smallzstd" "tailscale.com/types/logger" + "tailscale.com/util/mak" "tailscale.com/util/systemd" "tailscale.com/version/distro" "tailscale.com/wgengine" @@ -82,9 +81,9 @@ type Server struct { logf logger.Logf backendLogID string // resetOnZero is whether to call bs.Reset on transition from - // 1->0 connections. That is, this is whether the backend is + // 1->0 active HTTP requests. That is, this is whether the backend is // being run in "client mode" that requires an active GUI - // connection (such as on Windows by default). Even if this + // connection (such as on Windows by default). Even if this // is true, the ForceDaemon pref can override this. resetOnZero bool @@ -92,56 +91,64 @@ type Server struct { // lock order: mu, then LocalBackend.mu mu sync.Mutex lastUserID string // tracks last userid; on change, Reset state for paranoia - allClients map[net.Conn]*ipnauth.ConnIdentity + activeReqs map[*http.Request]*ipnauth.ConnIdentity } // LocalBackend returns the server's LocalBackend. func (s *Server) LocalBackend() *ipnlocal.LocalBackend { return s.b } -// bufferIsConnect reports whether br looks like it's likely an HTTP -// CONNECT request. -// -// Invariant: br has already had at least 4 bytes Peek'ed. -func bufferIsConnect(br *bufio.Reader) bool { - peek, _ := br.Peek(br.Buffered()) - return mem.HasPrefix(mem.B(peek), mem.S("CONN")) -} - -func (s *Server) serveConn(ctx context.Context, c net.Conn, logf logger.Logf) { - // First sniff a few bytes to check its HTTP method. - br := bufio.NewReader(c) - c.SetReadDeadline(time.Now().Add(30 * time.Second)) - br.Peek(len("GET / HTTP/1.1\r\n")) // reasonable sniff size to get HTTP method - c.SetReadDeadline(time.Time{}) - - // Handle logtail CONNECT requests early. (See docs on handleProxyConnectConn) - if bufferIsConnect(br) { - s.handleProxyConnectConn(ctx, br, c, logf) +func (s *Server) serveHTTP(w http.ResponseWriter, r *http.Request) { + if r.Method == "CONNECT" { + if envknob.GOOS() == "windows" { + // For the GUI client when using an exit node. See docs on handleProxyConnectConn. + s.handleProxyConnectConn(w, r) + } else { + http.Error(w, "bad method for platform", http.StatusMethodNotAllowed) + } return } - ci, err := s.addConn(c) + var ci *ipnauth.ConnIdentity + switch v := r.Context().Value(connIdentityContextKey{}).(type) { + case *ipnauth.ConnIdentity: + ci = v + case error: + http.Error(w, v.Error(), http.StatusUnauthorized) + return + case nil: + http.Error(w, "internal error: no connIdentityContextKey", http.StatusInternalServerError) + return + } + + onDone, err := s.addActiveHTTPRequest(r, ci) if err != nil { - fmt.Fprintf(c, "HTTP/1.0 500 Nope\r\nContent-Type: text/plain\r\nX-Content-Type-Options: nosniff\r\n\r\n%s\n", err.Error()) - c.Close() + http.Error(w, err.Error(), http.StatusUnauthorized) + return + } + defer onDone() + + if strings.HasPrefix(r.URL.Path, "/localapi/") { + lah := localapi.NewHandler(s.b, s.logf, s.backendLogID) + lah.PermitRead, lah.PermitWrite = s.localAPIPermissions(ci) + lah.PermitCert = s.connCanFetchCerts(ci) + lah.ServeHTTP(w, r) + return } - // Tell the LocalBackend about the identity we're now running as. - s.b.SetCurrentUserID(ci.UserID()) - - 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 - // that user. If the user has this page open, we don't - // want another switching user to be locked out for - // minutes. 5 seconds is enough to let browser hit - // favicon.ico and such. - IdleTimeout: 5 * time.Second, - ErrorLog: logger.StdLogger(logf), - Handler: s.localhostHandler(ci), + if r.URL.Path != "/" { + http.NotFound(w, r) + return } - httpServer.Serve(netutil.NewOneConnListener(&protoSwitchConn{s: s, br: br, Conn: c}, nil)) + + if envknob.GOOS() == "windows" { + // TODO(bradfitz): remove this once we moved to named pipes for LocalAPI + // on Windows. This could then move to all platforms instead at + // 100.100.100.100 or something (quad100 handler in LocalAPI) + s.ServeHTMLStatus(w, r) + return + } + + io.WriteString(w, "