diff --git a/client/tailscale/tailscale.go b/client/tailscale/tailscale.go index 80f41ac4d..696af8a6f 100644 --- a/client/tailscale/tailscale.go +++ b/client/tailscale/tailscale.go @@ -90,6 +90,27 @@ func DoLocalRequest(req *http.Request) (*http.Response, error) { return tsClient.Do(req) } +func doLocalRequestNiceError(req *http.Request) (*http.Response, error) { + res, err := DoLocalRequest(req) + if err == nil { + if server := res.Header.Get("Tailscale-Version"); server != "" && server != version.Long && onVersionMismatch != nil { + onVersionMismatch(version.Long, server) + } + return res, nil + } + if ue, ok := err.(*url.Error); ok { + if oe, ok := ue.Err.(*net.OpError); ok && oe.Op == "dial" { + path := req.URL.Path + pathPrefix := path + if i := strings.Index(path, "?"); i != -1 { + pathPrefix = path[:i] + } + return nil, fmt.Errorf("Failed to connect to local Tailscale daemon for %s; %s Error: %w", pathPrefix, tailscaledConnectHint(), oe) + } + } + return nil, err +} + type errorJSON struct { Error string } @@ -140,23 +161,11 @@ func send(ctx context.Context, method, path string, wantStatus int, body io.Read if err != nil { return nil, err } - res, err := DoLocalRequest(req) + res, err := doLocalRequestNiceError(req) if err != nil { - if ue, ok := err.(*url.Error); ok { - if oe, ok := ue.Err.(*net.OpError); ok && oe.Op == "dial" { - pathPrefix := path - if i := strings.Index(path, "?"); i != -1 { - pathPrefix = path[:i] - } - return nil, fmt.Errorf("Failed to connect to local Tailscale daemon for %s; %s Error: %w", pathPrefix, tailscaledConnectHint(), oe) - } - } return nil, err } defer res.Body.Close() - if server := res.Header.Get("Tailscale-Version"); server != "" && server != version.Long && onVersionMismatch != nil { - onVersionMismatch(version.Long, server) - } slurp, err := ioutil.ReadAll(res.Body) if err != nil { return nil, err @@ -295,6 +304,30 @@ func FileTargets(ctx context.Context) ([]apitype.FileTarget, error) { return fts, nil } +// PushFile sends Taildrop file r to target. +// +// A size of -1 means unknown. +// The name parameter is the original filename, not escaped. +func PushFile(ctx context.Context, target tailcfg.StableNodeID, size int64, name string, r io.Reader) error { + req, err := http.NewRequestWithContext(ctx, "PUT", "http://local-tailscaled.sock/localapi/v0/file-put/"+string(target)+"/"+url.PathEscape(name), r) + if err != nil { + return err + } + if size != -1 { + req.ContentLength = size + } + res, err := doLocalRequestNiceError(req) + if err != nil { + return err + } + if res.StatusCode == 200 { + io.Copy(io.Discard, res.Body) + return nil + } + all, _ := io.ReadAll(res.Body) + return fmt.Errorf("%s: %s", res.Status, all) +} + func CheckIPForwarding(ctx context.Context) error { body, err := get200(ctx, "/localapi/v0/check-ip-forwarding") if err != nil { diff --git a/cmd/tailscale/cli/file.go b/cmd/tailscale/cli/file.go index 9f42c1d04..775c44933 100644 --- a/cmd/tailscale/cli/file.go +++ b/cmd/tailscale/cli/file.go @@ -11,11 +11,9 @@ import ( "flag" "fmt" "io" - "io/ioutil" "log" "mime" "net/http" - "net/url" "os" "path/filepath" "strconv" @@ -30,6 +28,7 @@ import ( "tailscale.com/client/tailscale/apitype" "tailscale.com/ipn" "tailscale.com/net/tsaddr" + "tailscale.com/tailcfg" "tailscale.com/version" ) @@ -96,7 +95,7 @@ func runCp(ctx context.Context, args []string) error { return err } - peerAPIBase, isOffline, err := discoverPeerAPIBase(ctx, ip) + stableID, isOffline, err := getTargetStableID(ctx, ip) if err != nil { return fmt.Errorf("can't send to %s: %v", target, err) } @@ -154,32 +153,21 @@ func runCp(ctx context.Context, args []string) error { } } - dstURL := peerAPIBase + "/v0/put/" + url.PathEscape(name) - req, err := http.NewRequestWithContext(ctx, "PUT", dstURL, fileContents) - if err != nil { - return err - } - req.ContentLength = contentLength if cpArgs.verbose { - log.Printf("sending to %v ...", dstURL) + log.Printf("sending %q to %v/%v/%v ...", name, target, ip, stableID) } - res, err := http.DefaultClient.Do(req) + err := tailscale.PushFile(ctx, stableID, contentLength, name, fileContents) if err != nil { return err } - if res.StatusCode == 200 { - io.Copy(ioutil.Discard, res.Body) - res.Body.Close() - continue + if cpArgs.verbose { + log.Printf("sent %q", name) } - io.Copy(Stdout, res.Body) - res.Body.Close() - return errors.New(res.Status) } return nil } -func discoverPeerAPIBase(ctx context.Context, ipStr string) (base string, isOffline bool, err error) { +func getTargetStableID(ctx context.Context, ipStr string) (id tailcfg.StableNodeID, isOffline bool, err error) { ip, err := netaddr.ParseIP(ipStr) if err != nil { return "", false, err @@ -195,7 +183,7 @@ func discoverPeerAPIBase(ctx context.Context, ipStr string) (base string, isOffl continue } isOffline = n.Online != nil && !*n.Online - return ft.PeerAPIURL, isOffline, nil + return n.StableID, isOffline, nil } } return "", false, fileTargetErrorDetail(ctx, ip) diff --git a/ipn/localapi/localapi.go b/ipn/localapi/localapi.go index 00673730e..b8651ae74 100644 --- a/ipn/localapi/localapi.go +++ b/ipn/localapi/localapi.go @@ -376,6 +376,25 @@ func (h *Handler) serveFileTargets(w http.ResponseWriter, r *http.Request) { json.NewEncoder(w).Encode(fts) } +// serveFilePut sends a file to another node. +// +// It's sometimes possible for clients to do this themselves, without +// tailscaled, except in the case of tailscaled running in +// userspace-networking ("netstack") mode, in which case tailscaled +// needs to a do a netstack dial out. +// +// Instead, the CLI also goes through tailscaled so it doesn't need to be +// aware of the network mode in use. +// +// macOS/iOS have always used this localapi method to simplify the GUI +// clients. +// +// The Windows client currently (2021-11-30) uses the peerapi (/v0/put/) +// directly, as the Windows GUI always runs in tun mode anyway. +// +// URL format: +// +// * PUT /localapi/v0/file-put/:stableID/:escaped-filename func (h *Handler) serveFilePut(w http.ResponseWriter, r *http.Request) { if !h.PermitWrite { http.Error(w, "file access denied", http.StatusForbidden)