diff --git a/client/tailscale/tailscale.go b/client/tailscale/tailscale.go index 3b32d18b4..33f5742e0 100644 --- a/client/tailscale/tailscale.go +++ b/client/tailscale/tailscale.go @@ -21,6 +21,7 @@ import ( "runtime" "strconv" "strings" + "sync" "time" "go4.org/mem" @@ -33,30 +34,39 @@ import ( "tailscale.com/version" ) -// TailscaledSocket is the tailscaled Unix socket. -var TailscaledSocket = paths.DefaultTailscaledSocket() +var ( + // TailscaledSocket is the tailscaled Unix socket. It's used by the TailscaledDialer. + TailscaledSocket = paths.DefaultTailscaledSocket() -// tsClient does HTTP requests to the local Tailscale daemon. -var tsClient = &http.Client{ - Transport: &http.Transport{ - DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) { - if addr != "local-tailscaled.sock:80" { - return nil, fmt.Errorf("unexpected URL address %q", addr) - } - if TailscaledSocket == paths.DefaultTailscaledSocket() { - // On macOS, when dialing from non-sandboxed program to sandboxed GUI running - // a TCP server on a random port, find the random port. For HTTP connections, - // we don't send the token. It gets added in an HTTP Basic-Auth header. - if port, _, err := safesocket.LocalTCPPortAndToken(); err == nil { - var d net.Dialer - return d.DialContext(ctx, "tcp", "localhost:"+strconv.Itoa(port)) - } - } - return safesocket.Connect(TailscaledSocket, 41112) - }, - }, + // TailscaledDialer is the DialContext func that connects to the local machine's + // tailscaled or equivalent. + TailscaledDialer = defaultDialer +) + +func defaultDialer(ctx context.Context, network, addr string) (net.Conn, error) { + if addr != "local-tailscaled.sock:80" { + return nil, fmt.Errorf("unexpected URL address %q", addr) + } + if TailscaledSocket == paths.DefaultTailscaledSocket() { + // On macOS, when dialing from non-sandboxed program to sandboxed GUI running + // a TCP server on a random port, find the random port. For HTTP connections, + // we don't send the token. It gets added in an HTTP Basic-Auth header. + if port, _, err := safesocket.LocalTCPPortAndToken(); err == nil { + var d net.Dialer + return d.DialContext(ctx, "tcp", "localhost:"+strconv.Itoa(port)) + } + } + return safesocket.Connect(TailscaledSocket, 41112) } +var ( + // tsClient does HTTP requests to the local Tailscale daemon. + // We lazily initialize the client in case the caller wants to + // override TailscaledDialer. + tsClient *http.Client + tsClientOnce sync.Once +) + // DoLocalRequest makes an HTTP request to the local machine's Tailscale daemon. // // URLs are of the form http://local-tailscaled.sock/localapi/v0/whois?ip=1.2.3.4. @@ -67,6 +77,13 @@ var tsClient = &http.Client{ // // DoLocalRequest may mutate the request to add Authorization headers. func DoLocalRequest(req *http.Request) (*http.Response, error) { + tsClientOnce.Do(func() { + tsClient = &http.Client{ + Transport: &http.Transport{ + DialContext: TailscaledDialer, + }, + } + }) if _, token, err := safesocket.LocalTCPPortAndToken(); err == nil { req.SetBasicAuth("", token) } diff --git a/tsnet/example/tshello/tshello.go b/tsnet/example/tshello/tshello.go index 0153ac6ef..8bdfc9263 100644 --- a/tsnet/example/tshello/tshello.go +++ b/tsnet/example/tshello/tshello.go @@ -12,6 +12,7 @@ import ( "net/http" "strings" + "tailscale.com/client/tailscale" "tailscale.com/tsnet" ) @@ -22,9 +23,9 @@ func main() { log.Fatal(err) } log.Fatal(http.Serve(ln, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - who, ok := s.WhoIs(r.RemoteAddr) - if !ok { - http.Error(w, "WhoIs failed", 500) + who, err := tailscale.WhoIs(r.Context(), r.RemoteAddr) + if err != nil { + http.Error(w, err.Error(), 500) return } fmt.Fprintf(w, "

Hello, world!

\n") diff --git a/tsnet/tsnet.go b/tsnet/tsnet.go index b114ecb34..3fd36b536 100644 --- a/tsnet/tsnet.go +++ b/tsnet/tsnet.go @@ -12,6 +12,7 @@ import ( "fmt" "log" "net" + "net/http" "os" "path/filepath" "strconv" @@ -19,11 +20,12 @@ import ( "sync" "time" - "inet.af/netaddr" - "tailscale.com/client/tailscale/apitype" + "tailscale.com/client/tailscale" "tailscale.com/control/controlclient" "tailscale.com/ipn" "tailscale.com/ipn/ipnlocal" + "tailscale.com/ipn/localapi" + "tailscale.com/net/nettest" "tailscale.com/smallzstd" "tailscale.com/types/logger" "tailscale.com/wgengine" @@ -60,28 +62,6 @@ type Server struct { listeners map[listenKey]*listener } -// WhoIs reports the node and user who owns the node with the given -// address. The addr may be an ip:port (as from an -// http.Request.RemoteAddr) or just an IP address. -func (s *Server) WhoIs(addr string) (w *apitype.WhoIsResponse, ok bool) { - ipp, err := netaddr.ParseIPPort(addr) - if err != nil { - ip, err := netaddr.ParseIP(addr) - if err != nil { - return nil, false - } - ipp = ipp.WithIP(ip) - } - n, up, ok := s.lb.WhoIs(ipp) - if !ok { - return nil, false - } - return &apitype.WhoIsResponse{ - Node: n, - UserProfile: &up, - }, true -} - func (s *Server) doInit() { if err := s.start(); err != nil { s.initErr = fmt.Errorf("tsnet: %w", err) @@ -185,6 +165,24 @@ func (s *Server) start() error { if os.Getenv("TS_LOGIN") == "1" || os.Getenv("TS_AUTHKEY") != "" { s.lb.StartLoginInteractive() } + + // Run the localapi handler, to allow fetching LetsEncrypt certs. + lah := localapi.NewHandler(lb, logf, logid) + lah.PermitWrite = true + lah.PermitRead = true + + // Create an in-process listener. + // nettest.Listen provides a in-memory pipe based implementation for net.Conn. + // TODO(maisem): Rename nettest package to remove "test". + lal := nettest.Listen("local-tailscaled.sock:80") + + // Override the Tailscale client to use the in-process listener. + tailscale.TailscaledDialer = lal.Dial + go func() { + if err := http.Serve(lal, lah); err != nil { + logf("localapi serve error: %v", err) + } + }() return nil }