mirror of
https://github.com/tailscale/tailscale.git
synced 2026-02-07 22:42:02 -05:00
This file was never truly necessary and has never actually been used in the history of Tailscale's open source releases. A Brief History of AUTHORS files --- The AUTHORS file was a pattern developed at Google, originally for Chromium, then adopted by Go and a bunch of other projects. The problem was that Chromium originally had a copyright line only recognizing Google as the copyright holder. Because Google (and most open source projects) do not require copyright assignemnt for contributions, each contributor maintains their copyright. Some large corporate contributors then tried to add their own name to the copyright line in the LICENSE file or in file headers. This quickly becomes unwieldy, and puts a tremendous burden on anyone building on top of Chromium, since the license requires that they keep all copyright lines intact. The compromise was to create an AUTHORS file that would list all of the copyright holders. The LICENSE file and source file headers would then include that list by reference, listing the copyright holder as "The Chromium Authors". This also become cumbersome to simply keep the file up to date with a high rate of new contributors. Plus it's not always obvious who the copyright holder is. Sometimes it is the individual making the contribution, but many times it may be their employer. There is no way for the proejct maintainer to know. Eventually, Google changed their policy to no longer recommend trying to keep the AUTHORS file up to date proactively, and instead to only add to it when requested: https://opensource.google/docs/releasing/authors. They are also clear that: > Adding contributors to the AUTHORS file is entirely within the > project's discretion and has no implications for copyright ownership. It was primarily added to appease a small number of large contributors that insisted that they be recognized as copyright holders (which was entirely their right to do). But it's not truly necessary, and not even the most accurate way of identifying contributors and/or copyright holders. In practice, we've never added anyone to our AUTHORS file. It only lists Tailscale, so it's not really serving any purpose. It also causes confusion because Tailscalars put the "Tailscale Inc & AUTHORS" header in other open source repos which don't actually have an AUTHORS file, so it's ambiguous what that means. Instead, we just acknowledge that the contributors to Tailscale (whoever they are) are copyright holders for their individual contributions. We also have the benefit of using the DCO (developercertificate.org) which provides some additional certification of their right to make the contribution. The source file changes were purely mechanical with: git ls-files | xargs sed -i -e 's/\(Tailscale Inc &\) AUTHORS/\1 contributors/g' Updates #cleanup Change-Id: Ia101a4a3005adb9118051b3416f5a64a4a45987d Signed-off-by: Will Norris <will@tailscale.com>
470 lines
12 KiB
Go
470 lines
12 KiB
Go
// Copyright (c) Tailscale Inc & contributors
|
|
// 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 drive.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
|
|
// fileServerTokenAndAddr is the secretToken|fileserverAddress
|
|
fileServerTokenAndAddr string
|
|
shares []*drive.Share
|
|
children map[string]*compositedav.Child
|
|
userServers map[string]*userServer
|
|
}
|
|
|
|
// SetFileServerAddr implements drive.FileSystemForRemote.
|
|
func (s *FileSystemForRemote) SetFileServerAddr(addr string) {
|
|
s.mu.Lock()
|
|
s.fileServerTokenAndAddr = addr
|
|
s.mu.Unlock()
|
|
}
|
|
|
|
// SetShares implements drive.FileSystemForRemote. Shares must be sorted
|
|
// according to drive.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 {
|
|
getTokenAndAddr := func(shareName string) (string, string, error) {
|
|
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
|
|
fileServerTokenAndAddr := s.fileServerTokenAndAddr
|
|
s.mu.RUnlock()
|
|
|
|
if !shareFound {
|
|
return "", "", fmt.Errorf("unknown share %v", shareName)
|
|
}
|
|
|
|
var tokenAndAddr string
|
|
if !drive.AllowShareAs() {
|
|
tokenAndAddr = fileServerTokenAndAddr
|
|
} else {
|
|
userServer, found := userServers[share.As]
|
|
if found {
|
|
userServer.mu.RLock()
|
|
tokenAndAddr = userServer.tokenAndAddr
|
|
userServer.mu.RUnlock()
|
|
}
|
|
}
|
|
|
|
if tokenAndAddr == "" {
|
|
return "", "", fmt.Errorf("unable to determine address for share %v", shareName)
|
|
}
|
|
|
|
parts := strings.Split(tokenAndAddr, "|")
|
|
if len(parts) != 2 {
|
|
return "", "", fmt.Errorf("invalid address for share %v", shareName)
|
|
}
|
|
|
|
return parts[0], parts[1], nil
|
|
}
|
|
|
|
return &compositedav.Child{
|
|
Child: &dirfs.Child{
|
|
Name: share.Name,
|
|
},
|
|
BaseURL: func() (string, error) {
|
|
secretToken, _, err := getTokenAndAddr(share.Name)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return fmt.Sprintf("http://%s/%s/%s", hex.EncodeToString([]byte(share.Name)), secretToken, url.PathEscape(share.Name)), nil
|
|
},
|
|
Transport: &http.Transport{
|
|
DialContext: func(ctx context.Context, _, 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)
|
|
|
|
_, addr, err := getTokenAndAddr(shareName)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
_, err = netip.ParseAddrPort(addr)
|
|
if err == nil {
|
|
// this is a regular network address, dial normally
|
|
var std net.Dialer
|
|
return std.DialContext(ctx, "tcp", addr)
|
|
}
|
|
// assume this is a safesocket address
|
|
return safesocket.ConnectContext(ctx, addr)
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
// ServeHTTPWithPerms implements drive.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 taildrive user server: %v", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (s *FileSystemForRemote) closeChildren(children map[string]*compositedav.Child) {
|
|
for _, child := range children {
|
|
child.CloseIdleConnections()
|
|
}
|
|
}
|
|
|
|
// Close() implements drive.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-taildrive 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
|
|
tokenAndAddr 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-taildrive"}
|
|
for _, s := range s.shares {
|
|
args = append(args, s.Name, s.Path)
|
|
}
|
|
var cmd *exec.Cmd
|
|
|
|
if s.canSudo() {
|
|
s.logf("starting taildrive file server with sudo as user %q", s.username)
|
|
allArgs := []string{"-n", "-u", s.username, s.executable}
|
|
allArgs = append(allArgs, args...)
|
|
cmd = exec.Command("sudo", allArgs...)
|
|
} else if su := s.canSU(); su != "" {
|
|
s.logf("starting taildrive file server with su as user %q", s.username)
|
|
// Quote and escape arguments. Use single quotes to prevent shell substitutions.
|
|
for i, arg := range args {
|
|
args[i] = "'" + strings.ReplaceAll(arg, "'", "'\"'\"'") + "'"
|
|
}
|
|
cmdString := fmt.Sprintf("%s %s", s.executable, strings.Join(args, " "))
|
|
allArgs := []string{s.username, "-c", cmdString}
|
|
cmd = exec.Command(su, allArgs...)
|
|
} else {
|
|
// If we were root, we should have been able to sudo or su 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 taildrive 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-taildrive stdout: %v", stdoutScanner.Text())
|
|
}
|
|
}()
|
|
stderrScanner := bufio.NewScanner(stderr)
|
|
go func() {
|
|
for stderrScanner.Scan() {
|
|
s.logf("tailscaled serve-taildrive stderr: %v", stderrScanner.Text())
|
|
}
|
|
}()
|
|
s.mu.Lock()
|
|
s.tokenAndAddr = 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,
|
|
"DELETE": 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
|
|
}
|
|
|
|
// canSU checks whether the current process can run su with the right username.
|
|
// If su can be run, this returns the path to the su command.
|
|
// If not, this returns the empty string "".
|
|
func (s *userServer) canSU() string {
|
|
su, err := exec.LookPath("su")
|
|
if err != nil {
|
|
s.logf("can't find su command: %v", err)
|
|
return ""
|
|
}
|
|
|
|
// First try to execute su <user> -c true to make sure we can su.
|
|
err = exec.Command(
|
|
su,
|
|
s.username,
|
|
"-c", "true",
|
|
).Run()
|
|
if err != nil {
|
|
s.logf("su check failed: %s", err)
|
|
return ""
|
|
}
|
|
|
|
return su
|
|
}
|
|
|
|
// 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
|
|
}
|