mirror of
https://github.com/tailscale/tailscale.git
synced 2025-08-11 13:18:53 +00:00
tailfs: initial implementation
Add a WebDAV-based folder sharing mechanism that is exposed to local clients at 100.100.100.100:8080 and to remote peers via a new peerapi endpoint at /v0/tailfs. Add the ability to manage folder sharing via the new 'share' CLI sub-command. Updates tailscale/corp#16827 Signed-off-by: Percy Wegmann <percy@tailscale.com>
This commit is contained in:

committed by
Percy Wegmann

parent
2e404b769d
commit
993acf4475
@@ -18,7 +18,9 @@ import (
|
||||
"net/http/httputil"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strconv"
|
||||
@@ -41,6 +43,7 @@ import (
|
||||
"tailscale.com/net/portmapper"
|
||||
"tailscale.com/tailcfg"
|
||||
"tailscale.com/taildrop"
|
||||
"tailscale.com/tailfs"
|
||||
"tailscale.com/tka"
|
||||
"tailscale.com/tstime"
|
||||
"tailscale.com/types/key"
|
||||
@@ -107,6 +110,8 @@ var handler = map[string]localAPIHandler{
|
||||
"serve-config": (*Handler).serveServeConfig,
|
||||
"set-dns": (*Handler).serveSetDNS,
|
||||
"set-expiry-sooner": (*Handler).serveSetExpirySooner,
|
||||
"tailfs/fileserver-address": (*Handler).serveTailfsFileServerAddr,
|
||||
"tailfs/shares": (*Handler).serveShares,
|
||||
"start": (*Handler).serveStart,
|
||||
"status": (*Handler).serveStatus,
|
||||
"tka/init": (*Handler).serveTKAInit,
|
||||
@@ -1107,7 +1112,7 @@ func (h *Handler) connIsLocalAdmin() bool {
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
// Short timeout just in case sudo hands for some reason.
|
||||
// Short timeout just in case sudo hangs for some reason.
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
if err := exec.CommandContext(ctx, "sudo", "--other-user="+u.Name, "--list", "tailscale").Run(); err != nil {
|
||||
@@ -1120,6 +1125,34 @@ func (h *Handler) connIsLocalAdmin() bool {
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) getUsername() (string, error) {
|
||||
if h.ConnIdentity == nil {
|
||||
h.logf("[unexpected] missing ConnIdentity in LocalAPI Handler")
|
||||
return "", errors.New("missing ConnIdentity")
|
||||
}
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
tok, err := h.ConnIdentity.WindowsToken()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("get windows token: %w", err)
|
||||
}
|
||||
defer tok.Close()
|
||||
return tok.Username()
|
||||
case "darwin", "linux":
|
||||
uid, ok := h.ConnIdentity.Creds().UserID()
|
||||
if !ok {
|
||||
return "", errors.New("missing user ID")
|
||||
}
|
||||
u, err := osuser.LookupByUID(uid)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("lookup user: %w", err)
|
||||
}
|
||||
return u.Username, nil
|
||||
default:
|
||||
return "", errors.New("unsupported OS")
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) serveCheckIPForwarding(w http.ResponseWriter, r *http.Request) {
|
||||
if !h.PermitRead {
|
||||
http.Error(w, "IP forwarding check access denied", http.StatusForbidden)
|
||||
@@ -2498,6 +2531,95 @@ func (h *Handler) serveUpdateProgress(w http.ResponseWriter, r *http.Request) {
|
||||
json.NewEncoder(w).Encode(ups)
|
||||
}
|
||||
|
||||
// serveTailfsFileServerAddr handles updates of the tailfs file server address.
|
||||
func (h *Handler) serveTailfsFileServerAddr(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "PUT" {
|
||||
http.Error(w, "only PUT allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
b, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
h.b.TailfsSetFileServerAddr(string(b))
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
}
|
||||
|
||||
// serveShares handles the management of tailfs shares.
|
||||
func (h *Handler) serveShares(w http.ResponseWriter, r *http.Request) {
|
||||
if !h.b.TailfsSharingEnabled() {
|
||||
http.Error(w, `tailfs sharing not enabled, please add the attribute "tailfs:share" to this node in your ACLs' "nodeAttrs" section`, http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
switch r.Method {
|
||||
case "PUT":
|
||||
var share tailfs.Share
|
||||
err := json.NewDecoder(r.Body).Decode(&share)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
share.Path = path.Clean(share.Path)
|
||||
fi, err := os.Stat(share.Path)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if !fi.IsDir() {
|
||||
http.Error(w, "not a directory", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if tailfs.AllowShareAs() {
|
||||
// share as the connected user
|
||||
username, err := h.getUsername()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
share.As = username
|
||||
}
|
||||
err = h.b.TailfsAddShare(&share)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
case "DELETE":
|
||||
var share tailfs.Share
|
||||
err := json.NewDecoder(r.Body).Decode(&share)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
err = h.b.TailfsRemoveShare(share.Name)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
http.Error(w, "share not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
case "GET":
|
||||
shares, err := h.b.TailfsGetShares()
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
err = json.NewEncoder(w).Encode(shares)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
default:
|
||||
http.Error(w, "unsupported method", http.StatusMethodNotAllowed)
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
metricInvalidRequests = clientmetric.NewCounter("localapi_invalid_requests")
|
||||
|
||||
|
Reference in New Issue
Block a user