diff --git a/ipn/ipnlocal/peerapi.go b/ipn/ipnlocal/peerapi.go index 91b906542..37d8c655c 100644 --- a/ipn/ipnlocal/peerapi.go +++ b/ipn/ipnlocal/peerapi.go @@ -620,9 +620,31 @@ func (f *incomingFile) PartialFile() ipn.PartialFile { } } +// canPutFile reports whether h can put a file ("Taildrop") to this node. +func (h *peerAPIHandler) canPutFile() bool { + if h.isSelf { + return true + } + if h.peerNode == nil { + // Shouldn't happen, but in case. + return false + } + for _, addr := range h.peerNode.Addresses { + if !addr.IsSingleIP() { + continue + } + for _, cap := range h.ps.b.PeerCaps(addr.IP()) { + if cap == tailcfg.CapabilityFileSharingSend { + return true + } + } + } + return false +} + func (h *peerAPIHandler) handlePeerPut(w http.ResponseWriter, r *http.Request) { - if !h.isSelf { - http.Error(w, "not owner", http.StatusForbidden) + if !h.canPutFile() { + http.Error(w, "Taildrop access denied", http.StatusForbidden) return } if !h.ps.b.hasCapFileSharing() { diff --git a/ipn/ipnlocal/peerapi_test.go b/ipn/ipnlocal/peerapi_test.go index 18f69f999..11a889853 100644 --- a/ipn/ipnlocal/peerapi_test.go +++ b/ipn/ipnlocal/peerapi_test.go @@ -158,7 +158,7 @@ func TestHandlePeerAPI(t *testing.T) { req: httptest.NewRequest("PUT", "/v0/put/foo", nil), checks: checks( httpStatus(http.StatusForbidden), - bodyContains("not owner"), + bodyContains("Taildrop access denied"), ), }, { diff --git a/tailcfg/tailcfg.go b/tailcfg/tailcfg.go index 57c991c28..c6b556d69 100644 --- a/tailcfg/tailcfg.go +++ b/tailcfg/tailcfg.go @@ -1577,8 +1577,16 @@ type Oauth2Token struct { } const ( + // MapResponse.Node self capabilities. + CapabilityFileSharing = "https://tailscale.com/cap/file-sharing" CapabilityAdmin = "https://tailscale.com/cap/is-admin" + + // Inter-node capabilities. + + // CapabilityFileSharingSend grants the ability to receive files from a + // node that's owned by a different user. + CapabilityFileSharingSend = "https://tailscale.com/cap/file-send" ) // SetDNSRequest is a request to add a DNS record.