فهرست منبع

cmd/tailscale: make file cp send files via tailscaled localapi

So Taildrop sends work even if the local tailscaled is running in
netstack mode, as it often is on Synology, etc.

Updates #2179 (which is primarily about receiving, but both important)

Change-Id: I9bd1afdc8d25717e0ab6802c7cf2f5e0bd89a3b2
Signed-off-by: Brad Fitzpatrick <[email protected]>
Brad Fitzpatrick 4 سال پیش
والد
کامیت
3181bbb8e4
3فایلهای تغییر یافته به همراه73 افزوده شده و 33 حذف شده
  1. 46 13
      client/tailscale/tailscale.go
  2. 8 20
      cmd/tailscale/cli/file.go
  3. 19 0
      ipn/localapi/localapi.go

+ 46 - 13
client/tailscale/tailscale.go

@@ -90,6 +90,27 @@ func DoLocalRequest(req *http.Request) (*http.Response, error) {
 	return tsClient.Do(req)
 	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 {
 type errorJSON struct {
 	Error string
 	Error string
 }
 }
@@ -140,23 +161,11 @@ func send(ctx context.Context, method, path string, wantStatus int, body io.Read
 	if err != nil {
 	if err != nil {
 		return nil, err
 		return nil, err
 	}
 	}
-	res, err := DoLocalRequest(req)
+	res, err := doLocalRequestNiceError(req)
 	if err != nil {
 	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
 		return nil, err
 	}
 	}
 	defer res.Body.Close()
 	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)
 	slurp, err := ioutil.ReadAll(res.Body)
 	if err != nil {
 	if err != nil {
 		return nil, err
 		return nil, err
@@ -295,6 +304,30 @@ func FileTargets(ctx context.Context) ([]apitype.FileTarget, error) {
 	return fts, nil
 	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 {
 func CheckIPForwarding(ctx context.Context) error {
 	body, err := get200(ctx, "/localapi/v0/check-ip-forwarding")
 	body, err := get200(ctx, "/localapi/v0/check-ip-forwarding")
 	if err != nil {
 	if err != nil {

+ 8 - 20
cmd/tailscale/cli/file.go

@@ -11,11 +11,9 @@ import (
 	"flag"
 	"flag"
 	"fmt"
 	"fmt"
 	"io"
 	"io"
-	"io/ioutil"
 	"log"
 	"log"
 	"mime"
 	"mime"
 	"net/http"
 	"net/http"
-	"net/url"
 	"os"
 	"os"
 	"path/filepath"
 	"path/filepath"
 	"strconv"
 	"strconv"
@@ -30,6 +28,7 @@ import (
 	"tailscale.com/client/tailscale/apitype"
 	"tailscale.com/client/tailscale/apitype"
 	"tailscale.com/ipn"
 	"tailscale.com/ipn"
 	"tailscale.com/net/tsaddr"
 	"tailscale.com/net/tsaddr"
+	"tailscale.com/tailcfg"
 	"tailscale.com/version"
 	"tailscale.com/version"
 )
 )
 
 
@@ -96,7 +95,7 @@ func runCp(ctx context.Context, args []string) error {
 		return err
 		return err
 	}
 	}
 
 
-	peerAPIBase, isOffline, err := discoverPeerAPIBase(ctx, ip)
+	stableID, isOffline, err := getTargetStableID(ctx, ip)
 	if err != nil {
 	if err != nil {
 		return fmt.Errorf("can't send to %s: %v", target, err)
 		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 {
 		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 {
 		if err != nil {
 			return err
 			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
 	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)
 	ip, err := netaddr.ParseIP(ipStr)
 	if err != nil {
 	if err != nil {
 		return "", false, err
 		return "", false, err
@@ -195,7 +183,7 @@ func discoverPeerAPIBase(ctx context.Context, ipStr string) (base string, isOffl
 				continue
 				continue
 			}
 			}
 			isOffline = n.Online != nil && !*n.Online
 			isOffline = n.Online != nil && !*n.Online
-			return ft.PeerAPIURL, isOffline, nil
+			return n.StableID, isOffline, nil
 		}
 		}
 	}
 	}
 	return "", false, fileTargetErrorDetail(ctx, ip)
 	return "", false, fileTargetErrorDetail(ctx, ip)

+ 19 - 0
ipn/localapi/localapi.go

@@ -376,6 +376,25 @@ func (h *Handler) serveFileTargets(w http.ResponseWriter, r *http.Request) {
 	json.NewEncoder(w).Encode(fts)
 	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) {
 func (h *Handler) serveFilePut(w http.ResponseWriter, r *http.Request) {
 	if !h.PermitWrite {
 	if !h.PermitWrite {
 		http.Error(w, "file access denied", http.StatusForbidden)
 		http.Error(w, "file access denied", http.StatusForbidden)