ipn/ipnlocal, etc: require file sharing capability to send/recv files

tailscale/corp#1582

Signed-off-by: Brad Fitzpatrick <bradfitz@tailscale.com>
This commit is contained in:
Brad Fitzpatrick 2021-04-16 10:57:46 -07:00
parent 2f422434aa
commit b993d9802a
8 changed files with 76 additions and 4 deletions

View File

@ -66,6 +66,20 @@ func DoLocalRequest(req *http.Request) (*http.Response, error) {
return tsClient.Do(req) 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) { 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) req, err := http.NewRequestWithContext(ctx, method, "http://local-tailscaled.sock"+path, body)
if err != nil { if err != nil {
@ -81,7 +95,8 @@ func send(ctx context.Context, method, path string, wantStatus int, body io.Read
return nil, err return nil, err
} }
if res.StatusCode != wantStatus { if res.StatusCode != wantStatus {
return nil, fmt.Errorf("HTTP %s: %s (expected %v)", res.Status, slurp, wantStatus) err := fmt.Errorf("HTTP %s: %s (expected %v)", res.Status, slurp, wantStatus)
return nil, bestError(err, slurp)
} }
return slurp, nil return slurp, nil
} }

View File

@ -829,6 +829,9 @@ func (c *Direct) sendMapRequest(ctx context.Context, maxPolls int, cb func(*netm
p.Endpoints = []string{"127.9.9.9:456"} p.Endpoints = []string{"127.9.9.9:456"}
} }
} }
if Debug.StripCaps {
resp.Node.Capabilities = nil
}
if pf := resp.PacketFilter; pf != nil { if pf := resp.PacketFilter; pf != nil {
lastParsedPacketFilter = c.parsePacketFilter(pf) lastParsedPacketFilter = c.parsePacketFilter(pf)
@ -1072,6 +1075,7 @@ type debug struct {
OnlyDisco bool OnlyDisco bool
Disco bool Disco bool
StripEndpoints bool // strip endpoints from control (only use disco messages) StripEndpoints bool // strip endpoints from control (only use disco messages)
StripCaps bool // strip all local node's control-provided capabilities
} }
func initDebug() debug { func initDebug() debug {
@ -1080,6 +1084,7 @@ func initDebug() debug {
NetMap: envBool("TS_DEBUG_NETMAP"), NetMap: envBool("TS_DEBUG_NETMAP"),
ProxyDNS: envBool("TS_DEBUG_PROXY_DNS"), ProxyDNS: envBool("TS_DEBUG_PROXY_DNS"),
StripEndpoints: envBool("TS_DEBUG_STRIP_ENDPOINTS"), StripEndpoints: envBool("TS_DEBUG_STRIP_ENDPOINTS"),
StripCaps: envBool("TS_DEBUG_STRIP_CAPS"),
OnlyDisco: use == "only", OnlyDisco: use == "only",
Disco: use == "only" || use == "" || envBool("TS_DEBUG_USE_DISCO"), Disco: use == "only" || use == "" || envBool("TS_DEBUG_USE_DISCO"),
} }

View File

@ -2215,12 +2215,36 @@ func (b *LocalBackend) OpenFile(name string) (rc io.ReadCloser, size int64, err
return apiSrv.OpenFile(name) return apiSrv.OpenFile(name)
} }
// hasCapFileSharing reports whether the current node has the file
// sharing capability enabled.
func (b *LocalBackend) hasCapFileSharing() bool {
b.mu.Lock()
defer b.mu.Unlock()
return b.hasCapFileSharingLocked()
}
func (b *LocalBackend) hasCapFileSharingLocked() bool {
nm := b.netMap
if nm == nil || nm.SelfNode == nil {
return false
}
for _, c := range nm.SelfNode.Capabilities {
if c == tailcfg.CapabilityFileSharing {
return true
}
}
return false
}
// FileTargets lists nodes that the current node can send files to. // FileTargets lists nodes that the current node can send files to.
func (b *LocalBackend) FileTargets() ([]*apitype.FileTarget, error) { func (b *LocalBackend) FileTargets() ([]*apitype.FileTarget, error) {
var ret []*apitype.FileTarget var ret []*apitype.FileTarget
b.mu.Lock() b.mu.Lock()
defer b.mu.Unlock() defer b.mu.Unlock()
if !b.hasCapFileSharingLocked() {
return nil, errors.New("file sharing not enabled by Tailscale admin")
}
nm := b.netMap nm := b.netMap
if b.state != ipn.Running || nm == nil { if b.state != ipn.Running || nm == nil {
return nil, errors.New("not connected") return nil, errors.New("not connected")

View File

@ -410,6 +410,10 @@ func (h *peerAPIHandler) put(w http.ResponseWriter, r *http.Request) {
http.Error(w, "not owner", http.StatusForbidden) http.Error(w, "not owner", http.StatusForbidden)
return return
} }
if !h.ps.b.hasCapFileSharing() {
http.Error(w, "file sharing not enabled by Tailscale admin", http.StatusForbidden)
return
}
if r.Method != "PUT" { if r.Method != "PUT" {
http.Error(w, "not method PUT", http.StatusMethodNotAllowed) http.Error(w, "not method PUT", http.StatusMethodNotAllowed)
return return

View File

@ -88,7 +88,8 @@ type PeerStatus struct {
KeepAlive bool KeepAlive bool
ExitNode bool // true if this is the currently selected exit node. ExitNode bool // true if this is the currently selected exit node.
PeerAPIURL []string PeerAPIURL []string
Capabilities []string `json:",omitempty"`
// ShareeNode indicates this node exists in the netmap because // ShareeNode indicates this node exists in the netmap because
// it's owned by a shared-to user and that node might connect // it's owned by a shared-to user and that node might connect

View File

@ -9,6 +9,7 @@
"crypto/rand" "crypto/rand"
"encoding/hex" "encoding/hex"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"io" "io"
"net" "net"
@ -299,6 +300,18 @@ func (h *Handler) serveFiles(w http.ResponseWriter, r *http.Request) {
io.Copy(w, rc) io.Copy(w, rc)
} }
func writeErrorJSON(w http.ResponseWriter, err error) {
if err == nil {
err = errors.New("unexpected nil error")
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(500)
type E struct {
Error string `json:"error"`
}
json.NewEncoder(w).Encode(E{err.Error()})
}
func (h *Handler) serveFileTargets(w http.ResponseWriter, r *http.Request) { func (h *Handler) serveFileTargets(w http.ResponseWriter, r *http.Request) {
if !h.PermitRead { if !h.PermitRead {
http.Error(w, "access denied", http.StatusForbidden) http.Error(w, "access denied", http.StatusForbidden)
@ -310,7 +323,7 @@ func (h *Handler) serveFileTargets(w http.ResponseWriter, r *http.Request) {
} }
fts, err := h.b.FileTargets() fts, err := h.b.FileTargets()
if err != nil { if err != nil {
http.Error(w, err.Error(), 500) writeErrorJSON(w, err)
return return
} }
makeNonNil(&fts) makeNonNil(&fts)

View File

@ -177,7 +177,7 @@ type Node struct {
// They're free-form strings, but should be in the form of URLs/URIs // They're free-form strings, but should be in the form of URLs/URIs
// such as: // such as:
// "https://tailscale.com/cap/is-admin" // "https://tailscale.com/cap/is-admin"
// "https://tailscale.com/cap/recv-file" // "https://tailscale.com/cap/file-sharing"
Capabilities []string `json:",omitempty"` Capabilities []string `json:",omitempty"`
// The following three computed fields hold the various names that can // The following three computed fields hold the various names that can
@ -1140,3 +1140,8 @@ type Oauth2Token struct {
// mechanisms for that TokenSource will not be used. // mechanisms for that TokenSource will not be used.
Expiry time.Time `json:"expiry,omitempty"` Expiry time.Time `json:"expiry,omitempty"`
} }
const (
CapabilityFileSharing = "https://tailscale.com/cap/file-sharing"
CapabilityAdmin = "https://tailscale.com/cap/is-admin"
)

View File

@ -2984,6 +2984,11 @@ func (c *Conn) UpdateStatus(sb *ipnstate.StatusBuilder) {
ss.HostName = c.netMap.Hostinfo.Hostname ss.HostName = c.netMap.Hostinfo.Hostname
ss.DNSName = c.netMap.Name ss.DNSName = c.netMap.Name
ss.UserID = c.netMap.User ss.UserID = c.netMap.User
if c.netMap.SelfNode != nil {
if c := c.netMap.SelfNode.Capabilities; len(c) > 0 {
ss.Capabilities = append([]string(nil), c...)
}
}
} else { } else {
ss.HostName, _ = os.Hostname() ss.HostName, _ = os.Hostname()
} }