// 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 tailscale contains Tailscale client code. package tailscale import ( "bytes" "context" "encoding/json" "errors" "fmt" "io" "io/ioutil" "net" "net/http" "net/url" "strconv" "strings" "tailscale.com/client/tailscale/apitype" "tailscale.com/ipn" "tailscale.com/ipn/ipnstate" "tailscale.com/paths" "tailscale.com/safesocket" "tailscale.com/tailcfg" ) // TailscaledSocket is the tailscaled Unix socket. var 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) }, }, } // 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. // // The hostname must be "local-tailscaled.sock", even though it // doesn't actually do any DNS lookup. The actual means of connecting to and // authenticating to the local Tailscale daemon vary by platform. // // DoLocalRequest may mutate the request to add Authorization headers. func DoLocalRequest(req *http.Request) (*http.Response, error) { if _, token, err := safesocket.LocalTCPPortAndToken(); err == nil { req.SetBasicAuth("", token) } return tsClient.Do(req) } type errorJSON struct { Error string } // bestError returns either err, or if body contains a valid JSON // object of type errorJSON, its non-empty error body. func bestError(err error, body []byte) error { var j errorJSON if err := json.Unmarshal(body, &j); err == nil && j.Error != "" { return errors.New(j.Error) } return err } func send(ctx context.Context, method, path string, wantStatus int, body io.Reader) ([]byte, error) { req, err := http.NewRequestWithContext(ctx, method, "http://local-tailscaled.sock"+path, body) if err != nil { return nil, err } res, err := DoLocalRequest(req) if err != nil { return nil, err } defer res.Body.Close() slurp, err := ioutil.ReadAll(res.Body) if err != nil { return nil, err } if res.StatusCode != wantStatus { err := fmt.Errorf("HTTP %s: %s (expected %v)", res.Status, slurp, wantStatus) return nil, bestError(err, slurp) } return slurp, nil } func get200(ctx context.Context, path string) ([]byte, error) { return send(ctx, "GET", path, 200, nil) } // WhoIs returns the owner of the remoteAddr, which must be an IP or IP:port. func WhoIs(ctx context.Context, remoteAddr string) (*apitype.WhoIsResponse, error) { body, err := get200(ctx, "/localapi/v0/whois?addr="+url.QueryEscape(remoteAddr)) if err != nil { return nil, err } r := new(apitype.WhoIsResponse) if err := json.Unmarshal(body, r); err != nil { if max := 200; len(body) > max { body = append(body[:max], "..."...) } return nil, fmt.Errorf("failed to parse JSON WhoIsResponse from %q", body) } return r, nil } // Goroutines returns a dump of the Tailscale daemon's current goroutines. func Goroutines(ctx context.Context) ([]byte, error) { return get200(ctx, "/localapi/v0/goroutines") } // BugReport logs and returns a log marker that can be shared by the user with support. func BugReport(ctx context.Context, note string) (string, error) { body, err := send(ctx, "POST", "/localapi/v0/bugreport?note="+url.QueryEscape(note), 200, nil) if err != nil { return "", err } return strings.TrimSpace(string(body)), nil } // Status returns the Tailscale daemon's status. func Status(ctx context.Context) (*ipnstate.Status, error) { return status(ctx, "") } // StatusWithPeers returns the Tailscale daemon's status, without the peer info. func StatusWithoutPeers(ctx context.Context) (*ipnstate.Status, error) { return status(ctx, "?peers=false") } func status(ctx context.Context, queryString string) (*ipnstate.Status, error) { body, err := get200(ctx, "/localapi/v0/status"+queryString) if err != nil { return nil, err } st := new(ipnstate.Status) if err := json.Unmarshal(body, st); err != nil { return nil, err } return st, nil } func WaitingFiles(ctx context.Context) ([]apitype.WaitingFile, error) { body, err := get200(ctx, "/localapi/v0/files/") if err != nil { return nil, err } var wfs []apitype.WaitingFile if err := json.Unmarshal(body, &wfs); err != nil { return nil, err } return wfs, nil } func DeleteWaitingFile(ctx context.Context, baseName string) error { _, err := send(ctx, "DELETE", "/localapi/v0/files/"+url.PathEscape(baseName), http.StatusNoContent, nil) return err } func GetWaitingFile(ctx context.Context, baseName string) (rc io.ReadCloser, size int64, err error) { req, err := http.NewRequestWithContext(ctx, "GET", "http://local-tailscaled.sock/localapi/v0/files/"+url.PathEscape(baseName), nil) if err != nil { return nil, 0, err } res, err := DoLocalRequest(req) if err != nil { return nil, 0, err } if res.ContentLength == -1 { res.Body.Close() return nil, 0, fmt.Errorf("unexpected chunking") } if res.StatusCode != 200 { body, _ := ioutil.ReadAll(res.Body) res.Body.Close() return nil, 0, fmt.Errorf("HTTP %s: %s", res.Status, body) } return res.Body, res.ContentLength, nil } func FileTargets(ctx context.Context) ([]apitype.FileTarget, error) { body, err := get200(ctx, "/localapi/v0/file-targets") if err != nil { return nil, err } var fts []apitype.FileTarget if err := json.Unmarshal(body, &fts); err != nil { return nil, fmt.Errorf("invalid JSON: %w", err) } return fts, nil } func CheckIPForwarding(ctx context.Context) error { body, err := get200(ctx, "/localapi/v0/check-ip-forwarding") if err != nil { return err } var jres struct { Warning string } if err := json.Unmarshal(body, &jres); err != nil { return fmt.Errorf("invalid JSON from check-ip-forwarding: %w", err) } if jres.Warning != "" { return errors.New(jres.Warning) } return nil } func GetPrefs(ctx context.Context) (*ipn.Prefs, error) { body, err := get200(ctx, "/localapi/v0/prefs") if err != nil { return nil, err } var p ipn.Prefs if err := json.Unmarshal(body, &p); err != nil { return nil, fmt.Errorf("invalid prefs JSON: %w", err) } return &p, nil } func EditPrefs(ctx context.Context, mp *ipn.MaskedPrefs) (*ipn.Prefs, error) { mpj, err := json.Marshal(mp) if err != nil { return nil, err } body, err := send(ctx, "PATCH", "/localapi/v0/prefs", http.StatusOK, bytes.NewReader(mpj)) if err != nil { return nil, err } var p ipn.Prefs if err := json.Unmarshal(body, &p); err != nil { return nil, fmt.Errorf("invalid prefs JSON: %w", err) } return &p, nil } func Logout(ctx context.Context) error { _, err := send(ctx, "POST", "/localapi/v0/logout", http.StatusNoContent, nil) return err } // SetDNS adds a DNS TXT record for the given domain name, containing // the provided TXT value. The intended use case is answering // LetsEncrypt/ACME dns-01 challenges. // // The control plane will only permit SetDNS requests with very // specific names and values. The name should be // "_acme-challenge." + your node's MagicDNS name. It's expected that // clients cache the certs from LetsEncrypt (or whichever CA is // providing them) and only request new ones as needed; the control plane // rate limits SetDNS requests. // // This is a low-level interface; it's expected that most Tailscale // users use a higher level interface to getting/using TLS // certificates. func SetDNS(ctx context.Context, name, value string) error { v := url.Values{} v.Set("name", name) v.Set("value", value) _, err := send(ctx, "POST", "/localapi/v0/set-dns?"+v.Encode(), 200, nil) return err } // CurrentDERPMap returns the current DERPMap that is being used by the local tailscaled. // It is intended to be used with netcheck to see availability of DERPs. func CurrentDERPMap(ctx context.Context) (*tailcfg.DERPMap, error) { var derpMap tailcfg.DERPMap res, err := send(ctx, "GET", "/localapi/v0/derpmap", 200, nil) if err != nil { return nil, err } if err = json.Unmarshal(res, &derpMap); err != nil { return nil, fmt.Errorf("invalid derp map json: %w", err) } return &derpMap, nil }