mirror of
https://github.com/tailscale/tailscale.git
synced 2025-08-22 11:01:54 +00:00
tailscale: update tailfs file and package names (#11590)
This change updates the tailfs file and package names to their new naming convention. Updates #tailscale/corp#16827 Signed-off-by: Charlotte Brandhorst-Satzkorn <charlotte@tailscale.com>
This commit is contained in:

committed by
GitHub

parent
1c259100b0
commit
14683371ee
412
drive/driveimpl/remote_impl.go
Normal file
412
drive/driveimpl/remote_impl.go
Normal file
@@ -0,0 +1,412 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package driveimpl
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"log"
|
||||
"math"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/user"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/tailscale/xnet/webdav"
|
||||
"tailscale.com/drive"
|
||||
"tailscale.com/drive/driveimpl/compositedav"
|
||||
"tailscale.com/drive/driveimpl/dirfs"
|
||||
"tailscale.com/drive/driveimpl/shared"
|
||||
"tailscale.com/safesocket"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
func NewFileSystemForRemote(logf logger.Logf) *FileSystemForRemote {
|
||||
if logf == nil {
|
||||
logf = log.Printf
|
||||
}
|
||||
fs := &FileSystemForRemote{
|
||||
logf: logf,
|
||||
lockSystem: webdav.NewMemLS(),
|
||||
children: make(map[string]*compositedav.Child),
|
||||
userServers: make(map[string]*userServer),
|
||||
}
|
||||
return fs
|
||||
}
|
||||
|
||||
// FileSystemForRemote implements tailfs.FileSystemForRemote.
|
||||
type FileSystemForRemote struct {
|
||||
logf logger.Logf
|
||||
lockSystem webdav.LockSystem
|
||||
|
||||
// mu guards the below values. Acquire a write lock before updating any of
|
||||
// them, acquire a read lock before reading any of them.
|
||||
mu sync.RWMutex
|
||||
fileServerAddr string
|
||||
shares []*drive.Share
|
||||
children map[string]*compositedav.Child
|
||||
userServers map[string]*userServer
|
||||
}
|
||||
|
||||
// SetFileServerAddr implements tailfs.FileSystemForRemote.
|
||||
func (s *FileSystemForRemote) SetFileServerAddr(addr string) {
|
||||
s.mu.Lock()
|
||||
s.fileServerAddr = addr
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
// SetShares implements tailfs.FileSystemForRemote. Shares must be sorted
|
||||
// according to tailfs.CompareShares.
|
||||
func (s *FileSystemForRemote) SetShares(shares []*drive.Share) {
|
||||
userServers := make(map[string]*userServer)
|
||||
if drive.AllowShareAs() {
|
||||
// Set up per-user server by running the current executable as an
|
||||
// unprivileged user in order to avoid privilege escalation.
|
||||
executable, err := os.Executable()
|
||||
if err != nil {
|
||||
s.logf("can't find executable: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
for _, share := range shares {
|
||||
p, found := userServers[share.As]
|
||||
if !found {
|
||||
p = &userServer{
|
||||
logf: s.logf,
|
||||
username: share.As,
|
||||
executable: executable,
|
||||
}
|
||||
userServers[share.As] = p
|
||||
}
|
||||
p.shares = append(p.shares, share)
|
||||
}
|
||||
for _, p := range userServers {
|
||||
go p.runLoop()
|
||||
}
|
||||
}
|
||||
|
||||
children := make(map[string]*compositedav.Child, len(shares))
|
||||
for _, share := range shares {
|
||||
children[share.Name] = s.buildChild(share)
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
s.shares = shares
|
||||
oldUserServers := s.userServers
|
||||
oldChildren := s.children
|
||||
s.children = children
|
||||
s.userServers = userServers
|
||||
s.mu.Unlock()
|
||||
|
||||
s.stopUserServers(oldUserServers)
|
||||
s.closeChildren(oldChildren)
|
||||
}
|
||||
|
||||
func (s *FileSystemForRemote) buildChild(share *drive.Share) *compositedav.Child {
|
||||
return &compositedav.Child{
|
||||
Child: &dirfs.Child{
|
||||
Name: share.Name,
|
||||
},
|
||||
BaseURL: fmt.Sprintf("http://%v/%v", hex.EncodeToString([]byte(share.Name)), url.PathEscape(share.Name)),
|
||||
Transport: &http.Transport{
|
||||
Dial: func(_, shareAddr string) (net.Conn, error) {
|
||||
shareNameHex, _, err := net.SplitHostPort(shareAddr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to parse share address %v: %w", shareAddr, err)
|
||||
}
|
||||
|
||||
// We had to encode the share name in hex to make sure it's a valid hostname
|
||||
shareNameBytes, err := hex.DecodeString(shareNameHex)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to decode share name from host %v: %v", shareNameHex, err)
|
||||
}
|
||||
shareName := string(shareNameBytes)
|
||||
|
||||
s.mu.RLock()
|
||||
var share *drive.Share
|
||||
i, shareFound := slices.BinarySearchFunc(s.shares, shareName, func(s *drive.Share, name string) int {
|
||||
return strings.Compare(s.Name, name)
|
||||
})
|
||||
if shareFound {
|
||||
share = s.shares[i]
|
||||
}
|
||||
userServers := s.userServers
|
||||
fileServerAddr := s.fileServerAddr
|
||||
s.mu.RUnlock()
|
||||
|
||||
if !shareFound {
|
||||
return nil, fmt.Errorf("unknown share %v", shareName)
|
||||
}
|
||||
|
||||
var addr string
|
||||
if !drive.AllowShareAs() {
|
||||
addr = fileServerAddr
|
||||
} else {
|
||||
userServer, found := userServers[share.As]
|
||||
if found {
|
||||
userServer.mu.RLock()
|
||||
addr = userServer.addr
|
||||
userServer.mu.RUnlock()
|
||||
}
|
||||
}
|
||||
|
||||
if addr == "" {
|
||||
return nil, fmt.Errorf("unable to determine address for share %v", shareName)
|
||||
}
|
||||
|
||||
_, err = netip.ParseAddrPort(addr)
|
||||
if err == nil {
|
||||
// this is a regular network address, dial normally
|
||||
return net.Dial("tcp", addr)
|
||||
}
|
||||
// assume this is a safesocket address
|
||||
return safesocket.Connect(addr)
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// ServeHTTPWithPerms implements tailfs.FileSystemForRemote.
|
||||
func (s *FileSystemForRemote) ServeHTTPWithPerms(permissions drive.Permissions, w http.ResponseWriter, r *http.Request) {
|
||||
isWrite := writeMethods[r.Method]
|
||||
if isWrite {
|
||||
share := shared.CleanAndSplit(r.URL.Path)[0]
|
||||
switch permissions.For(share) {
|
||||
case drive.PermissionNone:
|
||||
// If we have no permissions to this share, treat it as not found
|
||||
// to avoid leaking any information about the share's existence.
|
||||
http.Error(w, "not found", http.StatusNotFound)
|
||||
return
|
||||
case drive.PermissionReadOnly:
|
||||
http.Error(w, "permission denied", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
s.mu.RLock()
|
||||
childrenMap := s.children
|
||||
s.mu.RUnlock()
|
||||
|
||||
children := make([]*compositedav.Child, 0, len(childrenMap))
|
||||
// filter out shares to which the connecting principal has no access
|
||||
for name, child := range childrenMap {
|
||||
if permissions.For(name) == drive.PermissionNone {
|
||||
continue
|
||||
}
|
||||
|
||||
children = append(children, child)
|
||||
}
|
||||
|
||||
h := compositedav.Handler{
|
||||
Logf: s.logf,
|
||||
}
|
||||
h.SetChildren("", children...)
|
||||
h.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
func (s *FileSystemForRemote) stopUserServers(userServers map[string]*userServer) {
|
||||
for _, server := range userServers {
|
||||
if err := server.Close(); err != nil {
|
||||
s.logf("error closing tailfs user server: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *FileSystemForRemote) closeChildren(children map[string]*compositedav.Child) {
|
||||
for _, child := range children {
|
||||
child.CloseIdleConnections()
|
||||
}
|
||||
}
|
||||
|
||||
// Close() implements tailfs.FileSystemForRemote.
|
||||
func (s *FileSystemForRemote) Close() error {
|
||||
s.mu.Lock()
|
||||
userServers := s.userServers
|
||||
children := s.children
|
||||
s.userServers = make(map[string]*userServer)
|
||||
s.children = make(map[string]*compositedav.Child)
|
||||
s.mu.Unlock()
|
||||
|
||||
s.stopUserServers(userServers)
|
||||
s.closeChildren(children)
|
||||
return nil
|
||||
}
|
||||
|
||||
// userServer runs tailscaled serve-tailfs to serve webdav content for the
|
||||
// given Shares. All Shares are assumed to have the same Share.As, and the
|
||||
// content is served as that Share.As user.
|
||||
type userServer struct {
|
||||
logf logger.Logf
|
||||
shares []*drive.Share
|
||||
username string
|
||||
executable string
|
||||
|
||||
// mu guards the below values. Acquire a write lock before updating any of
|
||||
// them, acquire a read lock before reading any of them.
|
||||
mu sync.RWMutex
|
||||
cmd *exec.Cmd
|
||||
addr string
|
||||
closed bool
|
||||
}
|
||||
|
||||
func (s *userServer) Close() error {
|
||||
s.mu.Lock()
|
||||
cmd := s.cmd
|
||||
s.closed = true
|
||||
s.mu.Unlock()
|
||||
if cmd != nil && cmd.Process != nil {
|
||||
return cmd.Process.Kill()
|
||||
}
|
||||
// not running, that's okay
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *userServer) runLoop() {
|
||||
maxSleepTime := 30 * time.Second
|
||||
consecutiveFailures := float64(0)
|
||||
var timeOfLastFailure time.Time
|
||||
for {
|
||||
s.mu.RLock()
|
||||
closed := s.closed
|
||||
s.mu.RUnlock()
|
||||
if closed {
|
||||
return
|
||||
}
|
||||
|
||||
err := s.run()
|
||||
now := time.Now()
|
||||
timeSinceLastFailure := now.Sub(timeOfLastFailure)
|
||||
timeOfLastFailure = now
|
||||
if timeSinceLastFailure < maxSleepTime {
|
||||
consecutiveFailures++
|
||||
} else {
|
||||
consecutiveFailures = 1
|
||||
}
|
||||
sleepTime := time.Duration(math.Pow(2, consecutiveFailures)) * time.Millisecond
|
||||
if sleepTime > maxSleepTime {
|
||||
sleepTime = maxSleepTime
|
||||
}
|
||||
s.logf("user server % v stopped with error %v, will try again in %v", s.executable, err, sleepTime)
|
||||
time.Sleep(sleepTime)
|
||||
}
|
||||
}
|
||||
|
||||
// Run runs the user server using the configured executable. This function only
|
||||
// works on UNIX systems, but those are the only ones on which we use
|
||||
// userServers anyway.
|
||||
func (s *userServer) run() error {
|
||||
// set up the command
|
||||
args := []string{"serve-tailfs"}
|
||||
for _, s := range s.shares {
|
||||
args = append(args, s.Name, s.Path)
|
||||
}
|
||||
var cmd *exec.Cmd
|
||||
if s.canSudo() {
|
||||
s.logf("starting TailFS file server as user %q", s.username)
|
||||
allArgs := []string{"-n", "-u", s.username, s.executable}
|
||||
allArgs = append(allArgs, args...)
|
||||
cmd = exec.Command("sudo", allArgs...)
|
||||
} else {
|
||||
// If we were root, we should have been able to sudo as a specific
|
||||
// user, but let's check just to make sure, since we never want to
|
||||
// access shared folders as root.
|
||||
err := s.assertNotRoot()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.logf("starting TailFS file server as ourselves")
|
||||
cmd = exec.Command(s.executable, args...)
|
||||
}
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
return fmt.Errorf("stdout pipe: %w", err)
|
||||
}
|
||||
defer stdout.Close()
|
||||
stderr, err := cmd.StderrPipe()
|
||||
if err != nil {
|
||||
return fmt.Errorf("stderr pipe: %w", err)
|
||||
}
|
||||
defer stderr.Close()
|
||||
|
||||
err = cmd.Start()
|
||||
if err != nil {
|
||||
return fmt.Errorf("start: %w", err)
|
||||
}
|
||||
s.mu.Lock()
|
||||
s.cmd = cmd
|
||||
s.mu.Unlock()
|
||||
|
||||
// read address
|
||||
stdoutScanner := bufio.NewScanner(stdout)
|
||||
stdoutScanner.Scan()
|
||||
if stdoutScanner.Err() != nil {
|
||||
return fmt.Errorf("read addr: %w", stdoutScanner.Err())
|
||||
}
|
||||
addr := stdoutScanner.Text()
|
||||
// send the rest of stdout and stderr to logger to avoid blocking
|
||||
go func() {
|
||||
for stdoutScanner.Scan() {
|
||||
s.logf("tailscaled serve-tailfs stdout: %v", stdoutScanner.Text())
|
||||
}
|
||||
}()
|
||||
stderrScanner := bufio.NewScanner(stderr)
|
||||
go func() {
|
||||
for stderrScanner.Scan() {
|
||||
s.logf("tailscaled serve-tailfs stderr: %v", stderrScanner.Text())
|
||||
}
|
||||
}()
|
||||
s.mu.Lock()
|
||||
s.addr = strings.TrimSpace(addr)
|
||||
s.mu.Unlock()
|
||||
return cmd.Wait()
|
||||
}
|
||||
|
||||
var writeMethods = map[string]bool{
|
||||
"PUT": true,
|
||||
"POST": true,
|
||||
"COPY": true,
|
||||
"LOCK": true,
|
||||
"UNLOCK": true,
|
||||
"MKCOL": true,
|
||||
"MOVE": true,
|
||||
"PROPPATCH": true,
|
||||
}
|
||||
|
||||
// canSudo checks wether we can sudo -u the configured executable as the
|
||||
// configured user by attempting to call the executable with the '-h' flag to
|
||||
// print help.
|
||||
func (s *userServer) canSudo() bool {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||
defer cancel()
|
||||
if err := exec.CommandContext(ctx, "sudo", "-n", "-u", s.username, s.executable, "-h").Run(); err != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// assertNotRoot returns an error if the current user has UID 0 or if we cannot
|
||||
// determine the current user.
|
||||
//
|
||||
// On Linux, root users will always have UID 0.
|
||||
//
|
||||
// On BSD, root users should always have UID 0.
|
||||
func (s *userServer) assertNotRoot() error {
|
||||
u, err := user.Current()
|
||||
if err != nil {
|
||||
return fmt.Errorf("assertNotRoot failed to find current user: %s", err)
|
||||
}
|
||||
if u.Uid == "0" {
|
||||
return fmt.Errorf("%q is root", u.Name)
|
||||
}
|
||||
return nil
|
||||
}
|
Reference in New Issue
Block a user