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 @@ "runtime" "strconv" "strings" + "sync" "time" "go4.org/mem" @@ -33,30 +34,39 @@ "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 @@ // // 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 @@ "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, "